The error envelope
Every error the API returns through its error handler uses one envelope, anerror object holding a code, a message and sometimes a details.
error.code, not on error.message. The codes come from an enum the platform labels as stable API error codes, which is what makes them safe to switch on.
There are 59 codes in all, 56 application codes and 3 infrastructure codes.
error.details is present only when there is something in it, so treat it as optional rather than assuming a shape.
Field validation failures arrive as error.details.fields, a list of message and path pairs, under the code VALIDATION_INVALID_INPUT when no more specific code applies.
On a 5xx the message is replaced with a generic one in production and no stack is included, so do not match on the text of a 5xx.
Idempotency
X-Idempotency-Key is attached route by route rather than to the API as a whole, so it is required on the routes that carry it and on no others.
The middleware acts on POST, PUT and PATCH only. A route that carries it still exempts its DELETE and its GET, and sending a key on one of those does nothing.
On the gateway surface the routes that require a key are POST /gateway/customers, PATCH /gateway/customers/{userId}, POST /gateway/customers/{userId}/clubs, PUT /gateway/customers/{userId}/country, PUT /gateway/customers/{userId}/xp/lock, POST /gateway/rewards, PUT /gateway/rewards/{rewardId}, POST /gateway/tiers, PUT /gateway/tiers/{tierId}, POST /gateway/xp/earn and POST /gateway/xp/redeem.
On the portal surface the routes that require a key are POST /tenant/{tenantId}/xp/earn and POST /tenant/{tenantId}/xp/redeem. No other portal route carries the middleware.
Any route on either list sent without the header returns 400 with the code IDEMPOTENCY_KEY_REQUIRED.
The header name is read case-insensitively, so the casing you send does not matter.
Keys are stored per tenant, so a key you pick cannot collide with one another tenant picked.
The stored record holds a hash of the method, the path and the body, and that hash is what a later request carrying the same key is compared against.
Replaying a key whose first request finished returns the stored status code and the stored body, without running the operation again.
Reusing a key with a different request returns 422 with the code IDEMPOTENCY_KEY_CONFLICT.
Replaying a key while its first request is still in flight returns 409, also with the code IDEMPOTENCY_KEY_CONFLICT, and a message saying the request is still being processed.
A request that died before its response was recorded leaves its key stuck: the record stays pending, and retrying with that key keeps returning 409 for the rest of the record’s retention. Send a fresh key rather than the one that failed.
Rate limits
The API rate limits by IP address, ahead of authentication and across the whole API except the info endpoints, so an unauthenticated request spends the same allowance as a signed one. Exceeding the limit returns 429. The response carries the standardRateLimit-* headers; the older X-RateLimit-* headers are switched off, so read the limit state from the standard names.
The window and the maximum come from the environment’s configuration rather than from the API, so they differ between environments. Read the state off the headers instead of hard-coding what you measured.
The 429 is the one response on either surface that does not use the envelope above. Its body is success set to false and error set to a string, with no error.code anywhere in it, so a client that branches on error.code has to special-case this response.
On a 429, back off and retry rather than retrying immediately, and use the RateLimit-* headers on the response to decide when. 
