# Errors & idempotency ## The envelope Every `/api/v1` error is: ```json { "error": "", "details": { "message": "human explanation", … } } ``` Branch on `error` — it is stable API contract. `details.message` is for humans and may change; other `details` fields are documented per code below. ## Error catalog | Code | Status | Meaning | Recovery | |---|---|---|---| | `missing_token` | 401 | No `Authorization: Bearer` header | Send the token | | `invalid_token` | 401 | Token not recognized | Check the value; mint a new token if lost | | `revoked_token` | 401 | Token was revoked | Mint a new token | | `insufficient_scope` | 403 | Token scope too narrow (e.g. `read` on a mutation) | Use a `write` token | | `not_found` | 404 | Domain not in your workspace's portfolio, or unknown route | Check the domain and workspace | | `bad_request` | 400 | Body/params failed validation | Fix per `details` | | `already_in_portfolio` | 409 | The domain is already in your workspace | Nothing to order — manage it under `/domains/{name}` | | `price_changed` | 409 | `confirm_price` ≠ live price | Re-quote; retry with `details.current_price` if acceptable | | `renewal_in_progress` | 409 | That renewal term is already being bought — by the auto-renew run or another renew call (`details.state`) | Wait for it to complete; a later renew stacks a further term on top | | `insufficient_balance` | 402 | Wallet can't cover the price | Top up, or pass `payment_method` / `shared_payment_token`; `details.required` and `details.available` are in cents | | `payment_failed` | 402/422 | Pay-as-you-go charge declined | 402 = card declined (`details.kind`); fix the instrument and retry | | `email_taken` | 409 | Signup email already registered | Sign in / use the existing account | | `pending_at_registry` | 409 | `GET /domains/{name}` on a domain you just registered or transferred in that the registry doesn't report yet (`details.addedVia`) | Retry later — minutes for a registration, up to days for a transfer | | `no_hosted_zone` | 409 | Zone operation on a domain with external nameservers | Manage DNS at the delegated provider, or switch to `ns1/ns2.ingram.tech` | | `delete_failed` | 422 | Registry rejected the delete | See `details.message` (often TLD policy) | | `registration_failed` / `renew_failed` / `transfer_failed` | 422 | Registry rejected the operation | `details.message` carries the registry's reason — often user-actionable (e.g. contact field policy) | | `internal` | 500 | Unexpected registry/server error | Retry with the same `Idempotency-Key` | Registry mutations are **asynchronous**: a success response with registry status `Pending` means the registry accepted the operation and is processing it. The change (new expiry date, transferred status, …) becomes visible when the registry completes — usually seconds to minutes, up to days for transfers. ## Idempotency Every mutating route accepts an `Idempotency-Key` header (any unique string; a UUID is conventional): ```bash -H 'idempotency-key: 0e8f3b7c-renew-example-com-2027' ``` Semantics: - The first request with a key executes; if it **succeeds**, its response is stored for about 24 hours. - A later request with the same key and body returns the **stored response** (with `Idempotent-Replay: true`) without re-executing — so retrying after a timeout, once the original has completed, can't double-charge or double-register. The same key with a different body is `409 idempotency_key_reused`. - **Errors are not stored**, whatever their status: a retry after a 4xx or 5xx executes again. Failed operations don't commit side effects, so this is safe and a transient failure doesn't need a new key. - Two requests with the same key **in flight at the same time** are not serialized: both execute. A client timeout doesn't mean the original stopped, so before retrying a spending call after a timeout, check its effect (`GET /domains/{name}`) rather than resending blind. The CLI generates a fresh key per mutation automatically. For direct API use, derive the key from your own operation identity (order id, task id) so your retries collapse naturally.