Skip to content
Linra Omni Vendor Docs

Rate limits & errors

The exact sliding-window limits, the response envelope, and the error codes you'll actually see.

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 Requests
Retry-After: 12
Content-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.

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 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).

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 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.

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.
  • 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 state isn’t SUCCESS/CREATED, the HTTP status code is always a matching 4xx/5xx too.