API conventions

One envelope, explicit error codes, honest rate limits.

The envelope

Every success wraps its payload in data, with list metadata in meta when there is any. Every failure is an error object with a stable machine-readable code, a human message, optional details, and the requestId to quote when you write to support.

Success
{
  "data": { … },
  "meta": { … }
}
Failure
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "…",
    "details": { … },
    "requestId": "…"
  }
}

Error codes you will meet

  • 401INVALID_API_KEYThe API key is missing, unknown or revoked
  • 401UNAUTHENTICATEDNo usable token on a route that needs one
  • 403INSUFFICIENT_SCOPEThe token lacks the scope this endpoint declares
  • 422VALIDATION_FAILEDThe body or query did not validate; details says what
  • 429RATE_LIMITEDOver the key’s limit; wait for Retry-After
  • 502UPSTREAM_UNAVAILABLEA service behind the API did not answer

Rate limits

Requests are counted per API key over a 60-second window. The default is 120 requests per minute, and a key can be issued with a higher limit — up to 10,000. Every response carries the three headers below; going over answers 429 RATE_LIMITED with a Retry-After header. Back off until it passes rather than retrying in a loop.

On every response
x-ratelimit-limit: 120
x-ratelimit-remaining: 118
x-ratelimit-reset: 1758854400

Checkout carries its own stricter limits on top: quoting and placing orders are throttled per cart and per shopper, and a throttled response says which dimension tripped.

Request ids

Send your own X-Request-Id (letters, digits, ._~-, at most 64 characters) and it is echoed on the response; without one, an id is assigned. Failures repeat it inside the error envelope as requestId.

Pagination

Catalogue lists take a page query parameter and answer with list metadata in meta.

Versioning

The version is in the path — /v1/…. Additive changes (new fields, new endpoints) land in the current version; breaking changes only ever arrive as a new version path, with the old one kept running while integrations move.