<!-- Generated by scripts/build-ai-docs.ts from docs/api.md (error table) and scripts/ai-docs-lib.ts. Do not edit. -->

# VeloShip error codes

Every error response has the same shape. Branch on `error.code`, never on `message`.

```json
{
  "error": {
    "code": "PARAMETER.INVALID",
    "message": "Request validation failed",
    "details": [{ "field": "parcel.weight", "message": "Too small: expected number to be >0" }]
  }
}
```

`details` appears only when there is something to list. Create-and-buy failures also carry `shipment_id`. `FORBIDDEN.SCOPE` carries `required_scopes`. Every response has an `x-request-id` header; quote it when reporting a problem.

## Handling by class

| Class | Do |
| --- | --- |
| 429, 502, 503, 504, 500 | Transient. Retry with exponential backoff and the **same** `Idempotency-Key`, honoring `Retry-After`. Exception: `502 SHIPMENT.PURCHASE_UNKNOWN` (see below) |
| 401, 403 | Fix the key, its scopes or its mode. Retrying does nothing |
| 402 | Billing. Retrying does nothing until the account is fixed |
| 404 | Wrong ID, wrong mode (test and live data are separate) or wrong organization |
| 409 | The resource is in another state. Read it first |
| 422 | The request is wrong or a rule or carrier refused it. Fix it; never retry unchanged |

`502 SHIPMENT.PURCHASE_UNKNOWN` means the carrier may have created a label. Never create a second shipment for the package: reconcile first.

## Codes

