Rate limits & errors
The exact sliding-window limits, the response envelope, and the error codes you'll actually see.
Rate limits
Section titled “Rate limits”Every route on the vendor Gateway (api-omni-vendors.linra.net) is rate-limited using a sliding
window (4 sub-segments — a burst right at a window boundary is smoothed, not doubled), partitioned
per authenticated caller (your credential’s client id), falling back to caller IP only on the
anonymous token endpoint itself.
| Route | Limit | Window |
|---|---|---|
POST /api/v1/auth/token |
10 requests | 60 seconds |
| Every other route | 120 requests | 60 seconds |
HTTP/1.1 429 Too Many RequestsRetry-After: 12Content-Type: application/json
{ "state": "RATE_LIMITED", "payload": null, "details": { "message": "Too many requests. Please try again later." } }Respect the Retry-After header (seconds) rather than retrying immediately or on a fixed-interval
guess. If you’re hitting the token endpoint’s 10/min limit, you almost certainly have a bug where
you’re fetching a fresh token on every request instead of caching it (see
Authentication & capabilities). The vendor data-route budget (120/min)
is intentionally more conservative than the partner API’s own 300/min — vendor integration traffic
is typically lower-volume (offer/stock/cost pushes, order polling) than partner order/cart traffic.
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. This holds for every error, including the
ones the vendor Gateway produces itself (see Gateway errors); a service’s own
error (e.g. NOT_FOUND_VENDOR_OFFER) 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-vendors.linra.net/api/v1/scent/offers/{id} (staging:
https://api-omni-vendors-stg.linra.net/...) — never an internal address.
HTTP status ↔ error shape
Section titled “HTTP status ↔ error shape”| HTTP status | Meaning |
|---|---|
| 200 / 201 | Success. |
| 400 | Request validation failed — a malformed/missing/out-of-range 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). |
| 403 | Authenticated, but your token doesn’t carry the capability this endpoint requires (or the route isn’t available to vendors: FORBIDDEN). |
| 404 | The resource doesn’t exist — or belongs to another vendor. Cross-vendor access always reports 404, never 403, so a not-yours resource is indistinguishable from a genuinely nonexistent one. |
| 405 | The path exists but not with this HTTP method — the Allow header lists the supported methods. |
| 415 | The request body isn’t sent as application/json. |
| 422 | The request was well-formed but violates a business rule (e.g. the webhook subscription cap). |
| 429 | Rate-limited. |
| 5xx | Something went wrong on our side — safe to retry with backoff (502/503/504 come from the Gateway when a service behind it is unreachable or slow). |
Gateway errors
Section titled “Gateway errors”These come from the vendor 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 vendor 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 on the vendor API. | Don’t retry. |
NOT_FOUND_ROUTE |
404 | No API route matches the path (including a malformed id, e.g. /api/v1/scent/returns/not-a-guid). |
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. |
Every 401 keeps the standard WWW-Authenticate: Bearer ... header; every 405 keeps Allow.
Query parameters
Section titled “Query parameters”Query input is strict. 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.
details.errors names every offending parameter.
| Code | When | Extra details |
|---|---|---|
VALIDATION_UNKNOWN_QUERY_PARAMETER |
A parameter name the endpoint doesn’t have. | details.allowedParameters lists the accepted names. |
VALIDATION_INVALID_DATE |
A date (e.g. changedSince) 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. |
details.allowedValues |
VALIDATION_INVALID_QUERY_PARAMETER |
A value that doesn’t parse — an id that isn’t a GUID, an unknown enum value (e.g. ?status=Banana on the orders list), a non-number, a boolean other than true/false. |
For an enum, details.allowedValues.{param} lists the accepted values. |
Paging: pageSize is 1–100 and defaults to 20; page starts at 1. Out-of-range values are
rejected, never clamped.
Dates: date filters are UTC. A value without an offset is read as UTC, and a date-only value
means midnight UTC (2026-01-01 = 2026-01-01T00:00:00Z).
Required fields: isActive is required on PATCH /api/v1/scent/webhooks/{id}/active and
PATCH /api/v1/credentials/{id}/active, and declaredInStock is required on
PATCH /api/v1/scent/offers/{offerId}/stock (false with a quantity above 0, or true with a
quantity of 0, is rejected too). A body without them returns 400 VALIDATION_ERRORS and changes
nothing. Webhook PUT stays a partial update.
Common error codes
Section titled “Common error codes”| Code | HTTP | When |
|---|---|---|
UNAUTHORIZED_INVALID_CREDENTIALS |
401 | Wrong clientId/clientSecret at the token endpoint. |
UNAUTHORIZED_VENDOR_INACTIVE |
401 | Your vendor account is inactive. |
FORBIDDEN_CAPABILITY_NOT_GRANTED |
403 | Your token doesn’t carry the capability this endpoint requires — see details.requiredCapability. |
NOT_FOUND_VENDOR_OFFER |
404 | Unknown offer id, or one belonging to another vendor. |
NOT_FOUND_FULFILLMENT_GROUP |
404 | Unknown fulfilment group id, or one belonging to another vendor. |
NOT_FOUND_VENDOR_WEBHOOK_SUBSCRIPTION |
404 | Unknown subscription id. |
VALIDATION_ERRORS |
400 | A request body failed field validation (e.g. a missing isActive or declaredInStock). details.errors names the offending field(s). |
VALIDATION_WEBHOOK_EVENT_TYPE_NOT_SUBSCRIBED |
400 | Test-firing an event type your subscription isn’t subscribed to. |
BUSINESS_VENDOR_WEBHOOK_SUBSCRIPTION_LIMIT |
422 | You already have 5 active subscriptions. |
RATE_LIMITED |
429 | See 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 an 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.

