Skip to content

Errors & Rate Limits

Last updated October 20, 2026

Error response shape

Every error is JSON with a stable machine-readable error code and a human-readable message. Switch on error; log message. Never parse message.

{
  "error": "not_found",
  "message": "The requested fulfilment order does not exist."
}

Validation errors add an errors array with one entry per problem, so you can surface all of them at once instead of fixing them one request at a time.

{
  "error": "validation_error",
  "message": "One or more parcel updates are invalid.",
  "errors": [
    {
      "parcel_id": "OMN-KONW-00042-1-1",
      "field": "tracking_number",
      "message": "tracking_number is required"
    },
    {
      "parcel_id": "OMN-KONW-00042-1-2",
      "field": "currency",
      "message": "currency is required when shipping_cost is supplied"
    }
  ]
}

On the shipment-level tracking endpoint each entry is keyed by parcel_id; on the order-level endpoint it is keyed by tracking_number, because parcels do not exist yet at that point. If the key field is itself missing, the key is null and the message gives the entry's index, e.g. "tracking_number is required (parcels[0])". On POST /v1/inventory each entry carries sku and warehouse_code.


Error code reference

HTTP Code When it happens Retryable
401 missing_api_key No Authorization header, not a Bearer token, or the token does not start with omni_sk_. No — fix the request
401 invalid_api_key The key is not recognised or has been revoked. No — get a new key
400 validation_error Bad or unknown query parameter, malformed JSON body, or one or more invalid parcel or inventory entries. Includes an errors array where parcels or inventory entries are involved. On GET /v1/fulfilment-orders, an unknown parameter is rejected with a hint naming the supported ones. No — fix the request
404 not_found Unknown endpoint, or the order / shipment / parcel does not exist for this seller. Another seller's record is indistinguishable from a missing one, and an order waiting on an Omnishelf-supplied label returns 404 until the label is attached (see Authentication). No
405 method_not_allowed The path exists but not for this HTTP method. The Allow header lists the methods the path accepts. No — use a listed method
409 label_not_ready No label has been generated yet for the requested parcel, shipment or order. Yes — poll again shortly
409 omnishelf_oms_order_missing Tracking submission on an order that is not yet linked to the fulfilment system. Yes — retry later, or contact Omnishelf
409 request_pending Tracking submission while a cancellation or SKU change request is open on the order. Nothing is saved or sent to the fulfilment system. Yes — once Omnishelf has decided the request
409 idempotency_conflict POST /v1/inventory: the Idempotency-Key was already used within 24 hours with a different request body. No — use a new key, or resend the original body
413 too_many_parcels A label request covers more than 20 parcels. Drop to shipment or parcel level. No — split the request
429 rate_limit_exceeded More than 100 requests in the current 60-second window for this key. Yes — after Retry-After
502 label_fetch_failed The label exists but could not be retrieved from the carrier system. Yes — retry shortly
502 omnishelf_oms_push_failed Tracking could not be written through to the fulfilment system. Nothing was saved on the Omnishelf side. Yes — safe to retry as-is
500 internal_error An unexpected failure. Logged on the Omnishelf side. Yes — with backoff
503 service_unavailable A tracking submission could not be checked against the order. Nothing was saved or sent. Yes — retry shortly

Retry only what is retryable

Retrying a 400 or 404 will never succeed and just burns rate limit. Retry 409 label_not_ready, 429, 502, 500 and 503 with exponential backoff. Both tracking submission endpoints are idempotent, so a retry after a network timeout is always safe — resubmitting a tracking number that is already stored returns 200 with "result": "unchanged" and writes nothing.


Rate limits

100 requests per minute per API key, counted in a fixed 60-second window. The limit is per key, not per endpoint or per order.

Header On Meaning
X-RateLimit-Limit Every authenticated response Requests allowed per window — currently 100.
X-RateLimit-Remaining Every authenticated response Requests left in the current window. 0 on a 429.
X-RateLimit-Reset Every authenticated response Unix epoch seconds at which the window resets.
Retry-After 429 only Whole seconds to wait before retrying.
{
  "error": "rate_limit_exceeded",
  "message": "Too many requests. Please retry after the reset window."
}

Back off using the reset header rather than a fixed sleep:

response=$(curl -s -D /tmp/h -o /tmp/b -w '%{http_code}' \
  "$BASE_URL/v1/fulfilment-orders" \
  -H "Authorization: Bearer $OMNI_KEY")
if [ "$response" = "429" ]; then
  reset=$(grep -i '^x-ratelimit-reset:' /tmp/h | tr -d '\r' | awk '{print $2}')
  now=$(date +%s)
  sleep $(( reset - now + 1 ))
fi

If the limiter itself is unavailable the API fails open rather than blocking a valid key, so an occasional burst above 100 may succeed. Do not design around that.