POST /api/seller/v1/orders
Commits a sale and issues entitlements. This is the endpoint that consumes allocation capacity and records a wholesale sale for settlement.
Call it after you have charged the buyer at your PSP and after POST /orders/preview returned canFulfill: true.
When to call it#
| Situation | Call this endpoint? |
|---|---|
| Buyer paid, preview confirmed fulfilability | Yes |
| Buyer has not paid yet | No — preview only |
Preview returned canFulfill: false | No — it will fail |
| You want to show a price | No — use POST /orders/preview |
| High-contention drop needing guaranteed capacity | Use reserve then commit (Hot drop) |
| More than one SKU in a single transaction | Use POST /orders/bulk |
Prerequisites#
- An active BMA with the vendor
- An allocation covering the SKU with
sellable: trueon GET /allocations - A territory catalog price for the buyer’s country (API business model)
- A completed payment at your PSP
- A recent preview for the same SKU, quantity, and country
Authorisation#
Scope: seller:keys:pull
Request headers#
| Header | Required | Notes |
|---|---|---|
Authorization | Yes | Bearer avrix_sk_sbx_your_key_here |
Content-Type | Yes | application/json |
Idempotency-Key | Yes | Stable per logical order. Honoured for 24 hours. |
X-Request-Id | No | Your correlation id. Echoed in responses and errors. |
Request body#
| Field | Type | Required | Description |
|---|---|---|---|
skuId | string (uuid) | One of | Provide either skuId or skuCode, not both |
skuCode | string | One of | Human-readable SKU code |
quantity | integer | Yes | 1–1000. All-or-nothing |
orderReference | string | Yes | Your immutable business identifier (max 255) |
expectedUnitPriceCents | integer | Usually | Canonical price echo — unitPriceCents from preview (see pricing note) |
expectedWholesaleUnitPriceCents | integer | No | Deprecated alias of the price echo (removal planned for v2); if both price fields are sent they must be equal |
countryCode | string | Recommended | ISO 3166-1 alpha-2 (e.g. US). Promoted into IOC when absent |
deliveryMode | string | No | key (default) — plaintext codes in the response |
checkoutHoldId | string (uuid) | No | Bind to an active checkout hold |
priceCommitmentToken | string | No | Signed price binding from preview, when issued |
customerReference | string | No | Your customer / account reference |
metadata | object | No | Opaque partner metadata |
backorderOnUnavailable | boolean | No | Opt in to a durable customer-order backorder when immediate fulfilment cannot allocate. Rejected with 403 FEATURE_DISABLED unless GET /whoami capabilities.backorderEnabled is true. When the capability is on, the vendor grant must also allow request_mode_backorder or the API returns 403 BACKORDER_NOT_ALLOWED. Accepted orders sit in awaiting_stock until order.ready, then POST /orders/{orderId}/fulfill |
immediateFulfilment | boolean | No | When false, keys are assigned but plaintext is omitted (deferredReveal: true, keys: []) |
integrationOrderContext | object | Yes in production | Commercial evidence for the retail transaction |
Pricing note. Send expectedUnitPriceCents equal to preview’s unitPriceCents. A priceCommitmentToken can satisfy the echo when present. Complimentary-model allocations may waive the echo. Omitting a required echo returns EXPECTED_UNIT_PRICE_REQUIRED.
Price vocabulary (one meaning per name). expectedUnitPriceCents is the only commit-time
price field going forward — expectedWholesaleUnitPriceCents is a deprecated alias that will be
removed in v2. pricePerKeyCents (on GET /allocations rows) is an advisory wholesale-cost
summary from the grant — never send it on a commit, and never confuse it with the catalog list
price that preview returns under the API business model.
integrationOrderContext#
Use Order context as the field authority. Production keys enforce the full commercial-evidence set (net, tax, payment method family, PSP reference, checkout session, buyer IP, and country) — sandbox does not. An integration that passes in sandbox can return 422 in production for missing evidence.
Example request (sandbox)#
curl -s -X POST "https://api.avrix.io/api/seller/v1/orders" \
-H "Authorization: Bearer $AVRIX_API_KEY" \
-H "Idempotency-Key: store-order-10432" \
-H "Content-Type: application/json" \
-d '{
"skuCode": "SANDBOX-ALWAYS-001",
"quantity": 1,
"orderReference": "store-order-10432",
"expectedUnitPriceCents": 5999,
"countryCode": "NL",
"deliveryMode": "key",
"integrationOrderContext": {
"schemaVersion": 1,
"salesCountryCode": "NL",
"currencyCode": "EUR",
"salesPriceGrossMinor": 7999,
"priceIncludesTax": true,
"salesTaxRatePercent": 21,
"salesChannel": "web",
"salesTaxAmountMinor": 1388,
"paymentProcessorReference": "pay_3QxK2mB9",
"checkoutSessionId": "cs_sandbox_10432",
"consumerIp": "198.51.100.42"
}
}'For a production-shaped body with all required evidence fields, see Order context.
Success response — 200#
Plaintext keys are a string array. Key UUIDs for refunds and returns are in a separate top-level keyIds array (keys[i] corresponds to keyIds[i]).
activation is the guaranteed activation set for the SKU (worldwide, region_locked with countries, or unknown) — the same object as GET /allocations and preview lines — so you can show "activates in" at delivery without a second lookup. It is the intersection across the SKU's inventory batches, not the specific batch this key came from. See Territory enforcement.
{
"data": {
"type": "fulfilled",
"skuId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"skuCode": "SANDBOX-ALWAYS-001",
"orderReference": "store-order-10432",
"fulfillmentState": "fulfilled",
"orderReadableId": "ord_01J8XYZ...",
"pulled": 1,
"keys": ["AAAA-BBBB-CCCC-DDDD"],
"keyIds": ["8f21a9b0-1234-5678-9abc-def012345678"],
"allocationId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"activation": { "type": "region_locked", "countries": ["DE", "AT", "CH"] },
"transactionIds": ["c3d4e5f6-a7b8-9012-cdef-123456789012"],
"fulfilledAt": "2026-08-05T09:14:22.108Z"
}
}
Deferred reveal#
When immediateFulfilment is false (or the platform defers reveal), the body includes deferredReveal: true and keys: [] while still reporting pulled and keyIds when available. Retrieve codes through your recovery / export workflows — do not assume plaintext will appear later on GET /orders.
Safe order creation pattern#
POST /orders/preview— gate oncanFulfill: true; storeunitPriceCents.- Optional:
POST /orders/holdwhen checkout hold is enabled. - Charge the buyer at your PSP.
POST /orderswith stableIdempotency-Key,orderReference, andexpectedUnitPriceCents.- Persist entitlements (
keys+keyIds) from the response before acknowledging the buyer UI. - Handle
order.fulfilledfor ops correlation (no key plaintext on the webhook wire).
Bulk orders#
When one buyer cart spans multiple SKUs, use POST /api/seller/v1/orders/bulk instead of multiple
independent POST /orders calls. Each line still needs sellability (allocation + territory) and a
stable idempotency strategy — see OpenAPI for the line-item body shape.
Prefer one logical cart → one bulk request with a cart-level idempotency key (or documented
per-line keys) so retries do not double-fulfil. For single-SKU checkout, stay on POST /orders.
Unknown outcome recovery#
If the HTTP call times out after you charged the buyer:
- Retry the same
Idempotency-Keyand body. - If still unclear,
GET /api/seller/v1/orders?orderReference=store-order-10432(status only — never key plaintext). - If the aggregate says
fulfilledbut you never stored the keys (you crashed between the pull and your own database write), callPOST /api/seller/v1/orders/recover-keyswith the sameorderReference(scopeseller:keys:pull). It returns the live keys andkeyIdsalready sold under that reference, never pulls new inventory, and is audited as a reveal. Re-postingPOST /orderswith the same reference and quantity replays the same keys as well. - If no aggregate exists and retries keep failing, escalate with
X-Request-Id— do not open a second order with a new key against the same payment. - If Avrix returns
NO_AVAILABLE_KEYS(or wallet/KYB 402 after charge), refund at your PSP. Avrix does not auto-refund.
See Idempotency and Order lifecycle.
Common status codes#
| Status | Meaning |
|---|---|
200 | Order committed |
400 | Malformed request / missing idempotency key / some billing failures |
401 | Invalid or wrong-environment key |
402 | Payment not cleared (PAYMENT_NOT_CLEARED) — refund the buyer |
403 | Missing scope, IP not allowed, contract inactive, or capability disabled |
404 | SKU not visible or PRODUCT_NOT_SELLABLE — branch on code, not status alone |
409 | Insufficient vendor stock (NO_AVAILABLE_KEYS), allocation cap reached (ALLOCATION_EXCEEDED), price mismatch, or idempotency conflict / in-flight |
410 | Hot-drop reservation gone (when using reserve/commit) |
412 | Checkout-hold precondition failed |
422 | Validation, territory, or commercial-evidence failure |
429 | Rate limited — honour Retry-After |
503 | Temporary failure — safe to retry with the same key |
504 | Hot-path timeout (HOT_PATH_TIMEOUT) — retry-safe; pull did not commit |
Common mistakes#
| Mistake | Fix |
|---|---|
Fresh Idempotency-Key per retry | Mint once per logical order |
Not persisting keys and keyIds immediately | Write both to your vault before buyer response |
Expecting keys[] objects with keyId | Keys are plaintext strings; UUIDs are in keyIds[] |
Sending deliveryMode: "keys" | Use "key" or omit (default) |
Reusing orderReference across different orders | One business order → one reference |
Sending wholesale cost as salesPriceGrossMinor | That field is retail gross |
| Implementing only the sandbox-minimal IOC | Production requires the full evidence set — see Order context |
| Calling without a recent preview | Re-preview when price or stock may have drifted |
Retrying 422 unchanged | Deterministic — fix the request |