POST /api/seller/v1/orders/preview is a read-only sibling of
POST /api/seller/v1/orders. It tells you what a real order would
cost and how it would be fulfilled, without claiming any keys.
Use it on your checkout summary screen so the buyer sees the final sale total before pressing "Pay" — no FOR UPDATE locks, no inventory holds, no transaction rows.
Which price is which#
Keep these three money concepts separate — mixing them causes CATALOG_PRICE_MISMATCH / settlement drift:
| Concept | What it is | Where it appears |
|---|---|---|
| Buyer retail | What the end customer paid in your store | integrationOrderContext on POST /orders — e.g. salesPriceGrossMinor, salesPriceNetMinor, currencyCode (audit / risk; not B2B settlement) |
| Catalog unit price | What Avrix validates per unit under the API business model | Preview unitPriceCents → echo as expectedUnitPriceCents (alias expectedWholesaleUnitPriceCents accepted; both must match if sent together) |
| Promo provenance | Why unitPriceCents is below the list price | Preview initialUnitPriceCents (list) + promo (sourceType, sourceId, dates) + promoResolution. Render was/now from the two integers; never recompute from discountPercent |
| Minor units | Integer currency subunits (no decimals in JSON) | All *Cents / *Minor fields. Use preview currencyExponent (e.g. 2 for USD/EUR, 0 for JPY) when formatting |
Preview’s unitPriceCents / totalCents are the binding catalog figures for the next POST /orders, not your storefront MSRP. Declared retail belongs only in integrationOrderContext. See also Pricing view and BMA authority and Glossary.
Why#
Before this endpoint shipped, the only way a store could compute the checkout price for a cart was to:
- call
GET /products/:id/pricingfor list prices — the catalog price list is authoritative under the API business model; or - (worse) call POST /orders "to see the price" and then call POST /orders/cancel if the buyer abandoned checkout — this both generated phantom transactions and burned pre-order capacity.
/orders/preview returns the binding unitPriceCents and
currencyCode that POST /orders would charge, plus the
fulfillmentType (fulfilled or pre-order) and an availability
snapshot — all in one round-trip and one rate-limit budget hit on the
orders class.
Sellability: Preview uses the same checkAvailabilityForSeller path as
GET /availability — non-live products (draft/discontinued/archived) return
404 with PRODUCT_NOT_SELLABLE in readiness details. Authoritative
sellability for catalog mirrors remains GET /allocations (sellable: true).
HTTP method: Preview uses POST with a JSON body because Next.js
does not yet route the HTTP QUERY method (RFC 10008), which would be
the semantically correct verb for this read-only operation. The endpoint
is rate-limited as a read and successful responses are sampled in
api_audit_log (not always-audited like mutating order writes).
API allocations: price comes from the catalog price list (resolve_sku_price)
for the requested territory. countryCode is required (ISO 3166-1 alpha-2).
Missing catalog pricing returns 422 CATALOG_PRICE_NOT_SET; omitting
countryCode returns 422 SALES_COUNTRY_REQUIRED.
Before POST /orders, call preview and echo the returned unitPriceCents as
expectedUnitPriceCents (the catalog list price under the API business
model). The alias expectedWholesaleUnitPriceCents is accepted for the same
echo — if both are sent they must match. If the price changed since preview, the
order fails with 409 CATALOG_PRICE_MISMATCH unless you bind the preview
price (see below).
Price binding (pick one):
priceCommitment— the guaranteed binding path for every account, at every tier (no capability required). Every priced preview returnspriceCommitment: { token, expiresAt }with a hard TTL of 15 minutes (priceCommitmentTokenis a compatibility alias ofpriceCommitment.token). Pass the token onPOST /orders; the previewedunitPriceCentsis honoured untilexpiresAteven if the catalog price changes. Expired/forged tokens → 422PRICE_COMMITMENT_EXPIRED/PRICE_COMMITMENT_INVALID— re-preview and re-quote.- Checkout hold — the premium inventory + price lock. When
whoami.capabilities.checkoutHoldEnabledis true, callPOST /orders/holdafter preview, then passcheckoutHoldIdonPOST /orderswith the same expected price. Live catalog drift is ignored while the hold is valid.
Either binding locks the price, not the offer itself. If the publisher
removes the sale country from the catalog between preview and commit (the
country's price-list entry disappears, or the territory is excluded), the
bound commit still fails closed — 422 CATALOG_PRICE_NOT_SET or the
territory rejection — rather than settling into a de-listed country.
Request#
curl -s -X POST "https://api.avrix.io/api/seller/v1/orders/preview" \
-H "Authorization: Bearer $AVRIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"skuCode": "SANDBOX-ALWAYS-001",
"quantity": 1,
"countryCode": "NL",
"orderReference": "store-order-10432",
"integrationOrderContext": {
"schemaVersion": 1,
"salesCountryCode": "NL",
"currencyCode": "EUR",
"salesPriceGrossMinor": 7999,
"priceIncludesTax": true,
"salesTaxRatePercent": 21,
"salesChannel": "web",
"salesTaxAmountMinor": 1388
}
}'You must supply either skuId or skuCode. quantity is
1..1000 (same caps as POST /orders).
Response (200)#
{
"data": {
"type": "order_preview",
"fulfillmentType": "fulfilled",
"allocationId": "f8b1…",
"allocationRemaining": 942,
"inventoryAvailable": 942,
"maxFulfillableQuantity": 942,
"canFulfill": true,
"partialFulfillmentExpected": false,
"cannotFullyFulfill": false,
"allOrNothingWillFail": false,
"lines": [
{
"skuId": "8b9d…",
"skuCode": "WITCHER3-WIN-EU",
"quantity": 3,
"unitPriceCents": 1599,
"lineTotalCents": 4797,
"currencyCode": "EUR",
"initialUnitPriceCents": 1999,
"promo": {
"sourceType": "event",
"sourceId": "3f6c…",
"discountPercent": 20,
"priceEffect": "settlement_price",
"startDate": "2026-09-01T00:00:00Z",
"endDate": "2026-09-30T23:59:59Z"
},
"promoResolution": "applied"
}
],
"totalCents": 4797,
"currencyCode": "EUR",
"currencyExponent": 2,
"estimateOnly": true
}
}
unitPriceCents already includes any settlement-affecting promotion. initialUnitPriceCents is
the catalog list price it came from and promo names the share or deal that discounted it
(sourceId is the same shareId / responseId you read under Promotions).
promoResolution: "unavailable" means promotions could not be read at quote time: the price is
the list price and a promo may still exist — re-preview before rendering a sale badge.
Scope and rate limits#
- Scope:
seller:orders:previeworseller:keys:pull(keys with pull scope may preview without a separate scope). - Rate class:
orders(same bucket asPOST /orders), not catalogread/read-cached. Cache preview results until the cart changes — do not call on every UI tick (Catalog sync & caching).
For pre-order SKUs the response also includes a preorder block:
"preorder": {
"releaseDate": "2026-12-12T00:00:00Z",
"preorderStart": "2026-10-01T00:00:00Z",
"preorderEnd": "2026-12-11T23:59:59Z",
"preorderUnlocked": false
}
Important#
estimateOnly: trueis set on every preview response. The price reflects live catalog or allocation pricing at the time of preview — the binding price is whateverPOST /orderssees on the next request. Price list rows and allocations can change; do not cache the preview total beyond the lifetime of the open cart.- API allocations without a catalog price return 422
CATALOG_PRICE_NOT_SETinstead of null totals. maxFulfillableQuantityis the same asavailable(min of allocation headroom and inventory). Use it withcanFulfillto gate checkout.canFulfillistruewhen the requestedquantitycan be satisfied for the estimatedfulfillmentType(fulfilledorpre-order).partialFulfillmentExpectedis true whenfulfillmentTypeisfulfilledandavailable < quantity.POST /ordersdoes not partially deliver keys — it returns 409NO_AVAILABLE_KEYSwith zero keys pulled. Disable pay / reduce quantity when this is true. Forpre-order, it is alwaysfalseuntil pre-order capacity is exhausted (thenPOST /ordersmay returnINSUFFICIENT_PREORDER_CAPACITY).- Prefer the clearer aliases
cannotFullyFulfillandallOrNothingWillFail(same boolean).partialFulfillmentExpectedremains for backward compatibility — it does not mean partial key delivery. - Money fields include
currencyExponent(ISO-4217 minor-unit digits:0for JPY/KRW,2for EUR/USD,3for KWD/BHD) next tocurrencyCode. - The endpoint is rate-limited under the
ordersclass (same bucket asPOST /orders), but it is anoperation: "read"so it does not consume thewritebudget.
Errors#
| HTTP | code | when |
|---|---|---|
| 404 | NOT_FOUND | SKU is not in your allocations or allocation is not active |
| 422 | VALIDATION_FAILED | Missing skuId/skuCode, bad quantity |
| 503 | PREORDER_STATE_UNAVAILABLE | Pre-order RPC timed out — retry after Retry-After seconds (5s) |
| 429 | RATE_LIMIT_EXCEEDED | Standard orders class rate limit — wait for Retry-After |
Forbidden anti-patterns#
- Calling
POST /ordersfollowed byPOST /orders/cancelto display a price. Use/orders/previewinstead. - Caching the preview total in your storefront DB beyond the open cart. The price changes whenever the publisher repriced the allocation; always refresh on the final "Pay" click.
- Treating
estimateOnly: trueas advisory. Stores that bill the buyer based on a stale preview total will see settlement discrepancies and chargebacks. - Charging when
canFulfillisfalse. Disable Pay (or reduce quantity) when preview orGET /availability?quantity=says the line cannot be fulfilled.POST /orderswill not partially deliver keys — expect 409NO_AVAILABLE_KEYSand refund at your PSP. See Getting started and Integration recipes.
Related#
- Integration recipes — checkout call order, idempotency, and checkout holds
- Catalog sync & caching — why preview prices drift and when to refresh
- Webhooks — push-first fulfillment after
POST /orders - Error playbook —
CATALOG_PRICE_MISMATCH,NO_AVAILABLE_KEYS, andvalidation-failed