Errors
The response envelope and a catalogue of the error codes you'll actually see.
The envelope
Section titled “The envelope”Every response — success or failure — is wrapped the same way:
// success{ "state": "SUCCESS", "payload": { "...": "..." } }
// created (201 responses){ "state": "CREATED", "payload": { "...": "..." } }
// error{ "state": "SOME_ERROR_CODE", "payload": null, "details": { "...": "optional context" } }state is always present. On an error, it carries a stable, machine-readable code — parse and
branch on state, not on the HTTP status code alone, and never on the human-readable text some
errors surface in details.
This holds for every error, including the ones the API Gateway produces itself before your
request reaches a service (a missing token, an unknown route, a wrong method, a downstream outage) —
see Gateway errors below. A service’s own error (e.g. NOT_FOUND_ORDER) passes
through the Gateway unchanged.
A 201 always carries a Location header with the absolute public URL of the new resource,
e.g. https://api-omni.linra.net/api/v1/scent/orders/{orderId} (staging:
https://api-omni-stg.linra.net/...) — never an internal host.
HTTP status ↔ error shape
Section titled “HTTP status ↔ error shape”| HTTP status | Meaning |
|---|---|
| 200 / 201 | Success (state: "SUCCESS" / "CREATED"). |
| 400 | Request validation failed — a malformed/missing body field, or a query parameter that is unknown, unparseable or out of range (see Query parameters). |
| 401 | Missing, invalid, or expired credentials/token (or, as UPSTREAM_AUTH_FAILED, a server-side fault — see Gateway errors). |
| 403 | Authenticated, but not allowed to do this specific thing (e.g. an invalid delegation target, or a route your credential can’t use). |
| 404 | The resource doesn’t exist — or belongs to another partner. Cross-tenant access always reports 404, never 403, so a not-yours resource is indistinguishable from a genuinely nonexistent one. |
| 409 | A conflict — usually a duplicate/in-progress/racing request. |
| 405 | The path exists but not with this HTTP method — the Allow header lists the methods it supports. |
| 415 | The request body isn’t sent as application/json. |
| 422 | The request was well-formed but violates a business rule (e.g. a price that is no longer available). |
| 429 | Rate-limited — see Rate limits. |
| 5xx | Something went wrong on our side (502/503/504 come from the Gateway when a service behind it is unreachable or slow — see Gateway errors). Retries with backoff are safe for these (and, for order creation, always safe regardless of status — see Idempotency). |
Gateway errors
Section titled “Gateway errors”These come from the API Gateway itself, in the same envelope (details.message is a short
human-readable hint):
| Code | HTTP | When | What to do |
|---|---|---|---|
UNAUTHORIZED_MISSING_TOKEN |
401 | No Authorization: Bearer ... header. |
Send a token from POST /api/v1/auth/token. |
UNAUTHORIZED_INVALID_TOKEN |
401 | The token is malformed, tampered, signed for a different audience, or not a partner token. | Fix the request; mint a new token only if you were sending the wrong one. |
UNAUTHORIZED_TOKEN_EXPIRED |
401 | The token is past its 5-minute lifetime (at most 30 seconds of clock skew is allowed). | Mint a new token and retry once. |
UPSTREAM_AUTH_FAILED |
401 | A service behind the Gateway refused the Gateway — not your token. A server-side fault. | Retry later with backoff. Do not re-mint your token. |
FORBIDDEN |
403 | The route is not available to your credential. | Don’t retry. |
AUTH_ENVIRONMENT_MISMATCH |
403 | A staging token sent to the production host, or the reverse. | Use the host that matches your credential’s environment (see Environments). |
NOT_FOUND_ROUTE |
404 | No API route matches the path. (A missing resource on a real route returns that service’s own NOT_FOUND_* code instead.) |
Check the path against the API Reference. |
METHOD_NOT_ALLOWED |
405 | The path exists, but not with this HTTP method. The Allow header lists the supported methods. |
Use a supported method. |
UNSUPPORTED_MEDIA_TYPE |
415 | The body isn’t application/json (or Content-Type is missing). Previously returned as application/problem+json. |
Send Content-Type: application/json. |
BAD_GATEWAY |
502 | A service behind the Gateway returned an invalid response. | Retry with backoff. |
SERVICE_UNAVAILABLE |
503 | A service behind the Gateway is temporarily unavailable. | Retry with backoff. |
GATEWAY_TIMEOUT |
504 | A service behind the Gateway didn’t answer in time. | Retry with backoff (order create is safe to retry with the same externalReference — see Idempotency). |
Every 401 keeps the standard WWW-Authenticate: Bearer ... header; every 405 keeps Allow.
{ "state": "UNAUTHORIZED_TOKEN_EXPIRED", "payload": null, "details": { "message": "The bearer token has expired. Request a new one from POST /api/v1/auth/token." } }Query parameters
Section titled “Query parameters”Query input is strict on every endpoint. A request with a typo that used to be silently
ignored (and return everything) now returns 400, so a filter can never be dropped without you
noticing. The first failing rule decides the code; details.errors names every offending parameter.
| Code | When | Extra details |
|---|---|---|
VALIDATION_UNKNOWN_QUERY_PARAMETER |
A parameter name the endpoint doesn’t have (e.g. ?type= where the real name is referenceType). |
details.allowedParameters lists the accepted names. |
VALIDATION_INVALID_DATE |
A date that doesn’t parse, or a from/to pair with from after to. | — |
VALIDATION_INVALID_PAGE_SIZE |
pageSize is not a whole number from 1 to 100. |
details.minimum / details.maximum |
VALIDATION_INVALID_PAGE |
page is not a whole number of 1 or more. |
details.minimum |
VALIDATION_UNKNOWN_INCLUDE |
An include token the endpoint doesn’t support (e.g. ?include=bogus). |
details.allowedValues.include lists the accepted tokens. |
VALIDATION_INVALID_QUERY_PARAMETER |
A value that doesn’t parse as the parameter’s type — an id that isn’t a GUID, an unknown enum value, a non-number, a boolean other than true/false. |
For an enum, details.allowedValues.{param} lists the accepted values. |
{ "state": "VALIDATION_UNKNOWN_QUERY_PARAMETER", "payload": null, "details": { "errors": { "type": ["'type' is not a parameter of this endpoint."] }, "allowedParameters": ["fromUtc", "page", "pageSize", "referenceType", "sortBy", "sortDescending", "toUtc"] }}Paging: pageSize is 1–100 and defaults to 20; page starts at 1 (the default). Out-of-range
values are rejected, never clamped.
Dates: every date filter (createdFrom/createdTo, fromUtc/toUtc, from/to,
fromDate/toDate, changedSince) is UTC. A value without an offset is read as UTC
(2026-01-01T10:00:00 = 2026-01-01T10:00:00Z), and a date-only value means midnight UTC
(2026-01-01 = 2026-01-01T00:00:00Z). A value with an offset (+03:00) is converted to UTC.
Required booleans: isActive is required on every PATCH .../active call (webhooks, discount
codes). A body without it returns 400 VALIDATION_ERRORS and changes nothing — an empty body never
deactivates anything. Webhook PUT stays a partial update (omitted fields are unchanged).
Common error codes
Section titled “Common error codes”This isn’t every code the API can ever return, but it covers what you’re likely to build error handling around:
| Code | HTTP | When |
|---|---|---|
UNAUTHORIZED_INVALID_CREDENTIALS |
401 | Wrong clientId/clientSecret at the token endpoint. |
UNAUTHORIZED_PARTNER_INACTIVE |
401 | Your partner account is inactive. |
FORBIDDEN_NOT_YOUR_DESCENDANT / FORBIDDEN_DELEGATION_TARGET_INVALID |
403 | An onBehalfOf delegation target you don’t actually have authority over. |
NOT_FOUND_ORDER |
404 | Unknown orderId on GET /api/v1/scent/orders/{orderId} (or its /tracking sibling), or one belonging to another partner. |
NOT_FOUND_SCENT_ORDER |
404 | Unknown orderId on a returns endpoint (POST/GET .../orders/{orderId}/return-requests), or one belonging to another partner. |
NOT_FOUND_WALLET / NOT_FOUND_STATEMENT |
404 | No wallet provisioned yet / unknown or not-yours statement. |
NOT_FOUND_COMMISSION_PERIOD |
404 | Unknown commission period, or one belonging to another partner. |
NOT_FOUND_WEBHOOK_SUBSCRIPTION |
404 | Unknown subscription id. |
NOT_FOUND_SCENT / NOT_FOUND_BRAND / NOT_FOUND_VARIANT |
404 | Unknown catalog entity. A variant that exists but isn’t in your account’s assortment is reported exactly the same way (NOT_FOUND_VARIANT) — on a variant read, adding it to your cart (or increasing its quantity), your wishlist, or a back-in-stock subscription. |
NOT_FOUND_CART_ITEM |
404 | No such line in your cart. |
NOT_FOUND_FULFILLMENT_GROUP / NOT_FOUND_ORDER_LINE |
404 | Unknown target for a return request. |
VALIDATION_MISSING_REPORT_RANGE |
400 | GET /api/v1/statements/report without both from and to. |
VALIDATION_INVALID_COMMISSION_PERIOD_STATUS |
400 | An unrecognized status value on GET /api/v1/commission-periods (not one of Provisional/Issued/Settled). |
VALIDATION_INVALID_PERIOD |
400 | fromUtc equals toUtc on a sales report endpoint (GET /api/v1/scent/reports/sales/**). fromUtc after toUtc is VALIDATION_INVALID_DATE. |
VALIDATION_REPORT_WINDOW_TOO_LARGE |
400 | A sales report window (fromUtc/toUtc) spans more than 400 days. |
VALIDATION_ERRORS |
400 | A request body failed field validation — e.g. an empty or over-1000-character reason on POST /api/v1/commission-periods/{id}/objection, or a PATCH .../active body without isActive. details.errors names the offending field(s). |
VALIDATION_UNKNOWN_LOOKUP_CODE |
400 | An unknown concentration or audience code on a catalog list (details.list, details.value). |
VALIDATION_WEBHOOK_URL_UNSAFE |
400 | A webhook url that isn’t a valid absolute https URL, doesn’t resolve, or resolves to a private/non-routable address (details.reason). |
VALIDATION_PRICE_LOCK_INVALID / VALIDATION_PRICE_LOCK_EXPIRED |
400 | A lockId that’s unusable (unknown/not-yours/cart-mutated, or expired) — see Cart preview & price locks. |
VALIDATION_INVALID_COUNTRY_CODE |
400 | shippingAddress.countryCode isn’t a recognized ISO 3166-1 alpha-3 code. |
VALIDATION_CUSTOMER_REF_TOO_LONG |
400 | customerRef over 128 characters. |
VALIDATION_WEBHOOK_EVENT_TYPE_NOT_SUBSCRIBED |
400 | Test-firing an event type your subscription isn’t subscribed to. |
VALIDATION_INVALID_REASON_CODE |
400 | An unrecognized reasonCode on a return request line. |
VALIDATION_INVALID_RETURN_STATUS |
400 | An unrecognized status filter value on GET /api/v1/scent/return-requests (not one of Requested/Accepted/Processing/Refunded/Rejected/Withdrawn). |
BUSINESS_PRICE_NOT_AVAILABLE |
422 | A price in the cart (or the price lock) is no longer available — prices moved since you priced it. Re-run the checkout preview and check out again at the new price. The checkout preview reports the same condition as the issue PRICE_NOT_AVAILABLE. |
BUSINESS_VARIANT_UNAVAILABLE |
422 | Adding a variant to your cart or wishlist that isn’t currently available for purchase. |
BUSINESS_VARIANT_NOT_PRICED |
422 | Adding a variant to your cart that has no active price for your account. |
BUSINESS_PRODUCT_ACCESS_DISABLED |
422 | Linra has paused your partner account’s access to start anything new on Scent (existing orders keep fulfilling). Nothing to retry — stop attempting new cart/order actions and contact your account manager. Always check the checkout-preview’s cartIssue first — see Cart preview & price locks — so you find out before you even try. |
BUSINESS_CART_NOT_CHECKOUT_READY |
422 | POST .../cart/{customerRef}/price-lock was called on a cart whose recomputed preview reports canCheckout: false — a broken basket is never locked. Order-create returns the same code with details.cartIssue: "VARIANT_NOT_ASSIGNED" and details.variantIds when a cart line’s variant was removed from your assortment after it was carted (this applies to a create with a lockId too). Remove those lines and preview again. |
BUSINESS_WEBHOOK_SUBSCRIPTION_LIMIT |
422 | You already have 5 active subscriptions. |
BUSINESS_VARIANT_ALREADY_AVAILABLE |
422 | A back-in-stock registration for a variant that is already available — just add it to the cart. |
BUSINESS_RETURN_REQUEST_GROUP_NOT_DELIVERED / _WINDOW_EXPIRED / _EXCEEDS_REMAINING / _DUPLICATE_LINE |
422 | A return request eligibility rule failed — see the API Reference for each. |
BUSINESS_RETURN_REQUEST_NOT_PENDING |
422 | You tried to withdraw a return request that’s no longer Requested — we’ve already taken some action on it, so withdrawal is locked out permanently. |
CONFLICT_CART_HAS_PENDING_ORDER |
409 | The cart already has an order in flight. |
CONFLICT_CART_CHECKOUT_RACE / CONFLICT_PRICE_LOCK_CREATE_RACE |
409 | A concurrent request on the same cart lost a race — safe to retry. |
CONFLICT_ORDER_DUPLICATE_REFERENCE |
409 | A create with this externalReference already succeeded — details.orderId names the existing order; fetch it via GET /api/v1/scent/orders/{orderId} rather than retrying. See Idempotency. |
CONFLICT_ORDER_IN_PROGRESS |
409 | A create with this externalReference is still being processed. Retry again shortly. |
CONFLICT_ORDER_REQUIRES_INTERVENTION |
409 | A prior attempt with this externalReference hit an unrecoverable state and needs operator intervention before it can be retried. Contact your account manager rather than retrying. |
CONFLICT_PRICE_LOCK_ALREADY_USED |
409 | The lockId you passed to order-create was already used to create an order (single-use). details.orderId names that order. |
CONFLICT_COMMISSION_PERIOD_ALREADY_OBJECTED / _NOT_PROVISIONAL / _PAST_ISSUE_DATE |
409 | POST /api/v1/commission-periods/{id}/objection — see Commission periods. |
RATE_LIMITED |
429 | See Rate limits. |
The Gateway and query-parameter codes are listed in their own sections above.
What error responses never do
Section titled “What error responses never do”- Never distinguish “wrong client ID” from “wrong secret” at the token endpoint — both are the
same generic
UNAUTHORIZED_INVALID_CREDENTIALS, to avoid letting a caller enumerate valid client IDs. - Never leak a stack trace, an internal exception message, or implementation detail in
details. - Never return a 200 with an error state embedded in the payload — if
stateisn’tSUCCESS/CREATED, the HTTP status code is always a matching 4xx/5xx too.

