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

HTTPcodeWhat to do
400validation_errorSomething in the request is wrong. The message says what; fix it before retrying.
401unauthorizedThe token is missing or expired. Get a new one from /v1/api-clients/token and retry.
403api_key_not_allowedAPI 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.
403forbiddenYour role on the account doesn't allow it, or the resource isn't yours.
403account_restrictedThe account is on hold or under review. Contact support.
404not_foundIt doesn't exist on this account (or belongs to another account).
404endpoint_movedAn old path. error.moved_to has the new one.
409conflictThe account's state doesn't allow it. The message says why, e.g. insufficient_funds (top up first) or an already-cancelled label.
413payload_too_largeThe request body is too big.
429rate_limitedToo many requests. Wait the Retry-After header (or error.retry_after_seconds) seconds, then retry.
500internal_errorSomething failed on our side. Retry later; quote the request_id if it keeps happening.
503service_unavailableA 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 an Idempotency-Key header. 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} or GET /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"
  }
}