Every Seller API error code, what causes it, whether retrying is safe, and how to fix it.
Error recovery overview#
API error responses include a doc_url that links directly to the relevant section on this page.
If you arrived here from an error response, you are already at the right place.
The error envelope#
Every error returns the same flat JSON body (fields at the top level — not nested under
error):
{
"code": "NO_AVAILABLE_KEYS",
"message": "Insufficient inventory: requested 2, available 0",
"recoverable": false,
"hint": "Check GET /availability first to see allocation and inventory",
"doc_url": "https://docs.avrix.io/seller-api/errors#no-available-keys",
"requestId": "req_01J8XYZ...",
"timestamp": "2026-08-05T09:14:22.108Z",
"details": {
"requested": 2,
"available": 0,
"recommendedAction": "refund_buyer"
},
"error": "Insufficient inventory: requested 2, available 0"
}
| Field | Use |
|---|---|
code | Branch on this. Stable across releases. Never parse message. |
message | Human-readable. Wording may change; do not match on it. |
recoverable | Whether the condition may resolve on retry. Not the same as "safe to retry blindly". |
hint | Short remediation guidance. |
doc_url | Deep link to the section on this page. |
requestId | Log this. Quote it in every support request. |
timestamp | ISO-8601 time the error was formed. |
details | Structured context, varying by code. May include recommendedAction, and — when the fix lives in the Console — consoleUrl + consoleAction (see below). Footprint refusals also carry fixItUrl with the countryCode or currencyCode seen; SRP_MISMATCH_FOR_CURRENCY carries floorMinor; consumer geo refusals carry declaredCountry and, when the buyer IP resolved, detectedCountry. |
error | Deprecated. String alias of message for older clients. Do not read error.code — error is a string, not an object. |
Fix it in the Console. When the cause can be resolved by the company that owns the API key,
details.consoleUrl is an absolute link to the right Console page (scoped to that company, never
to the shared sandbox catalog) and details.consoleAction is a stable label you can branch on:
consoleAction | Console page | Typical codes / decisionCode |
|---|---|---|
review_partner_contracts | Partners → Contracts | CONTRACT_NOT_ACTIVE, NO_ACTIVE_BMA, API_MODEL_AGREEMENT_REQUIRED, BUSINESS_MODEL_NOT_ALLOWED, MINIMUM_ORDER_VIOLATION; CONNECTION_NOT_ACTIVE, MCA_NOT_ACTIVE, BMA_NOT_SIGNED, SKU_SHARE_TERM_MISMATCH |
review_allocations | Commerce → Allocations | ALLOCATION_EXCEEDED, NO_AVAILABLE_KEYS; ALLOCATION_PAUSED, ALLOCATION_REVOKED |
manage_api_keys | Settings → API keys | INSUFFICIENT_SCOPE, IP_NOT_ALLOWED, ALLOWLIST_REQUIRED, KEY_ENVIRONMENT_MISMATCH, PRODUCTION_ACCESS_NOT_APPROVED |
complete_kyb | Settings → KYB | KYB_NOT_VERIFIED, TREASURY_CAPABILITY_MISSING |
top_up_wallet | Finance → Wallet | INSUFFICIENT_WALLET_BALANCE, PAYMENT_NOT_CLEARED |
review_partner_catalog | Catalog → Products | PRODUCT_NOT_SELLABLE, TERRITORY_*, SKU_REGION_UNRESOLVED, ACTIVATION_REGION_*, CATALOG_PRICE_NOT_SET, INVALID_CATALOG_PRICE; SKU_SHARE_MISSING, SKU_SHARE_SUSPENDED |
Codes whose fix is in your own request (validation, idempotency, price echo) or in Avrix operations
(rate limits, timeouts) carry no consoleUrl. Territory, activation and price guard failures on
POST /orders and POST /keys/pull now return the same structured details (skuId,
countryCode, allowedCountryCodes, catalogUnitPriceCents, …) that POST /orders/preview
already returned, instead of only pullError.
Always log requestId. It is how Avrix correlates your report with server-side traces, and a
support request without one takes substantially longer to resolve.
Retry policy#
| Class | Retry? | How |
|---|---|---|
5xx | Yes | Exponential backoff, same Idempotency-Key |
429 | Yes | Wait for Retry-After |
409 IDEMPOTENCY_REQUEST_IN_FLIGHT | Yes | Wait for Retry-After |
| Timeout, no response | Yes | Same key, same body. See unknown outcome |
409 NO_AVAILABLE_KEYS | No | Deterministic |
409 ALLOCATION_EXCEEDED | No | Request a larger allocation |
422 | No | Fix the request first |
401, 403 | No | Credential or permission problem |
Failed responses are never cached. Only successful (2xx) responses are stored for
Idempotency-Key replay. After any 4xx/5xx, fix the underlying cause (KYB, wallet balance,
stock, prices) and retry with the same key — the request is re-evaluated against live state.
You never need to rotate the key to escape a stale error.
Edge challenges (HTML 403)#
The API is served through an edge network that protects the platform against abusive traffic.
A client that sends aggressive bursts (see rate limits) can be
challenged at the edge before the request reaches the API: the response is a 403 with an
HTML body instead of the JSON error envelope, and it carries no X-Request-Id or rate-limit
headers.
How to tell the three rejection shapes apart:
| Signal | Meaning |
|---|---|
403 + JSON envelope with code | Authorization problem — see the codes below |
403 + HTML body, no X-Request-Id | Edge challenge — your traffic profile tripped platform protection |
429 + JSON + Retry-After | API rate limiting — back off and retry |
Fix. Treat an HTML 403 as a signal to slow down, not something to retry. Reduce your request
rate, prefer cached reads (send If-None-Match, honour Cache-Control),
and spread bursts over time. The challenge decays automatically after a cool-down. If a
well-behaved integration keeps being challenged, contact support with your egress IP addresses —
sustained high-volume integrations should make sure every egress IP is on the key allowlist so the edge bypass applies.
Authentication and authorisation#
unauthorized#
UNAUTHORIZED · 401 · Recoverable
The API key was not accepted: malformed header, revoked key, or wrong environment.
Fix. Verify the header is Authorization: Bearer avrix_sk_… with no doubled prefix, quotes, or
non-ASCII characters. Confirm the key is active in the Console. Call GET /whoami to check the
environment. See Authentication.
forbidden#
FORBIDDEN · 403 · Not recoverable
The credential is valid but not permitted to perform this operation on this resource.
Fix. Confirm the key belongs to the company that owns the resource, and check scopes.
insufficient-scope#
INSUFFICIENT_SCOPE · 403 · Not recoverable
The key lacks a required scope. Empty scope sets are always denied — there is no implicit default.
Fix. Scopes cannot be added to an existing key. Create a new key with the required scopes and
rotate. details names the missing scope.
ip-not-allowed#
IP_NOT_ALLOWED · 403 · Not recoverable
Your source address is not on the key's allowlist.
Fix. Call GET /whoami-ip from the failing environment to see the address Avrix observed, then add it. Note that an empty allowlist means no restriction; a populated one is strictly enforced.
allowlist-required#
ALLOWLIST_REQUIRED · 403 · Not recoverable
A production key has an empty or null allowed_ips list while the deployment requires a non-empty
allowlist for production traffic. Sandbox keys are not affected.
Fix. Set a non-empty IP allowlist via Settings → Integrations → API Keys or PATCH /keys/{id}/allowed-ips. Discover your egress with GET /whoami-ip, then retry from an allowlisted address. See IP allowlist.
ip-not-resolvable#
IP_NOT_RESOLVABLE · 403 · Not recoverable
The key has a populated allowlist, but Avrix could not determine a client IP from the request headers — so the allowlist check cannot run safely.
Fix. Ensure your edge or proxy forwards a usable client address (for example X-Forwarded-For
is not stripped). Call GET /whoami-ip from the same path to confirm what Avrix observes.
key-environment-mismatch#
KEY_ENVIRONMENT_MISMATCH · 401 · Not recoverable
The API key's environment (production or sandbox) does not match the request environment.
Fix. Use a sandbox key against sandbox hosts and a production key against production. Call
GET /whoami and check environment. Do not send sandbox-only headers with a production key.
See Sandbox to production.
production-access-not-approved#
PRODUCTION_ACCESS_NOT_APPROVED · 403 · Not recoverable
Your seller company is not approved for production Seller API access. Sandbox keys continue to work.
Fix. Complete certification and ask Avrix operations to enable production API access. See Go live and Certification.
api-model-agreement-required#
API_MODEL_AGREEMENT_REQUIRED · 403 · Not recoverable
Your company has not signed the API Revenue Share Channel Terms. Sandbox keys can still be created; production keys cannot.
Fix. In the Avrix Console open Settings → Commercial Agreements, sign the API Revenue
Share Channel Terms, then create the production key. Live traffic on production still requires
Avrix to enable production API access (PRODUCTION_ACCESS_NOT_APPROVED). See
Go live.
Request validation#
unsupported-api-version#
UNSUPPORTED_API_VERSION · 400 · Recoverable
The request sent an X-Avrix-Api-Version value that this host does not support.
Fix. Omit the header, or send the version advertised on responses (currently 2026-04-29).
See Versioning and compatibility.
validation-failed#
VALIDATION_FAILED · 422 · Recoverable
The request failed schema validation.
Fix. Use details.issues — an array of field and message pairs pinpointing each problem. For
query parameters, match the OpenAPI definition. For path parameters, UUID segments must be valid
UUIDs.
bad-request#
BAD_REQUEST · 400 · Not recoverable
Malformed request that failed before schema validation — unparseable JSON, wrong content type.
not-found#
NOT_FOUND · 404 · Not recoverable
The resource does not exist, or is not visible to your company. Avrix does not distinguish the two, by design.
Fix. For SKUs, confirm it appears in GET /allocations. A SKU that exists on the platform but
is not shared or allocated to you returns 404.
conflict#
CONFLICT · 409 · Recoverable
The resource state conflicts with the request.
payload-too-large#
PAYLOAD_TOO_LARGE · 413 · Recoverable
The JSON body exceeds the size limit. Reduce it — for bulk orders, split into smaller batches.
Idempotency#
idempotency-key-mismatch#
IDEMPOTENCY_KEY_MISMATCH · 409 · Not recoverable
The Idempotency-Key was already used with a different request body.
Fix. Reuse a key only when retrying a byte-identical request. Compare
details.requestBodyHash with the first request to find the difference. If this is genuinely a new
order, use a new key. Do not treat this as a form-validation (422) failure — the HTTP status
is 409.
Bodies are compared by canonical-JSON hash, so key ordering and whitespace do not matter — a mismatch means real content differs.
idempotency-request-in-flight#
IDEMPOTENCY_REQUEST_IN_FLIGHT · 409 · Recoverable
Another request with this key is currently executing.
Fix. Wait for Retry-After seconds and retry with the same key. Do not issue a new key.
idempotency-store-unavailable#
IDEMPOTENCY_STORE_UNAVAILABLE · 503 · Recoverable
The idempotency store is temporarily unavailable. The request was rejected rather than executed, to avoid a duplicate that could not be detected.
Fix. Retry with the same key after a short delay. Nothing was executed.
Contracts and permissions#
contract-not-active#
CONTRACT_NOT_ACTIVE · 403 · Not recoverable
The connection or a required contract — MCA or API BMA — is not active, or the agreement that
granted this SKU is paused. POST /orders/preview, /orders/hold, /orders/reserve, /orders
and /keys/export all answer this code (with previewError / holdError / reserveError /
skuResolve: "CONTRACT_NOT_ACTIVE") — never SKU_NOT_FOUND — so a paused agreement is not
mistaken for a catalog problem. Your allocation is intact and becomes sellable again when the
agreement is resumed; nothing needs to be re-requested.
Fix. Commercial, not technical. Contact the vendor. Check hasActiveBma on GET /whoami.
Show the product as unavailable rather than removing it from your catalog.
minimum-order-violation#
MINIMUM_ORDER_VIOLATION · 422 · Not recoverable
Quantity is below the minimum for this connection. details gives the minimum.
payment-not-cleared#
PAYMENT_NOT_CLEARED · 402 · Not recoverable
Allocation payment must be confirmed before keys are released. This concerns your settlement with Avrix, not your buyer's payment.
Fix. If you have already charged the buyer, refund at your PSP. Contact support if settlement is complete but pulls still fail.
business-model-not-allowed#
BUSINESS_MODEL_NOT_ALLOWED · 403 · Not recoverable
The SKU's allowed_business_models allow-list does not include the allocation's distribution
model. Seller API partnerships use the API business model — the SKU must allow api.
Fix. Ask the vendor to enable the SKU for the API distribution model, or use an allocation on
an enabled model. details.remediation repeats the guidance.
insufficient-wallet-balance#
INSUFFICIENT_WALLET_BALANCE · 402 · Not recoverable
Not currently returned on Seller API pulls or orders. The code remains reserved for future prepaid / wholesale wallet settlement. Console wallet mutations may still surface it when a withdraw or convert exceeds available balance.
Fix. When this code returns on a pull or order again, top up the wallet or reduce quantity. If you already charged the buyer, refund at your PSP — Avrix does not auto-refund.
kyb-not-verified#
KYB_NOT_VERIFIED · 403 · Recoverable
The seller company has not completed Airwallex account verification (KYB), so settlement is not possible yet. Nothing is wrong with the request body — do not audit your payload.
Fix. An account admin must finish verification in Settings → Verification. Once the
account reaches the ACTIVE state, retry with the same Idempotency-Key; the request will
succeed unchanged (failures are never cached, so the retry is evaluated fresh).
treasury-capability-missing#
TREASURY_CAPABILITY_MISSING · 403 · Recoverable
The seller's Airwallex account is verified, but the treasury (transfers) capability required for API settlement has not been enabled by Airwallex. Nothing is wrong with the request body.
Fix. Check Settings → Verification for pending steps. Once the capability is enabled,
retry with the same Idempotency-Key — the retry is evaluated fresh. If verification shows
complete and this error persists, contact support with your requestId.
no-active-bma#
NO_ACTIVE_BMA · 409 · Not recoverable
The seller company has no active Business Model Agreement (BMA) with any publisher.
POST /webhooks refuses to create a subscription, and catalog and fulfilment routes return empty
results until at least one BMA is active. GET /whoami reports the same state as a warnings[]
entry carrying this code.
Fix. Commercial, not technical. Sign a Business Model Agreement with a publisher in the console,
then retry. Unlike CONTRACT_NOT_ACTIVE (one connection or contract on a specific order), this
code means no agreement exists at all.
Catalog and sellability#
product-not-sellable#
PRODUCT_NOT_SELLABLE · 404 (immediate pull) / 422 (pre-order acceptance) · Not recoverable now, may resolve on its own
The SKU cannot be transacted right now: the product lifecycle is not live (draft, announced,
discontinued, archived), the pre-order window has not opened or has already closed, or your
company does not have the pre-order capability for a pre_order release.
details explains which, and when it changes:
| Key | Meaning |
|---|---|
reason | PREORDER_NOT_OPEN · PREORDER_CLOSED · PREORDER_DISABLED · LIFECYCLE_NOT_LIVE |
launchPhase | Effective phase of the release (draft, announced, pre_order, available) |
preorderStart, preorderEnd, releaseDate | ISO instants where known — the same skuId becomes sellable at these boundaries |
recommendedAction | wait_for_preorder_window · wait_for_release · request_preorder_capability · delist_sku |
There is no successor product to switch to: a pre-order becomes its release in place, and
release.available / sku.sellability_changed fire on the same skuId.
Fix. Branch on details.reason. For the wait_* actions keep the SKU listed but not
purchasable and re-check GET /allocations (or subscribe to sku.sellability_changed) at the
instant given. For delist_sku your catalog is stale — re-sync and delist. Do not charge buyers
for SKUs in this state. Branch on code, not status alone — 404 can also mean the SKU is not
visible to your company.
no-available-keys#
NO_AVAILABLE_KEYS · 409 · Not recoverable
Insufficient vendor inventory for the requested quantity. Nothing was pulled — orders are
all-or-nothing. When the shortfall is your own allocation cap rather than vendor stock, you get
ALLOCATION_EXCEEDED instead (see below).
If you have already charged the buyer, refund at your PSP. Avrix does not refund your buyer and has no mechanism to.
Fix. Gate on canFulfill from POST /orders/preview before charging, and treat
cannotFullyFulfill: true (alias allOrNothingWillFail; deprecated name partialFulfillmentExpected)
as "this order will 409" — it never means partial delivery. For high-contention drops, use a
checkout hold or the reserve/commit flow. details.available gives the current count.
allocation-exceeded#
ALLOCATION_EXCEEDED · 409 · Not recoverable
Your allocation cap for this SKU is reached — the vendor may still hold stock, but your
allocation has fewer keys remaining than the requested quantity. Nothing was pulled.
details.remainingInAllocation gives the keys still available under your cap.
Allocation-cap shortfalls historically returned NO_AVAILABLE_KEYS; branch on
ALLOCATION_EXCEEDED to distinguish "request a bigger allocation" from "vendor out of stock".
NO_AVAILABLE_KEYS still fires for genuine vendor stock-outs.
Fix. Request a larger allocation from the publisher, or reduce the quantity to the remaining
headroom. Retrying unchanged fails until the allocation is raised
(details.recommendedAction is request_allocation_increase).
order-reference-conflict#
ORDER_REFERENCE_CONFLICT · 409 · Not recoverable
POST /orders reused an orderReference that already has a partial live fulfillment on the
same allocation (fewer live keys than the requested quantity). Avrix never tops up under the same
reference — that would silently double-sell.
When the prior fulfillment is complete and still live, the same orderReference replays the
original keys instead (no new inventory consumption). When every prior key was returned or
refunded, a fresh pull is allowed.
Fix. Use a new orderReference (and a new Idempotency-Key) for a genuinely new sale.
Investigate the prior delivery with GET /orders?orderReference=….
preorder-state-unavailable#
PREORDER_STATE_UNAVAILABLE · 503 · Recoverable
Preorder metadata could not be loaded. Retry after Retry-After (typically 5 seconds).
Pricing#
expected-unit-price-required#
EXPECTED_UNIT_PRICE_REQUIRED · 422 · Recoverable
expectedUnitPriceCents was omitted or invalid. The guard fails closed — there is no default.
Fix. Pass expectedUnitPriceCents (or expectedWholesaleUnitPriceCents) echoing
unitPriceCents from your most recent preview. It must be a positive integer.
catalog-price-mismatch#
CATALOG_PRICE_MISMATCH · 409 · Recoverable
Under the API business model, expectedUnitPriceCents does not match the current territory catalog
list price — or does not match the price snapshot bound by an active checkout hold.
Fix. Re-run POST /orders/preview for the same country and echo the new price. Then decide
deliberately whether to proceed: if the price rose above what you sold at, you would be selling
at a loss. This guard exists to make that a conscious decision.
Prevent it with a checkout hold, which binds the price for its TTL.
wholesale-price-mismatch#
WHOLESALE_PRICE_MISMATCH · 409 · Recoverable
Legacy contract code for non-API allocation price binding. Current Seller API partnerships use the
API business model and should expect CATALOG_PRICE_MISMATCH instead when the echo does
not match the territory catalog list price.
Fix. Re-run POST /orders/preview for the same country and echo the returned unitPriceCents
as expectedUnitPriceCents.
catalog-price-not-set#
CATALOG_PRICE_NOT_SET · 422 · Not recoverable
No catalog price list entry exists for this SKU in the declared country.
Fix. Commercial. The vendor must set territory pricing. Exclude the SKU from that market.
invalid-catalog-price#
INVALID_CATALOG_PRICE · 422 · Not recoverable
The catalog list price is zero or negative. A data problem — escalate to the vendor.
currency-mismatch#
CURRENCY_MISMATCH · 422 · Recoverable
integrationOrderContext.currencyCode does not match the currency an active checkout hold froze
with its price. Only the hold path returns this. On a plain POST /orders, a currency other than
the catalog's is not a mismatch: it is a secondary-currency sale, and whether it may proceed is
answered by FOOTPRINT_COUNTRY_NOT_DECLARED, CURRENCY_NOT_PERMITTED_BY_TERM,
CURRENCY_NOT_PRICED_FOR_COUNTRY and the SRP_MISMATCH_FOR_CURRENCY floor, in that order.
Fix. Re-read the hold, or create a new one in the currency you intend to sell in.
consumer-geo-mismatch-policy-refused#
CONSUMER_GEO_MISMATCH_POLICY_REFUSED · 422 · Not recoverable
The country you declared and the country the checkout evidence points at disagree, and the Content Owner's agreement refuses that.
Fix. Send the country the buyer is actually in, with a consumerIp that resolves there.
This is the agreement's geo-mismatch policy rather than a platform rule: other Content Owners
may allow the same sale, and the same one may allow it on a different SKU. Under the more
common warn policy the sale proceeds and the disagreement is recorded on the transaction
instead, where it lowers the evidence rank the sale settles at.
sales-channel-required#
SALES_CHANNEL_REQUIRED · 422 · Recoverable
integrationOrderContext.salesChannel is missing.
Fix. Declare which of your own surfaces the sale came through: web, mobile_app,
partner_embed, or marketplace:<slug>. GET /whoami lists it under
requiredDeclarations. Sandbox enforces this exactly as production does.
sales-channel-not-permitted#
SALES_CHANNEL_NOT_PERMITTED · 422 · Not recoverable
Your agreement's Commercial Schedule does not permit sales through this channel. Under the
default, own storefronts only, any marketplace:<slug> is refused.
Fix. The grant is to sell to an end user through your own storefront. Listing a Key on a third-party marketplace is outside it, and an operator of a marketplace may hold the agreement only in respect of its own first-party storefront. Widening this is a countersigned amendment, not a settings change.
footprint-country-not-declared#
FOOTPRINT_COUNTRY_NOT_DECLARED · 422 · Recoverable
integrationOrderContext.salesCountryCode is not in your declared selling footprint.
Fix. Add the country in Commercial configuration. The error details carry a fixItUrl
that links straight there. Your footprint is what the platform settles your sales against,
so a country you have not declared cannot be sold into.
footprint-currency-not-declared#
FOOTPRINT_CURRENCY_NOT_DECLARED · 422 · Recoverable
The sale currency is not in your declared selling currencies.
Fix. Add the currency in Commercial configuration. The error details carry a fixItUrl
that links straight there. Declaring a currency is not the same as being priced in it: the
Content Owner's agreement still has to permit it and the title still has to carry a price,
so this is the first of three checks rather than the only one.
currency-not-priced-for-country#
CURRENCY_NOT_PRICED_FOR_COUNTRY · 422 · Recoverable
This SKU has no price in the declared currency for that country, and the Content Owner has not permitted the currency on the SKU.
Fix. Sell in the currency POST /orders/preview returns for that country, or ask the
Content Owner to permit yours. See Currency and FX.
currency-not-permitted-by-term#
CURRENCY_NOT_PERMITTED_BY_TERM · 422 · Not recoverable
Your agreement's Commercial Schedule does not permit sales in this currency. Under the default, each country's own currency only, any other currency is refused however the SKU is priced.
Fix. Widening this is a countersigned amendment to the agreement, not a settings change.
Talk to the Content Owner. GET /whoami reports what each of your agreements permits.
srp-mismatch-for-currency#
SRP_MISMATCH_FOR_CURRENCY · 422 · Recoverable
A sale in a permitted currency the SKU has no price in must be at least the SRP converted at the dated reference rate. This sale is below that floor.
Fix. The floor is in the error details as floorMinor, and POST /orders/preview
returns it before you commit. If reason is no_rate, the platform publishes no fixing for
that currency pair and the sale cannot be priced at all; use the SKU's own currency.
price-commitment-invalid#
PRICE_COMMITMENT_INVALID · 422 · Recoverable
The supplied priceCommitmentToken is not valid for this request.
Fix. Re-run POST /orders/preview and pass the new token, or use checkoutHoldId instead.
price-commitment-expired#
PRICE_COMMITMENT_EXPIRED · 422 · Recoverable
The preview price token has expired.
Fix. Re-run POST /orders/preview (or POST /orders/hold) before charging the buyer.
Territory#
sales-country-required#
SALES_COUNTRY_REQUIRED · 422 · Recoverable
No sales country was supplied. There is no default — the guard fails closed, because guessing a country would mean guessing a price and a tax treatment.
Fix. Pass countryCode on preview and integrationOrderContext.salesCountryCode on the order.
A top-level countryCode on POST /orders is promoted into the context when that field is absent.
tax-declaration-required#
TAX_DECLARATION_REQUIRED · 422 · Recoverable
integrationOrderContext.salesTaxRatePercent or salesPriceGrossMinor is missing. JSON null
counts as absent for both.
Fix. Declare what you charged the consumer and what tax was in it. The Content Owner's share is
computed on the price net of that tax, so a sale that does not state it cannot be settled, and the
refusal comes before a key is delivered rather than after. A zero rate is a declaration: send
salesTaxRatePercent: 0 where no consumer tax applies. GET /whoami lists the required fields
under requiredDeclarations, and sandbox enforces them exactly as production does.
territory-not-allowed-for-sku#
TERRITORY_NOT_ALLOWED_FOR_SKU · 422 · Not recoverable
The buyer's country is not permitted for this SKU.
Fix. Gate checkout using allowedCountries on product detail, or GET /regions. Offer a
different market SKU.
territory-excluded-for-release#
TERRITORY_EXCLUDED_FOR_RELEASE · 422 · Not recoverable
The country is explicitly excluded for this release.
region-locked-for-contract#
REGION_LOCKED_FOR_CONTRACT · 422 · Not recoverable
The Content Owner locked this country under your agreement. The agreement's territory includes it, and this lock removes it.
A lock is restrictive only: it can refuse a sale the agreement would have allowed, and can never allow one the agreement refuses. That is why the Content Owner may set one without an amendment, and why you can always see it before you meet it. details.reason carries their own words, details.countryCode the country, and GET /whoami lists every lock that applies to you. Content Owners set partner locks on the partner page. Content Sellers can see them there and, when listed, under Commercial Settings as read-only.
Widening one is a conversation with the Content Owner, not a retry.
region-locked-for-sku#
REGION_LOCKED_FOR_SKU · 422 · Not recoverable
This title may not be sold in that country, whatever any agreement grants: a rating refusal, a platform restriction, a distribution carve-out. details.reason says which.
Different from TERRITORY_EXCLUDED_FOR_RELEASE, which is territorial configuration of the regional release, and from TERRITORY_NOT_ALLOWED_FOR_SKU, which means the country was never in the SKU's coverage. This one means the country IS in the coverage and someone chose to remove it for this title. Other titles in the same territory are unaffected, so offer a different SKU rather than a different market.
sku-region-unresolved#
SKU_REGION_UNRESOLVED · 422 · Not recoverable
The SKU has no territorial coverage configured. A data problem — escalate to the vendor.
activation-region-mismatch#
ACTIVATION_REGION_MISMATCH · 422 · Not recoverable
countryCode is outside this SKU's guaranteed activation set. Sale territory may still allow the sale, but the delivered key may not redeem in that country. Returned when whoami.capabilities.activationPolicy is enforce_known or enforce_strict — the effective posture is per company: the platform default, or enforce_known when your company opted in (whoami.capabilities.activationEnforceOptIn). In warn (the default) the same condition is an advisory preview warning only. Enforcement responses carry details: skuId, countryCode, activationType, allowedCountries.
Fix. Gate checkout on activation.countries from GET /allocations or preview lines. Do not charge a buyer outside the guaranteed set.
activation-region-unknown#
ACTIVATION_REGION_UNKNOWN · 422 · Not recoverable
The SKU has no unarchived key-batch activation metadata, so coverage cannot be guaranteed. Returned only when activationPolicy is enforce_strict.
Fix. Ask the vendor to set batch activation (worldwide or an explicit country set), or keep the deployment on warn / enforce_known.
Consumer verification#
These apply to production keys where consumer geo enforcement is enabled. Check
capabilities.consumerGeoEnforcementEnabled on GET /whoami.
consumer-ip-required#
CONSUMER_IP_REQUIRED · 422 · Recoverable
Production keys require integrationOrderContext.consumerIp.
Fix. Supply the buyer's public routable client IP — not your server's address, and not a private-range address.
consumer-ip-invalid#
CONSUMER_IP_INVALID · 422 · Recoverable
consumerIp is not a public routable IPv4 or IPv6 address. Private, loopback, and reserved ranges
are rejected.
Fix. Extract the real client address from your proxy headers, correctly — behind a load balancer, the immediate peer address is your own infrastructure.
consumer-geo-mismatch#
CONSUMER_GEO_MISMATCH · 422 · Recoverable
The country derived from consumerIp does not match salesCountryCode.
Fix. Ensure the declared sales country reflects where the buyer actually is.
consumer-ip-high-risk#
CONSUMER_IP_HIGH_RISK · 422 · Not recoverable
The address is a VPN, hosting provider, Tor exit, or public proxy.
consumer-geo-unavailable#
CONSUMER_GEO_UNAVAILABLE · 503 · Recoverable
The geo database is temporarily unavailable. Retry with the same Idempotency-Key after
Retry-After (typically 5 seconds).
country-restricted#
COUNTRY_RESTRICTED · 422 · Not recoverable
Sales are not available for this location.
Fix. Do not offer checkout for that market. Use GET /allocations and product
territory fields to gate the storefront.
consumer-geo-attest-required#
CONSUMER_GEO_ATTEST_REQUIRED · 422 · Recoverable
This catalog requires confirming the buyer’s network from the checkout page.
Fix. POST /geo/attest with your API key, load the returned pixelUrl in the
buyer’s browser (or POST /geo/attest/{attestId}/complete from that page), then
retry the commercial call with consumerGeoAttestId. Do not complete the pixel
from your store server.
location-blocked#
LOCATION_BLOCKED · 403 · Not recoverable
Access is not available from the current location.
Fix. Call the API from an allowed network. This is an access control on your integration egress, not a buyer-checkout error.
Checkout holds#
checkout-hold-disabled#
CHECKOUT_HOLD_DISABLED · 412 · Recoverable
Checkout hold is not enabled for this deployment or account.
Fix. Check capabilities.checkoutHoldEnabled on GET /whoami before calling hold endpoints,
and fall back to preview gating alone. See Checkout holds.
hold-not-found#
HOLD_NOT_FOUND · 404 · Recoverable
The checkoutHoldId was unknown, expired, or already consumed. POST /orders no longer fails on
this: a lapsed hold is dropped and the commit proceeds as a plain order (the response carries
checkoutHold.committedWithoutHold: true). The code still surfaces from DELETE /orders/hold
and hold-scoped reads.
Fix. Nothing on the commit path. Elsewhere, create a new hold with POST /orders/hold or
proceed from a fresh preview.
hold-mismatch#
HOLD_MISMATCH · 422 · Recoverable
The order body does not match the hold — SKU, quantity, or orderReference differs.
Fix. Reuse the same SKU, quantity, and orderReference that were bound when the hold was
created, or create a new hold for the corrected request.
expected-price-conflict#
EXPECTED_PRICE_CONFLICT · 422 · Recoverable
On POST /orders/hold, the expected price echo did not match the bindable catalog or wholesale
price for the request.
Fix. Re-run POST /orders/preview for the same country and pass the returned price fields on
the hold request.
Hot-drop reservations#
hot-drop-not-enabled#
HOT_DROP_NOT_ENABLED · 412 · Recoverable
The allocation is not in hot-drop mode. POST /orders/reserve is a no-op for that allocation.
Fix. Fall back to Profile B — call POST /orders directly. See Hot drop.
reservation-gone#
RESERVATION_GONE · 410 · Recoverable
The hot-drop reservation was not found, expired, or already consumed.
Fix. Call POST /orders/reserve again, then commit within the new TTL. Do not invent a new
idempotency key for a commit you believe already succeeded — check GET /orders first.
allocation-not-available#
ALLOCATION_NOT_AVAILABLE · 409 · Recoverable
The hot-drop reservation you are committing points at an allocation that is no longer active or no longer in hot-drop mode (the publisher changed the grant between reserve and commit). The reservation is released.
Fix. Preview again and place the order through the normal POST /orders path; if the SKU is
gone from GET /allocations, refund the buyer at your PSP.
quantity-invalid#
QUANTITY_INVALID · 422 · Recoverable
POST /orders/reserve received a quantity that is not a positive integer within the hot-drop
per-reservation cap.
Fix. Send an integer quantity ≥ 1 and at most the maxFulfillableQuantity the preview
reported.
reservation-commit-in-progress#
RESERVATION_COMMIT_IN_PROGRESS · 409 · Recoverable
Another commit for this reservation is already running.
Fix. Retry shortly with the same body and idempotency key. Do not open a second reservation for the same payment.
reservation-extend-exhausted#
RESERVATION_EXTEND_EXHAUSTED · 409 · Not recoverable
The reservation was already extended once; further extensions are not allowed. (Internal
ALREADY_EXTENDED maps to this partner-facing code.)
Fix. Commit before the current TTL expires, or re-reserve if the slot is gone.
Capability gates#
feature-disabled#
FEATURE_DISABLED · 403 · Not recoverable
A capability required by the request is off for this environment. Check
capabilities on GET /whoami and use only features that are enabled for your
key. backorderOnUnavailable: true returns this code while
capabilities.backorderEnabled is false (durable backorders are not generally
available).
Fix. Do not retry with the same request until the capability is enabled for
your environment. Prefer the documented key-delivery path (deliveryMode: "key").
Omit backorderOnUnavailable until backorderEnabled is true.
backorder-not-allowed#
BACKORDER_NOT_ALLOWED · 403 · Not recoverable
backorderOnUnavailable: true was sent and durable backorders are enabled, but
the vendor allocation policy does not allow customer-order backorders for this
grant.
Fix. Omit backorderOnUnavailable, or ask the vendor to enable
request_mode_backorder on the allocation policy. Do not treat this as a
preorder.
order-not-ready#
ORDER_NOT_READY · 409 · Recoverable
POST /orders/{orderId}/fulfill was called before keys were available for the
pending quantity. Wait for order.ready.
Fix. Subscribe to order.ready, then retry fulfill with the same
Idempotency-Key only after that event (or after GET /orders/{orderId}
shows ready).
export-expired#
EXPORT_EXPIRED · 410 · Not recoverable
The catalog export artifact TTL has elapsed. The signed URL is no longer issued.
Fix. Call POST /catalog/exports again and poll the new exportId. Do not retry
the expired id.
already-activated#
ALREADY_ACTIVATED · 409 · Not recoverable
A fulfilment management action was rejected because the fulfilment is already in a terminal activated/redeemed state.
Fix. Do not call regenerate after activation. Issue a new order if the business process requires a fresh fulfilment.
regenerate-not-supported#
REGENERATE_NOT_SUPPORTED · 422 · Not recoverable
The fulfilment provider does not support regenerate for this fulfilment.
Fix. Check the provider on the fulfilment. Do not assume regenerate is available for every delivery path.
keyless-issuance-failed#
KEYLESS_ISSUANCE_FAILED · 503 · Recoverable
Fulfilment issuance failed after inventory pull. Orphaned keys are compensated and the idempotency claim is released so a retry can heal.
Fix. Retry with the same Idempotency-Key after Retry-After (typically 30 seconds). Do
not create a second order against the same payment.
Capabilities and environment#
sandbox-magic-value-rejected#
SANDBOX_MAGIC_VALUE_REJECTED · 422 · Not recoverable
A sandbox fixture — a magic SKU, sandbox_order_* reference, or sandbox_pay_* payment
reference — was used with a production key.
Fix. Replace test fixtures with real values. See Sandbox to production.
Rate limiting and server errors#
rate-limit-exceeded#
RATE_LIMIT_EXCEEDED · 429 · Recoverable
You exceeded the rate limit for this endpoint class. Limits fail closed: if the rate-limit
store is unavailable the request is rejected rather than allowed through, so a burst of 429
responses during an Avrix incident is expected behaviour, not a bug in your client.
Fix. Wait for Retry-After seconds. Longer term: cache catalog reads and honour ETag, since
conditional requests returning 304 do not consume budget. Do not call checkout-authority
endpoints during browsing.
hot-path-timeout#
HOT_PATH_TIMEOUT · 504 · Recoverable
The inventory hot-path RPC exceeded its time budget. The pull did not commit.
Fix. Retry with the same Idempotency-Key after Retry-After (typically 1 second). Safe to
retry — nothing was fulfilled.
key-decrypt-failed#
KEY_DECRYPT_FAILED · 503 · Recoverable
Key material could not be decrypted after a successful pull attempt.
Fix. Retry with the same Idempotency-Key after Retry-After (typically 5 seconds). If it
persists, contact support with the requestId.
billing-record-failed#
BILLING_RECORD_FAILED · 503 · Recoverable
The order pull completed inventory work but the commercial sale record could not be written. Keys are compensated (not delivered); nothing was billed.
Fix. Retry with the same Idempotency-Key after Retry-After. If it persists, escalate with
the requestId.
service-unavailable#
SERVICE_UNAVAILABLE · 503 · Recoverable
A required backing store is temporarily unavailable (for example the hot-drop reservation store).
Fix. Retry with exponential backoff. For writes, reuse the same Idempotency-Key.
internal-error#
INTERNAL_ERROR · 500 · Recoverable
An unexpected server error.
Fix. Retry with exponential backoff, reusing the same Idempotency-Key for writes. If it
persists, contact support with the requestId.
Webhook registration#
Webhook URL checks on POST /webhooks and PATCH /webhooks/{id} surface as VALIDATION_FAILED
(422, schema issues on url) or BAD_REQUEST (400, repository guard messages such as
"URL must use https://" / public-host requirements). Delivery-time SSRF guards use internal
WEBHOOK_URL_* strings and are not returned as partner envelope codes.
OAuth#
Optional Seller API OAuth (POST /api/seller/oauth/token) returns the standard flat Avrix error
envelope — not the RFC 6749 {"error": "..."} body. Mapping from the RFC 6749 concepts:
| HTTP | code | RFC 6749 analogue | When | Action |
|---|---|---|---|---|
| 422 | VALIDATION_FAILED | invalid_request / unsupported_grant_type | Body fails schema: missing field, grant_type is not client_credentials, or client_id is not a UUID | Fix the request body |
| 401 | INVALID_CLIENT | invalid_client / invalid_grant | Unknown client_id, wrong client_secret, expired/revoked/archived key, or the company is revoked/deleted/not a seller | Verify credentials; rotate or replace the key |
| 429 | RATE_LIMIT_EXCEEDED | slow_down | Per-source-IP or per-client_id mint throttle | Back off per Retry-After |
| 501 | NOT_IMPLEMENTED | — | Token minting is disabled on this deployment | Fall back to the long-lived key Bearer |
| 503 | SERVICE_UNAVAILABLE | — | Token store unavailable | Retry with backoff |
Tokens are opaque avrix_oat_* strings; an expired or unknown token on a v1 route fails like any
bad Bearer credential (401 UNAUTHORIZED). Full endpoint contract:
OAuth tokens. Prefer scoped API keys documented in
Authentication.
invalid-client#
INVALID_CLIENT · 401 · Not recoverable without new credentials
The OAuth token mint rejected the client: unknown client_id, wrong client_secret, an
expired/revoked/archived key, or the owning company is revoked, deleted, or not a seller.
Fix. Verify the client_id / client_secret pair against an active API key. If the key was
rotated or revoked, mint credentials from the replacement key.
not-implemented#
NOT_IMPLEMENTED · 501 · Not recoverable on this deployment
OAuth token minting is disabled on this deployment.
Fix. Authenticate with the long-lived API key Bearer instead (see Authentication).
Getting help#
Include in every support request: the requestId, the code, the endpoint and method, the
approximate timestamp with timezone, your orderReference if applicable, and what you expected.
Do not include your API key, key plaintext, or webhook secret.
Related#
- Order lifecycle — failure modes in flow context
- Authentication — credential troubleshooting
- Order preview — catch pricing and availability errors before you charge
Code example#
# Retry manually on 429 / 5xx — read Retry-After from response headers
curl -s -D - "$AVRIX_BASE_URL/api/seller/v1/whoami" \
-H "Authorization: Bearer $AVRIX_API_KEY" | head