> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kintra.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Request conventions

> The error envelope and its codes, the idempotency header a write needs, and what a rate limit returns.

Three rules apply across both surfaces: what an error body looks like, when a write has to carry an idempotency key, and what happens when you exceed the rate limit. {/* trace: C-29, C-19, C-37 */}

## The error envelope

Every error the API returns through its error handler uses one envelope, an `error` object holding a `code`, a `message` and sometimes a `details`. {/* trace: C-29, C-30 */}

```json theme={null}
{
  "error": {
    "code": "VALIDATION_INVALID_INPUT",
    "message": "A human readable reason",
    "details": {
      "fields": [{ "message": "why this field was rejected", "path": "the field it applies to" }]
    }
  }
}
```

Branch on `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. {/* trace: C-33 */}

There are 59 codes in all, 56 application codes and 3 infrastructure codes. {/* trace: C-32 */}

`error.details` is present only when there is something in it, so treat it as optional rather than assuming a shape. {/* trace: C-30 */}

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. {/* trace: C-31 */}

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. {/* trace: C-34 */}

## 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. {/* trace: C-19 */}

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. {/* trace: C-20 */}

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`. {/* trace: CreateCustomer, PatchCustomerStatus, AssignCustomerClub, SetCustomerCountry, LockXp, CreateReward, UpdateReward, CreateTierRule, UpdateTierRule, EarnXp, RedeemReward */}

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. {/* trace: C-42 */}

Any route on either list sent without the header returns 400 with the code `IDEMPOTENCY_KEY_REQUIRED`. {/* trace: C-21 */}

The header name is read case-insensitively, so the casing you send does not matter. {/* trace: C-22 */}

Keys are stored per tenant, so a key you pick cannot collide with one another tenant picked. {/* trace: C-23 */}

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. {/* trace: C-24 */}

Replaying a key whose first request finished returns the stored status code and the stored body, without running the operation again. {/* trace: C-25 */}

Reusing a key with a different request returns 422 with the code `IDEMPOTENCY_KEY_CONFLICT`. {/* trace: C-26 */}

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. {/* trace: C-27 */}

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. {/* trace: C-28 */}

## 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. {/* trace: C-35, C-40 */}

Exceeding the limit returns 429. {/* trace: C-37 */}

The response carries the standard `RateLimit-*` headers; the older `X-RateLimit-*` headers are switched off, so read the limit state from the standard names. {/* trace: C-36 */}

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. {/* trace: C-39 */}

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. {/* trace: C-38 */}

On a 429, back off and retry rather than retrying immediately, and use the `RateLimit-*` headers on the response to decide when. {/* trace: C-36, C-37 */}
