# Errors & Idempotency

Every v5 error response uses the same envelope:

```json
{
  "error": {
    "code": "validation_failed",
    "message": "Human-readable summary.",
    "details": {
      "field": ["Per-field error message"]
    }
  }
}
```

The HTTP status gives the broad category. The `error.code` tells you what
specifically happened. Branch on the code, not the message text.

## Common recovery paths

| Code | What to do |
|  --- | --- |
| `validation_failed` | Fix `error.details` before retrying. |
| `unauthenticated`, `token_expired`, `token_revoked` | Issue or select a valid token. |
| `scope_missing` | Re-issue a token with the missing scope. |
| `insufficient_credits` | Top up credits before purchasing labels. |
| `too_many_requests` | Retry with exponential backoff. |
| `conflict`, `*_in_use`, `not_voidable`, `not_cancellable` | Inspect the resource state and retry only after it changes. |
| `internal_error` | Retry; contact support with the request ID if persistent. |


The [API Reference](/openapi) documents the full error vocabulary.

## Idempotency-Key

Every v5 endpoint that creates or charges accepts an optional
`Idempotency-Key` header. Generate a unique value per logical operation and
reuse the same key only when retrying the exact same request body.

```bash
KEY=$(uuidgen)
curl -X POST "$STALLION_BASE_URL/labels" \
  -H "Authorization: Bearer $STALLION_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d '{ ... }'
```

Behavior:

- Same key and same body within 24 hours returns the original response with
`Idempotent-Replay: true`.
- Same key with a different body returns `409 idempotency_conflict`.
- `5xx` responses are not cached, so transient infrastructure failures can be
retried with the same key.


Use this header for shipment creation, label purchase, pickups, LTL bookings,
credit top-ups, product classification, and manufacturer verification.