# Idempotency, rate limits and errors

The parts of the Supplier API that keep an unattended integration honest: how to retry a write without shipping a parcel twice, how much you can send, and what every error looks like.

## Request and response conventions

**Success envelope.** Every `2xx` response wraps the result:

```json
{ "statusCode": 200, "message": "Success", "data": { ... }, "timestamp": "2026-09-07T09:01:20.963Z" }
```

`message` is `"Created"` on a `201` and `"Success"` otherwise. The shapes in these guides describe `data`.

**Validation is strict.** A request body or query string containing a property the endpoint does not know is a `400`, with the message `property <name> should not exist`. Numbers in a JSON body must be JSON numbers, not strings. Path ids must be UUIDs; anything else answers `400 Validation failed (uuid is expected)`.

**Ids are scoped to you.** A purchase order, webhook subscription or delivery that belongs to someone else answers the same `404` as one that never existed.

## Idempotency-Key

A write that times out on your side may or may not have been applied on Orbit's. For `POST /api/supplier-api/v1/orders/{id}/shipments`, the one call where a blind retry could ship the same parcel twice, send an `Idempotency-Key` header and retry freely.

```http
POST /api/supplier-api/v1/orders/3d5f7e9c-1b0a-4c8e-9c4e-2b1a7f3d4c8e/shipments HTTP/1.1
Idempotency-Key: wms-despatch-88213
Content-Type: application/json
```

| Rule | Behaviour |
|---|---|
| Format | 1 to 255 printable ASCII characters. Anything else is a `400`. Use something your system already has, such as the despatch or parcel id. |
| Scope | Per API key. Keys from different systems, or different suppliers, can never collide. |
| First request | Runs normally. Its response is stored for 24 hours. |
| Same key again within 24 hours | The stored response is returned unchanged, with the header `Idempotency-Replayed: true`, and nothing is written again. |
| Same key while the first is still running | `409 A request with this Idempotency-Key is still being processed`. Wait and retry. |
| The first request failed | Nothing is stored. A retry with the same key runs for real, so a validation error corrected and resent goes through. |

Two things to design around:

- **The body is not compared.** A replay with the same key and a different body returns the first response. Never reuse a key for a different parcel; derive it from the parcel, not from a counter you might reset.
- **Only the shipments endpoint reads the header.** The tracking endpoint is safe to retry without it because an unchanged row is `skipped`, acknowledging is only accepted once, and stock updates set absolute levels. Webhook creation is guarded by the one-per-topic rule.

## Rate limits

Limits are per key, in fixed one-minute windows aligned to the clock:

| Requests | Limit |
|---|---|
| Reads (`GET`) | 600 per minute |
| Writes (`POST`, `PATCH`, `DELETE`) | 300 per minute |

Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (the window's end, as a Unix timestamp in seconds). Past the limit you receive:

```json
{
  "statusCode": 429,
  "timestamp": "2026-09-07T09:31:02.410Z",
  "path": "/api/supplier-api/v1/stock/bulk",
  "method": "POST",
  "message": "Rate limit exceeded. Max 300 requests per minute for Supplier API endpoints.",
  "retryAfter": 14
}
```

with a matching `Retry-After` header in seconds. Sleep for that long and continue. Bulk endpoints exist so the limit rarely matters: 500 stock rows or 500 tracking rows per call, 100 lines per shipment.

Three smaller ceilings apply on top:

- Webhook test pings: 10 a minute per key.
- Redeliveries: 30 a minute per subscription.
- A platform-wide limit of 600 requests a minute per client IP address, in front of everything. Its `429` is a plain message without the `X-RateLimit-*` headers. If several systems call Orbit from behind one NAT address, spread their schedules so they do not burst together.

## Errors

Errors other than `401` share one shape. `message` is a string for a rule violation and an array of strings for validation failures, one per problem. Extra fields such as `retryAfter` appear when they apply.

```json
{
  "statusCode": 400,
  "timestamp": "2026-09-07T09:33:15.020Z",
  "path": "/api/supplier-api/v1/orders/3d5f7e9c-1b0a-4c8e-9c4e-2b1a7f3d4c8e/shipments",
  "method": "POST",
  "message": [ "lines must contain at least 1 elements" ]
}
```

A `401` is shorter, with no path:

```json
{ "statusCode": 401, "error": "unauthorized", "message": "Invalid or expired token", "timestamp": "2026-09-07T09:33:15.020Z" }
```

| Status | When | What to do |
|---|---|---|
| `400` | Validation, an unknown property, a non-UUID id, or a business rule such as over-shipping, acknowledging a non-`sent` order, an unknown product id in a stock batch, a badly formed `Idempotency-Key`. | Fix the request. Do not retry as-is. |
| `401` `No API key provided` | The `Authorization` header is missing or is not `Bearer …`. | Fix the header. |
| `401` `Invalid or expired token` | The key is unknown, revoked, or past its expiry. The message is deliberately the same for all three. | Stop and alert. Retrying will not help. |
| `403` `Missing required scopes: …` | The key is valid but lacks the scope this endpoint needs. The message names it. | Mint a key with the scope. Scopes cannot be added to a live key. |
| `404` | The id does not exist for this supplier: order, line item on this order, subscription, delivery. | Treat as gone. |
| `409` | Idempotency request still running; a subscription for this topic already exists; a redelivery on a paused, disabled or dead-key subscription; more than 10 active keys. | Wait or resolve the state, then retry. |
| `429` | A limit above. | Honour `Retry-After`. |
| `500` | An unhandled error. The body carries a `requestId`. | Retry with backoff; quote the `requestId` if you contact support. |

### A retry policy that fits

- `429` and `500`: retry with exponential backoff, using `Retry-After` when present.
- Network timeouts on a shipment: retry with the same `Idempotency-Key`.
- Network timeouts on anything else: retry as-is; the other writes are safe to repeat.
- `400`, `401`, `403`, `404`: do not retry. Log, alert, move to the next item so one bad row never stalls a batch.

## Next steps

- The endpoints these rules apply to: [Purchase orders and shipments](/supplier-api/guides/purchase-orders), [Stock updates](/supplier-api/guides/stock), [Webhooks](/supplier-api/guides/webhooks).
- Field-level schemas: [Supplier API Reference](/supplier-api/reference).