| HTTP | Code | Meaning | Handling |
| --- | --- | --- | --- |
| 401 | `UNAUTHORIZED` | Missing, wrong or revoked API key Also: the key's `expires_at` has passed | Check the key: wrong value, revoked, expired, or a test key where a live one is needed. |
| 402 | `BILLING.PAST_DUE` | The subscription's last payment failed and the grace period is over | Not retryable until the payment is fixed; send the owner to `billing_url`. |
| 402 | `BILLING.REQUIRED` | A live label beyond the free monthly allowance with no active subscription; `billing_url`, `free_labels` and `labels_this_period` say where to fix it. Never for test mode or the platform organization | Not retryable until billing is set up; send the owner to `billing_url`. Test mode is never refused. |
| 403 | `ACCOUNT.EMAIL_UNVERIFIED` | A self-serve organization bought a live label before an owner verified their email | This key or role may not do this. Do not retry unchanged. |
| 403 | `BRANDING.LIVE_ONLY` | Branding or the logo was changed with a test key or in test mode; it is shared by both modes | This key or role may not do this. Do not retry unchanged. |
| 403 | `FORBIDDEN` | A key or member called a root-only route (`/bootstrap`, `/organizations`); or a key (or a member who isn't an owner or admin) called `POST /v1/rules/copy_to_live` | This key or role may not do this. Do not retry unchanged. |
| 403 | `FORBIDDEN.ORIGIN` | A cookie-authenticated write didn't come from this site | Cookie sessions only: send a same-origin `Origin` header. Integrations should use API keys. |
| 403 | `FORBIDDEN.SCOPE` | The key lacks the scope this route needs; `required_scopes` lists the options | Use a key holding one of `required_scopes` (see the error body). |
| 404 | `BATCH.NO_LABELS` | None of the batch's orders were bought | Check the ID. Test and live data are separate, and so are organizations. |
| 404 | `LABELS.MISSING` | A label file is missing from storage | Check the ID. Test and live data are separate, and so are organizations. |
| 404 | `NOT_FOUND` | Unknown route or resource | Check the ID. Test and live data are separate, and so are organizations. |
| 409 | `BATCH.IN_PROGRESS` | Batch labels requested before the batch finished; add `?partial=true` | The resource is in a different state. Read it and act on its `status`; do not retry unchanged. |
| 409 | `CARRIER_ACCOUNT.IMPLICIT` | Changing or deleting `ca_env_fedex` | The resource is in a different state. Read it and act on its `status`; do not retry unchanged. |
| 409 | `CARRIER_ACCOUNT.IN_USE` | Deleting an account that shipments or trackers were made with | The resource is in a different state. Read it and act on its `status`; do not retry unchanged. |
| 409 | `CLAIM.ALREADY_DECIDED` | Changing `status` on a claim that's already approved or denied | The resource is in a different state. Read it and act on its `status`; do not retry unchanged. |
| 409 | `CLAIM.ALREADY_OPEN` | The shipment already has an open claim | The resource is in a different state. Read it and act on its `status`; do not retry unchanged. |
| 409 | `CLAIM.NOT_SHIPPED` | Claim on a shipment that isn't `purchased` | The resource is in a different state. Read it and act on its `status`; do not retry unchanged. |
| 409 | `IDEMPOTENCY.IN_PROGRESS` | The same `Idempotency-Key` is still being processed | The first request is still running. Wait a moment, then retry with the same key. |
| 409 | `PRINT.DIAGNOSTICS_PENDING` | Downloading a diagnostics bundle the station hasn't sent yet | The resource is in a different state. Read it and act on its `status`; do not retry unchanged. |
| 409 | `PRINT.JOB_NOT_CANCELLABLE` | Cancelling a job the printer already has, or one that finished | The resource is in a different state. Read it and act on its `status`; do not retry unchanged. |
| 409 | `PRINT.PRINTER_EXISTS` | Adding a network printer at an address the station already has | The resource is in a different state. Read it and act on its `status`; do not retry unchanged. |
| 409 | `PRINT.PRINTER_NOT_REMOVABLE` | Removing a printer queue the station reports; only network printers can be removed | The resource is in a different state. Read it and act on its `status`; do not retry unchanged. |
| 409 | `PRINT.STATION_REVOKED` | Changing a revoked station | The resource is in a different state. Read it and act on its `status`; do not retry unchanged. |
| 409 | `PRINT.UPDATE_UNAVAILABLE` | Asking a station to update when it is up to date, nothing is published for its platform, or its agent is older than 0.2.0 | The resource is in a different state. Read it and act on its `status`; do not retry unchanged. |
| 409 | `RULES.CONFLICT` | The rules were edited at the same moment, or `reorder` didn't list every rule exactly once | Someone edited the rules at the same moment. Reload the list and try again. |
| 409 | `SHIPMENT.ALREADY_PURCHASED` | `buy` on a shipment that is purchased, refunded or mid-purchase | Do not buy again. Read the shipment: if it is `purchased`, use its label and tracking code. |
| 409 | `SHIPMENT.NOT_RECONCILABLE` | `reconcile` on a shipment that isn't `purchase_unknown` | The resource is in a different state. Read it and act on its `status`; do not retry unchanged. |
| 409 | `SHIPMENT.NOT_REFUNDABLE` | `refund` on a shipment that isn't `purchased`, or that the carrier has already scanned | The resource is in a different state. Read it and act on its `status`; do not retry unchanged. |
| 409 | `SHIPMENT.NOT_RERATABLE` | `rerate` on a shipment that isn't `created` | The resource is in a different state. Read it and act on its `status`; do not retry unchanged. |
| 409 | `SHIPMENT.NOT_RETURNABLE` | `return` on a shipment that isn't `purchased`, or is already a return | The resource is in a different state. Read it and act on its `status`; do not retry unchanged. |
| 409 | `SHIPMENT.PURCHASE_UNKNOWN` | `buy` while an earlier attempt is unresolved | Do not retry the buy and do not create a new shipment for the package. `POST /v1/shipments/{id}/reconcile` (or wait up to 15 minutes), then buy again if the status is `created`. |
| 409 | `SHIPMENT.RETURN.EXISTS` | `return` on a shipment that already has a return label | The resource is in a different state. Read it and act on its `status`; do not retry unchanged. |
| 410 | `PRINT.FILE_EXPIRED` | Reprinting a job whose file is past retention |  |
| 413 | `BRANDING.LOGO_TOO_LARGE` | A logo over 512 KB | Shrink the body (256 KB limit). |
| 413 | `PAYLOAD_TOO_LARGE` | Body over 256 KB (1 MB for FedEx push, 512 KB for a logo, a 20 MB file for print jobs) | Shrink the body (256 KB limit). |
| 422 | `ADDRESS.VERIFY.FAILURE` | `verify_strict` and FedEx couldn't resolve the address | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `BRANDING.LOGO_INVALID` | The logo isn't a PNG, JPEG, WebP or SVG, or the body is empty | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `BRANDING.LOGO_UNSAFE` | An SVG logo with scripts, event handlers, `foreignObject`, entities or references to other files | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `CARRIER.ERROR` | FedEx rejected the request (4xx); `details` holds FedEx's errors | 422: the carrier rejected the request; fix what `details` names. 502: the carrier failed or is rate limiting; retry with the same key. |
| 422 | `CARRIER.LABELS_NOT_ENABLED` | The carrier refused to create a label as unauthorized (FedEx `FORBIDDEN.ERROR`, a 401 or 403): label creation isn't enabled for the account yet. The shipment stays `created`; its rates from that account are flagged `purchasable: false` until a test or a purchase shows otherwise | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `CARRIER.NOT_CONFIGURED` | No enabled account matches the carrier or account asked for, or detected from a tracking number; or the rate's account was disabled | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `CARRIER.NOT_SUPPORTED` | Creating an account for a carrier VeloShip doesn't support yet | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `CARRIER.UNSUPPORTED_DESTINATION` | The carrier doesn't ship between these countries (domestic-only carrier, embargoed or suspended destination). Inside `carrier_errors` when another carrier can | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `CARRIER.UNSUPPORTED_OPTION` | The carrier can't do something you asked for (an option, packaging, label format, return label, flat rate, duties payer); `details` lists what | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `CLAIM.NOT_PROTECTED` | Claim on a shipment without protection | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `IDEMPOTENCY.INVALID` | `Idempotency-Key` longer than 255 characters | Use a key of at most 255 characters, such as a UUID. |
| 422 | `IDEMPOTENCY.MISMATCH` | `Idempotency-Key` reused with a different request | Bug in the caller: the key was reused with a different body. Use one key per operation. |
| 422 | `LABELS.NOT_MERGEABLE` | `POST /labels/merge` with a shipment that isn't purchased, has no PDF label or doesn't exist; `details` lists them | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `LIMIT.DAILY_SPEND` | The key's daily label or dollar limit is reached; nothing was bought. Resets 00:00 UTC | Nothing was bought. Wait for 00:00 UTC or raise the key's daily limit. |
| 422 | `PARAMETER.INVALID` | Body or query failed validation; see `details` | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `PARCEL.PRESET.NOT_FOUND` | `parcel` names a preset that doesn't exist | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `PRINT.AGENT_OUTDATED` | The station's agent can't print ZPL or reach network printers yet; update it | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `PRINT.FORMAT_NOT_SUPPORTED_BY_PRINTER` | ZPL for a printer whose `languages` lack `zpl`, or a PDF or image for one that lacks `pdf`; `printer_id`, `format` and `languages` say which | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `PRINT.INVALID_FILE` | The PDF or image can't be read, or a ZPL file has no `^XA` … `^XZ` label | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `PRINT.NO_LABEL` | `shipment_id` names a shipment without a label | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `PRINT.NO_PRINTER` | No printer has the role the job's `kind` or `role` needs at that location; `roles_available` lists the roles there | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `PRINT.PAPER_MISMATCH` | The printer doesn't offer the job's paper (Letter on a 4×6 printer, or the reverse) | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `PRINT.UNSUPPORTED_FORMAT` | The file isn't a PDF, PNG, JPEG or ZPL, doesn't match `content_type`, or `shipment_ids` mixes ZPL with other labels | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `PROTECTION.DISABLED` | `protection` requested while it's turned off in settings | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `PROTECTION.VALUE_TOO_HIGH` | The order value is over `max_value` | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `RULES.BLOCKED` | A shipping rule refused the order; `message` is the rule's own text, `details` names the rule and `applied_rules` lists what matched. See [rules](https://ship.vprev.dev/docs/md/rules.md#rulesblocked) | A shipping rule refused the order on purpose. Show `message` to the user; do not retry. |
| 422 | `RULES.COPY.EMPTY` | `copy_to_live` with no test rules while live has some; it would only delete them | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `RULES.COPY.WRONG_MODE` | `copy_to_live` called in live mode; it works from test mode | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `RULES.DRAFT_INVALID` | The model's draft didn't validate, even after a retry; reword it or build the rule by hand | Reword the description, or build the rule by hand. |
| 422 | `RULES.LIMIT` | The organization already has 50 rules in this mode | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `RULES.NO_RATE` | Shipping rules removed every rate; `details` lists each rule and what it removed. Send `apply_rules: false` to skip them | Rules removed every rate. Read `details`, loosen the rule, or send `apply_rules: false` for a deliberate exception. |
| 422 | `RULES.SERVICE_EXCLUDED` | A requested service was removed by a shipping rule (limits beat requests); `details` names the rule | A rule removed the service you asked for. Choose another service. The error carries `shipment_id`. |
| 422 | `SCENARIO.UNKNOWN` | Test mode: `VeloShip-Scenario` names no scenario; see [Testing](https://ship.vprev.dev/docs/md/testing.md) | Test mode only: the `VeloShip-Scenario` name is wrong; the error lists the valid names. |
| 422 | `SHIPMENT.ADDRESS.INCOMPLETE` | Missing phone, or name/company, at purchase time | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `SHIPMENT.CUSTOMS.EEI_REQUIRED` | Goods over $2,500 under one Schedule B number, outside Canada, without an AES ITN in `eel_pfc`; `details` names the lines | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `SHIPMENT.CUSTOMS.INVALID` | The customs items weigh more than the parcel; `details` has both weights | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `SHIPMENT.CUSTOMS.REQUIRED` | The parcel crosses a customs border and `customs_info` is missing. See international shipping | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `SHIPMENT.FROM_ADDRESS.REQUIRED` | No `from_address` and no `default_from_address` in settings | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `SHIPMENT.ONE_RATE.INELIGIBLE` | One Rate rules not met; see `details` | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `SHIPMENT.RATE.NOT_FOUND` | `rate_id` / `service` isn't on this shipment | Fix the request (see `details`); retrying unchanged fails the same way. |
| 422 | `SHIPMENT.RATES.EXPIRED` | Rates are older than `RATE_TTL_HOURS`; call `/rerate` | `POST /v1/shipments/{id}/rerate`, then buy again. |
| 422 | `SHIPMENT.REFUND.REJECTED` | The carrier refused to void the label | Fix the request (see `details`); retrying unchanged fails the same way. |
| 429 | `CARRIER.RATE_LIMITED` | The carrier refused with a 429 after retries; honor `Retry-After` | Wait `Retry-After` seconds, then retry with the same `Idempotency-Key`. |
| 429 | `RATE_LIMITED` | Too many requests for this API key; honor `Retry-After` | Wait `Retry-After` seconds, then retry with the same `Idempotency-Key`. |
| 500 | `INTERNAL_ERROR` | Unexpected | Retry once with the same `Idempotency-Key`; if it persists, report the `x-request-id` response header. |
| 502 | `CARRIER.ALL_FAILED` | No carrier could quote; `details` has each carrier's error | No carrier could quote. Read `details` for each carrier's error, then retry later. |
| 502 | `CARRIER.ERROR` | FedEx was unavailable, rate-limited us, or failed authentication | 422: the carrier rejected the request; fix what `details` names. 502: the carrier failed or is rate limiting; retry with the same key. |
| 502 | `RULES.AI_FAILED` | The model call failed, timed out or was busy; try again | Transient. Try again, or reword the text. |
| 502 | `SHIPMENT.LABEL_STORAGE_FAILED` | The label couldn't be stored, so it was voided; buy again | Safe to buy again: the label was voided and the shipment is back to `created`. |
| 502 | `SHIPMENT.PURCHASE_UNKNOWN` | FedEx didn't confirm a label; the shipment is being reconciled | Do not retry the buy and do not create a new shipment for the package. `POST /v1/shipments/{id}/reconcile` (or wait up to 15 minutes), then buy again if the status is `created`. |
| 503 | `CARRIER.UNAVAILABLE` | FedEx has failed repeatedly, so calls are paused briefly and the request was **not** sent. Retry after `Retry-After` seconds. A `buy` that gets this leaves the shipment `created` | The carrier's circuit breaker is open and nothing was sent. Retry after `Retry-After` seconds. A `buy` that gets this leaves the shipment `created`. |
| 503 | `RULES.AI_UNAVAILABLE` | "Describe it in English" needs an `ANTHROPIC_API_KEY` on the server | The server has no model key. Build the rule by hand instead. |
| 504 | `CARRIER.TIMEOUT` | Only inside `carrier_errors`: a carrier missed the quote time budget | Appears only inside `carrier_errors`: the quote succeeded without that carrier. Re-quote if you need its rates. |
