REST API · Guide
Errors & retries
Every error has the same shape. Branch on error.code (stable) and show error.message (written
for people).
{
"ok": false,
"error": { "code": "validation_error", "message": "packages[0].weight must be greater than 0" },
"request_id": "01JBX…",
"trace_id": "…",
"timestamp": "2026-10-31T15:04:05Z"
} Codes
| HTTP | code | What to do |
|---|---|---|
| 400 | validation_error | Something in the request is wrong. The message says what; fix it before retrying. |
| 401 | unauthorized | The token is missing or expired. Get a new one from /v1/api-clients/token and retry. |
| 403 | api_key_not_allowed | API keys can never do this: a read-only key tried to change something, or any key tried to add a card, manage keys or change account settings. Do it in the dashboard. |
| 403 | forbidden | Your role on the account doesn't allow it, or the resource isn't yours. |
| 403 | account_restricted | The account is on hold or under review. Contact support. |
| 404 | not_found | It doesn't exist on this account (or belongs to another account). |
| 404 | endpoint_moved | An old path. error.moved_to has the new one. |
| 409 | conflict | The account's state doesn't allow it. The message says why, e.g. insufficient_funds (top up first) or an already-cancelled label. |
| 413 | payload_too_large | The request body is too big. |
| 429 | rate_limited | Too many requests. Wait the Retry-After header (or error.retry_after_seconds) seconds, then retry. |
| 500 | internal_error | Something failed on our side. Retry later; quote the request_id if it keeps happening. |
| 503 | service_unavailable | A carrier or payment service didn't answer. Nothing was done; retry in a minute. |
A validation_error can also list error.fields, one entry per field that is wrong. Quote the request_id when you contact support: it finds the exact call.
Retrying safely
- Buying a label (
POST /v1/shipments) and return labels take anIdempotency-Keyheader. Retry with the same key and you get the first result, never a second label. - Other calls that charge money (pickups, top-ups, paying invoices, buying a draft) have no key. If a response
is lost, look before you retry:
GET /v1/pickups,GET /v1/wallet/transactions,GET /v1/invoices/{id}orGET /v1/shipments/draft/{id}show whether it went through. - Reads and quotes are always safe to retry.
- On 429 and 503, wait and retry with backoff: a few seconds, then longer.
Paging
Lists take limit and offset (for example GET /v1/shipments?limit=50&offset=100).
Store orders page with a cursor: pass the previous page's next_cursor.
Moved endpoints
When a path changes, the old one answers 404 with endpoint_moved and the new path, both in the body
and in an x-ph-moved-to header. An agent or client can follow it automatically. Changes are listed in the changelog.
{
"ok": false,
"error": {
"code": "endpoint_moved",
"message": "This endpoint moved... call GET /v1/shipments instead",
"moved_to": "/v1/shipments"
}
}