# Errors, idempotency and recovery

Most errors use `{error:{code,message,details?}}`. Device-token polling
uses RFC 8628 string errors; webhook ingress failures return
`invalid_signature`.

| Code / status | Action |
|---|---|
| `unauthorized` 401 | Supply/rotate a valid runtime credential; complete human pairing |
| `forbidden` 403 | Obtain the missing human/key/grant authority; do not impersonate the owner |
| `policy_violation` 403 | Inspect reasons and the pinned policy; ask the human to change authority |
| `approval_required` 403 | Show `details.approval_request` to the human; retry with its approval once granted |
| `destination_not_supported` 403 | US service-area restriction; cannot be relaxed by policy |
| `limit_reached` 403 | The organization's number-holding Line limit is reached, including pending claims |
| `feature_disabled` 403 | This deployment has `details.feature` switched off; nothing was reserved or sent |
| `spend_limit_exceeded` 402 | Inspect `scope: org/grant/line` and the limit/reserved/committed micros; reduce, fund, or await reset |
| `insufficient_funds` 402 | The organization's wallet does not cover the action (`details.available_micros`, `estimated_micros`); nothing was reserved or billed. Top up or wait for automatic recharge |
| `payment_required` 402 | Billing is suspended or canceled (`details.status`); nothing paid runs until a payment succeeds |
| `a2p_registration_pending` 409 | Finish campaign/number assignment; nothing was sent |
| `conflict` 409, `details.reason: inbound_rate_unavailable` | No verified inbound rate covers the number(s); choose another area code. Nothing was bought |
| `conflict` 409, `details.reason: idempotency_in_progress` | Wait, then retry the identical body/key; another request owns the operation |
| `conflict` 409, `details.reason: idempotency_key_in_use` | The key belongs to a different caller in this org; use a new key |
| `conflict` 409 | Retrieve the current resource; state/document/one-time report constraints may apply |
| `validation_error` 422 | Correct schema/units/E.164/document fields |
| `provider_error` 502 | Inspect reason/reference; unknown routes need verified quotes; carrier outcomes may be ambiguous |
| Rate limiting 429 | Back off; honor `Retry-After` where present |

## One key, one logical operation

Send `Idempotency-Key` on every POST. SDK retries reuse the same key and
body. Reusing a key with different data is invalid; adding an
`approval` changes the body, so use a new key for the approved retry.

A key belongs to the caller that first used it — signed-in user, project
key, or agent, within its organization, with the authority it then held.
Any other caller (or the same caller after a role change or grant
replacement) gets 409 `idempotency_key_in_use`: look the resource up
instead. A replay is never an authorization.

## Ambiguous outcomes are honest

An acknowledged carrier action may return an error **after it acted**.
ORDan retains the reference and the financial hold. Retrieve the
resource; do not create a new number, call, or registration with a fresh
key to "fix" uncertainty. A timeout is not a definitive refusal — and it
does not prove zero provider cost. Pending states (`provisioning`,
`releasing`, `billing_status: pending`, port `needs_attention`) resolve
through signed webhooks, fair worker polling, and authoritative carrier
evidence.
