Fulfillment
Delivered codes exist in exactly one place — the response of an explicit, audited, counted reveal call.
Delivered secrets — gift-card codes, PINs, eSIM activation values, claim tokens — are never embedded in order bodies, lists, or webhooks. They exist in exactly one place: the response of an explicit, audited reveal call. This keeps codes out of your logs, your webhook infrastructure, and every cache between us and you, until the moment you actually need them.
Reveal an order's artifacts
POST /api/v1/orders/{orderId}/fulfillment
Idempotency-Key: <1–128 printable characters>
Requires a key with the orders.reveal scope, or with orders.create — a key that can buy may show what it bought. A key with orders.read alone is refused 403 forbidden: reading orders never reveals their codes. The call takes no body, and an Idempotency-Key header is required.
{
"orderId": "0198e000-1111-7abc-9def-222233334444",
"revealCount": 1,
"artifacts": [
{ "type": "code", "value": "XXXX-XXXX-XXXX-XXXX", "expiresAtUtc": null },
{ "type": "pin", "value": "4821", "expiresAtUtc": null }
],
"revealLimit": 10,
"remainingReveals": 9
}| Field | Meaning |
|---|---|
revealCount | How many times the order's codes have been revealed, this reveal included, across every channel (API, dashboard). Monitor it — a count you cannot account for means a leaked credential. |
revealLimit | How many times they may be revealed in all: ten, unless support raised the limit for the order. |
remainingReveals | How many reveals are left after this one. |
artifacts[].type | code (a redemption code), pin, activation_code (an eSIM activation value), link (a voucher page to open — see below), claim (a claim token), or confirmation (a service delivered without a secret; its value is completed). Treat the list as open and render types you do not recognize as opaque text. |
artifacts[].value | The secret itself. |
artifacts[].expiresAtUtc | Optional expiry for time-limited artifacts; null otherwise. |
An order can carry several artifacts (e.g. a card number and a PIN; multi-quantity orders carry one set per unit). A successful reveal always carries at least one; an order with nothing that can be shown is refused 409 fulfillment_unavailable rather than answered with an empty list.
A link is a secret too. Some brands deliver a card as a page the buyer opens rather than a code they type, and then the
whole of what was bought is that address — its path or query carries the voucher's own token. Treat it exactly as you treat a
code: keep it out of logs and analytics, never put it in a URL you build or a referrer you leak, and show it only to the buyer
it belongs to. Offer it as something to open and to copy; never fetch it yourself, because a voucher page can be single-use and
loading it can spend the card. The value is always an absolute https address, passed through byte for byte — do not re-encode
it, as that can break the token. Anyone who can read it can redeem the card, so a reveal spends one of the order's reveals
whichever kind comes back.
The workspace may reveal the codes of orders it already bought even while its subscription is overdue or its entitlement has ended (see Authentication).
The Idempotency-Key — a short replay window
The key makes a lost response recoverable without spending a reveal:
- An exact replay within 15 minutes — the same API key, the same
Idempotency-Key, the same order — returns the stored response of the reveal it repeats: the same artifacts and the samerevealCount. Nothing new is counted, but the replay is recorded in the audit trail like the reveal itself. - After 15 minutes the key is spent: the same key is refused
409 fulfillment_replay_expired. To show the codes again, reveal under a new key — a new, counted reveal. - The same key for another order is refused
409 idempotency_conflict— the same code an order's or a withdrawal's reused key answers with. Nothing is revealed and nothing is counted. (A reveal in the merchant dashboard answers a key bound elsewhere the same way.) - Keys belong to the API key that sent them: two of your API keys never share one, and a key presented by a different API key is
409 idempotency_conflicttoo.
Use a fresh key for every intended reveal (e.g. reveal-{orderId}-{attempt}) and reuse it only to retry that reveal after a timeout.
Check the allowance without revealing
GET /api/v1/orders/{orderId}/fulfillment/allowance (scope: orders.read)
How many more times the order's codes can be revealed — the same allowance the dashboard shows — read without revealing or counting anything:
{
"orderId": "0198e000-1111-7abc-9def-222233334444",
"revealable": true,
"revealCount": 1,
"revealLimit": 10,
"remainingReveals": 9
}revealable is false while the order is not fulfilled or has no codes to show; remainingReveals is then 0 and revealCount says how often the codes were shown while they could be. Another workspace's order, or one that does not exist, is 404 not_found. The response is sent with Cache-Control: no-store, private.
Response codes
| Status | code | Meaning |
|---|---|---|
| 200 | — | Artifacts returned. |
| 400 | invalid_request | Missing or malformed Idempotency-Key (empty, over 128 characters, or containing control characters). |
| 403 | forbidden | The key holds neither orders.reveal nor orders.create. |
| 403 | api_access_inactive | The workspace is suspended or its setup fee is unpaid. |
| 404 | not_found | No such order in your workspace. |
| 409 | fulfillment_unavailable | Your order has nothing to reveal: it is not fulfilled yet, it failed or was refunded, or its codes were withdrawn. The body usually carries orderStatus — the order's status (reserved, processing, failed, refunded, …) — so you know whether to wait for the order.fulfilled event or to stop. |
| 409 | fulfillment_reveal_limit_reached | The codes have been revealed as often as allowed. Use the values you already hold; contact support if they were lost. |
| 409 | fulfillment_replay_expired | This Idempotency-Key revealed the codes more than 15 minutes ago. Reveal again under a new key. |
| 409 | idempotency_conflict | This Idempotency-Key is already bound to another reveal — of another order, or by another of your API keys. Nothing was revealed or counted; use a key of this reveal's own. |
| 409 | conflict | Rarely: the delivery cannot be revealed safely right now. Nothing was revealed; contact support with the correlationId. |
{
"title": "The request could not be completed.",
"status": 409,
"detail": "Use the error code and correlation ID when contacting support.",
"code": "fulfillment_unavailable",
"correlationId": "0198e001a2b37c4d8e9f0a1b2c3d4e5f",
"orderStatus": "processing",
"message": "The code isn't ready yet. It appears once the order is fulfilled."
}The reveal response is always sent with Cache-Control: no-store, private and Pragma: no-cache. Honor that end to end: disable response caching and body logging on any proxy in your path.
Handling secrets on your side
- Reveal late. Call the endpoint when your customer or process actually needs the code, not eagerly at fulfillment time.
- Store like passwords, if at all. If your product requires storing codes, encrypt them at rest with access controls and audit; if it does not, deliver and forget — but remember that each reveal spends one of the order's limited reveals.
- Keep codes out of telemetry. Scrub
artifacts[].valuefrom request/response logging, APM captures, and error reports. - Watch
revealCount. Persist the count you expect and alert when the API reports more. - GET is not reveal. Only the
POST …/fulfillmentcall returns values; the order body and the allowance never will, so nothing sensitive rides along with routine polling.
Updated 13 days ago