Guides
Errors & idempotency
The envelope
Every /api/v1 error is:
{ "error": "<machine_code>", "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):
-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 is409 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.