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

# Conventions

> Money, identifiers, error shape, rate limits and pagination

## Money

Every monetary value is an **integer count of minor units** of its stated
currency.

| Currency | Minor unit         | Human amount | On the wire |
| -------- | ------------------ | ------------ | ----------- |
| INR      | paise              | ₹500.00      | `"50000"`   |
| USD      | cents              | \$25.00      | `"2500"`    |
| JPY      | yen (zero-decimal) | ¥1000        | `"1000"`    |

| Direction    | Accepted types       | Validation                                                                                                                                                             |
| ------------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| In responses | `string`             | Always a string. Values can exceed what a double holds exactly, and a string is the only lossless JSON encoding.                                                       |
| In requests  | `number` or `string` | A number must be a safe integer (≤ 2⁵³−1) or the request is **rejected rather than rounded**. A string must match `^[0-9]+$` — no sign, no decimal point, no exponent. |

One order carries one currency, a three-letter uppercase ISO 4217 code matching
`^[A-Z]{3}$`. Lower case is rejected. A mixed-currency order has no single
amount to check against and is refused.

## Identifiers

| Field               | Form   | Owned by | Constraints                                                                                    |
| ------------------- | ------ | -------- | ---------------------------------------------------------------------------------------------- |
| `order_id`          | UUID   | Endl     | Returned by create. Also the pagination cursor for list.                                       |
| `item_id`           | UUID   | Endl     | Stable per order line.                                                                         |
| `voucher_id`        | UUID   | Endl     | Safe to log. Use it as the reference in support tickets.                                       |
| `canonical_sku`     | string | Endl     | For example `BIGBASKET--IN--INR`. Stable, and the catalogue cursor.                            |
| `partner_order_ref` | string | **You**  | 1–200 characters, trimmed, unique across your orders. Your lookup key when a response is lost. |
| `payment_ref`       | string | **You**  | 1–200 characters. Recorded against the order, never interpreted or verified.                   |

<Note>
  A malformed identifier answers exactly like an unknown one. Sending
  `not-a-uuid` returns `404 ORDER_NOT_FOUND` rather than a validation error, so an
  id-guessing caller learns nothing from the difference.
</Note>

## Error shape

Every error, on every endpoint, has the same two fields.

```json theme={null}
{ "code": "VALIDATION_ERROR", "message": "items must be a non-empty array" }
```

Codes are **contractual and stable**. Messages are written for a human reading a
log and may be reworded without notice — never match on them.

## Rate limits

| Bucket                  | Sustained | Keyed on  | Shares budget with                                           |
| ----------------------- | --------- | --------- | ------------------------------------------------------------ |
| `/orders*`              | 300 / min | Your key  | All four signed routes                                       |
| `/orders/{id}/vouchers` | 30 / min  | Your key  | Nothing — its own budget, spent **in addition** to the group |
| `/catalog/products`     | 120 / min | Source IP | Every caller behind that IP                                  |

A refusal is `429 RATE_LIMITED` with a `Retry-After` header in whole seconds,
never below 1. Honour it — retrying sooner only spends the next window.

<Note>
  Limits are enforced as a token bucket, so a short burst above the sustained rate
  is admitted. **That burst allowance is not contractual** and should not be
  designed against.
</Note>

## Size limits

| Limit                  | Value       | Exceeding it returns      |
| ---------------------- | ----------- | ------------------------- |
| Request body           | 1 MiB       | `413`                     |
| Items per order        | 1–50        | `VALIDATION_ERROR`        |
| Units per item (`qty`) | 1–1000      | `QUANTITY_LIMIT_EXCEEDED` |
| String fields          | 1–200 chars | `VALIDATION_ERROR`        |

## Pagination

| Endpoint            | Cursor is       | Default | Max | End of pages                |
| ------------------- | --------------- | ------- | --- | --------------------------- |
| `/catalog/products` | `canonical_sku` | 100     | 500 | `next_cursor` is `null`     |
| `/orders`           | `order_id`      | 25      | 100 | A page shorter than `limit` |

`limit` is **clamped rather than rejected**: a value above the maximum silently
becomes the maximum, and a non-numeric value becomes the default.

<Warning>
  The two endpoints end differently. The catalogue gives you a `next_cursor` that
  goes `null`. **`GET /orders` returns no cursor field at all** — continue from the
  `order_id` of the last order in the page, and stop when a page comes back shorter
  than `limit`.
</Warning>
