Errors
Every error carries an HTTP status, a JSON body with a human-readable detail and, for most of them, a stable code in the X-Error-Code header. Branch on the code; show the detail.
Error shape
The body is always {"detail": …}. The machine-readable part travels in a response header, so the body stayed the same for every client when the codes arrived:
HTTP/2 409
content-type: application/json
x-error-code: zone_exists_in_org
{"detail":"Domain already added to this organization"}Branch on the code, not the message
detail is written for people and follows the Accept-Language header of the request: English by default, Portuguese with pt. Its wording may change at any time; X-Error-Code does not.Errors without a code
When the body fails schema validation the status is 422, there is no X-Error-Code, and detail is an array pinpointing each offending field:
{
"detail": [
{
"type": "greater_than_equal",
"loc": ["body", "ttl"],
"msg": "Input should be greater than or equal to 60",
"input": 30,
"ctx": { "ge": 60 }
}
]
}A few domain rules also answer 422 without a code, with a single message in detail: record content that does not match its type, a CNAME that would share its name with other records, and the zone and record limits of your plan. 502 from the DNS backend has no code either. Branch on the status and the endpoint there.
Rate limits
Every zone, record and Monitor route counts toward an hourly ceiling per organization, set by your plan. Over it the API answers 429 with X-Error-Code: rate_limited and a Retry-After header in seconds. Three expensive routes also have a per-minute ceiling of their own: import (10), ownership verification (12) and delegation checks (12).
HTTP/2 429
retry-after: 1873
x-error-code: rate_limited
{"detail":"Too many attempts. Wait and try again."}Code reference
Authentication and access
| code | Status | When |
|---|---|---|
| not_authenticated | 401 | No Authorization header. |
| api_key_invalid | 401 | The token starts with qfx_ but is malformed. |
| api_key_revoked | 401 | Unknown or revoked API key. |
| invalid_token | 401 | Invalid or expired session token. Also what an API key gets on session-only routes (DNSSEC, key management). |
| api_key_scope_missing | 403 | The key lacks the scope for this route; detail names the scopes that would work. |
| role_admin_required | 403 | A session user without the owner or admin role tried a write. |
| org_suspended | 403 | A write in an organization suspended by the platform. Reads keep working. |
| org_not_found | 404 | The organization does not exist, or the key belongs to another one. |
| rate_limited | 429 | Plan or per-route ceiling reached. Wait Retry-After seconds. |
Zones and records
| code | Status | When |
|---|---|---|
| zone_not_found | 404 | The zone does not exist in your organization, or is outside the key's allowed_zone_ids. |
| zone_exists_in_org | 409 | The domain was already added to this organization. |
| zone_active_elsewhere | 409 | The domain is active in another organization. |
| verify_email_first_zones | 403 | A session user with an unverified e-mail tried to create a zone. |
| zone_import_input_required | 422 | Import without bind_file or "quick_scan": true. |
| zone_file_too_large | 422 | BIND file over 1 MB. |
| zone_file_invalid | 422 | BIND file that does not parse. |
| record_not_found | 404 | The record does not exist in this zone. |
| — | 422 | Record content, CNAME coexistence or plan limit. No code: read detail. |
| — | 502 | The DNS backend rejected the operation. Nothing was persisted: safe to retry. |
DNSSEC
| code | Status | When |
|---|---|---|
| dnssec_zone_inactive | 409 | The zone is not active yet: delegate it first. |
| dnssec_already_enabled | 409 | DNSSEC is already enabled on this zone. |
| dnssec_not_enabled | 409 | Disabling or removing a key on a zone that is not signed. |
| dnssec_enable_first | 409 | Rotating before enabling. |
| dnssec_remove_ds_first | 422 | Disabling without {"confirm": true}. |
| dnssec_last_key | 422 | Removing the only key: disable DNSSEC instead. |
| dnssec_key_not_found | 404 | Unknown key_id. |
Monitor
| code | Status | When |
|---|---|---|
| watched_domain_not_found | 404 | The watched domain does not exist in your organization. |
| watched_domain_exists | 409 | Your organization already watches this domain. |
| watched_domain_invalid | 422 | Not a valid domain name. |
| monitor_quota_reached | 422 | Watched-domain quota reached. Nothing in the batch was added. |
| watched_domain_txt_missing | 409 | The verification TXT is not visible yet. |
| monitor_check_invalid | 422 | Unknown check_kind. |
| monitor_check_invalid | 409 | Accepting a check that was never measured. |
| certificate_domain_unverified | 409 | Adding hosts to a domain that has not proven ownership. |
| certificate_host_not_found | 404 | The host is not in this domain's inventory. |
| certificate_host_exists | 409 | The host is already in the inventory. |
| certificate_host_invalid | 422 | Not a name strictly below the watched domain (wildcards and the apex are rejected). |
| certificate_host_not_public | 422 | The host does not resolve to a public IP. |
| certificate_quota_reached | 422 | Certificate-host quota for this domain reached. |
| service_profile_unknown | 422 | Profile outside the catalog (web, webmail, email). |
| service_check_exists | 409 | The profile is already on for this host. |
| service_check_not_found | 404 | The profile is not on for this host. |
| service_quota_reached | 422 | Service-check quota for this domain reached. |
Retrying safely
502 means the DNS backend refused the operation before anything was persisted: retry with backoff. 429 tells you how long to wait in Retry-After. Other 4xx errors are yours to fix: retrying the same request fails the same way.