Understand and handle API errors
The CaptainDNS public API returns every error in a standardized JSON envelope. This page lists the canonical codes, what they mean and the recommended corrective action.
Standard envelope
Every error response follows this format:
{
"code": "QUOTA_EXCEEDED",
"message": "Monthly credits quota exceeded for plan 'starter'.",
"details": {
"tier": "starter",
"credits_used": 50000,
"credits_limit": 50000
},
"request_id": "req_a1b2c3d4",
"documentation_url": "https://www.captaindns.com/en/docs/api/errors"
}
code: stable string documented here. Use it to branch your error handling logic.message: human-readable description in English. Do not parse it; it may evolve.details: optional object with context specific to the code. Its schema varies by code. Most codes do not exposedetails: the information is then carried bymessage.request_id: CaptainDNS request identifier, useful when opening a support ticket. Present only if theX-Request-Idheader was propagated.documentation_url: link to this page.
The HTTP status always accompanies the code. A robust client branches on the (status, code) pair and uses code as the primary key.
Authentication codes (401)
INVALID_API_KEY
Status: 401.
Cause: missing Authorization header, missing prefix, malformed key, tampered secret, or key not found on the CaptainDNS side. An HMAC mismatch and an unknown key return the same code so the attacker cannot distinguish between the two.
Action: verify the header is Authorization: Bearer cdns_live_.... A stray whitespace, a newline, or a truncated key are the most common culprits.
{
"code": "INVALID_API_KEY",
"message": "Invalid API key.",
"request_id": "req_a1b2c3d4",
"documentation_url": "https://www.captaindns.com/en/docs/api/errors"
}
No details field; the information is only in message.
EXPIRED_API_KEY
Status: 401.
Cause: the key has passed its expires_at date.
{
"code": "EXPIRED_API_KEY",
"message": "This API key has expired.",
"request_id": "req_a1b2c3d4",
"documentation_url": "https://www.captaindns.com/en/docs/api/errors"
}
No details field.
Action: create a new key with a later expiration or no expiration. Keys without expires_at are valid until revoked.
REVOKED_API_KEY
Status: 401.
Cause: the key was revoked manually or automatically (rotation grace period end).
{
"code": "REVOKED_API_KEY",
"message": "This API key has been revoked.",
"request_id": "req_a1b2c3d4",
"documentation_url": "https://www.captaindns.com/en/docs/api/errors"
}
No details field.
Action: use the active key. If you do not have one, create one from the dashboard.
Authorization codes (403)
INSUFFICIENT_SCOPE
Status: 403.
Cause: the key lacks the scope required by the endpoint. The missing scope is concatenated into message.
{
"code": "INSUFFICIENT_SCOPE",
"message": "This API key does not have the required scope: web:read",
"request_id": "req_a1b2c3d4",
"documentation_url": "https://www.captaindns.com/en/docs/api/errors"
}
No details field; the required scope is read from message.
Action: create a new key with the missing scope or use a key that carries it. Scopes are not editable after creation.
IP_NOT_ALLOWED
Status: 403.
Cause: the key has an IP allowlist and the request's source IP is not in it. The client IP is derived from r.RemoteAddr on the backend (never from a client-supplied X-Forwarded-For), after potential rewriting by the TrustedProxyRealIP layer when the immediate hop is a trusted proxy configured on the CaptainDNS side.
{
"code": "IP_NOT_ALLOWED",
"message": "Your IP is not in this key's allowlist.",
"request_id": "req_a1b2c3d4",
"documentation_url": "https://www.captaindns.com/en/docs/api/errors"
}
No details field; the detected IP and the CIDR list are not returned to the client (they are logged server-side).
Action: add your IP to the key allowlist, or route your traffic through an authorized egress (proxy, NAT).
QUOTA_EXCEEDED
Status: 403.
Cause: the monthly credits quota is reached and the plan does not allow overage (Free plan, hard cap).
{
"code": "QUOTA_EXCEEDED",
"message": "Monthly credits quota exceeded for plan 'free'.",
"details": {
"tier": "free",
"credits_used": 500,
"credits_limit": 500
},
"request_id": "req_a1b2c3d4",
"documentation_url": "https://www.captaindns.com/en/docs/api/errors"
}
Action: wait until the next period or upgrade to a higher plan. The CaptainDNS dashboard lets you switch plans in one click.
OVERAGE_BUDGET_EXCEEDED
Status: 403.
Cause: the profile has enabled overage but has reached the monthly budget cap configured from the dashboard. Subsequent calls are refused until the period closes or the cap is raised.
{
"code": "OVERAGE_BUDGET_EXCEEDED",
"message": "Overage budget cap reached for plan 'starter'.",
"details": {
"tier": "starter",
"credits_used": 68000,
"credits_limit": 50000
},
"request_id": "req_a1b2c3d4",
"documentation_url": "https://www.captaindns.com/en/docs/api/errors"
}
Action: raise the cap from Account > API usage, upgrade to a plan with a larger envelope, or wait for the next monthly period.
Request codes (400)
INVALID_REQUEST
Status: 400.
Cause: a component of the request cannot be processed. The public API emits two cases:
- Malformed
Idempotency-Keyheader (non-ASCII characters, whitespace, out-of-range length, empty value after trim). - Unreadable request body or body exceeding the idempotency size limit (11 MiB).
{
"code": "INVALID_REQUEST",
"message": "Invalid Idempotency-Key header.",
"request_id": "req_a1b2c3d4",
"documentation_url": "https://www.captaindns.com/en/docs/api/errors"
}
No details field; the precise reason is in message.
Action: fix the header or the body, then retry. For endpoint-specific validation errors (invalid domain, unknown selector, etc.), see the Validation codes (400) section.
Conflict codes (409)
IDEMPOTENCY_CONFLICT
Status: 409.
Cause: an Idempotency-Key is reused with a body different from the original request. The comparison is based on the SHA-256 hash of the raw body.
{
"code": "IDEMPOTENCY_CONFLICT",
"message": "Idempotency-Key reused with a different request body.",
"request_id": "req_a1b2c3d4",
"documentation_url": "https://www.captaindns.com/en/docs/api/errors"
}
No details field; the conflicting key is the one you just sent.
Action: generate a new key for the new request, or fix the client that mutates the body between retries.
Rate limit codes (429)
RATE_LIMITED
Status: 429.
Cause: the per-key token bucket is empty or the per-IP rate limit is exceeded. Two message variants depending on which layer was hit:
- Pre-auth IP bucket:
"Too many requests from this IP. Slow down.". - Per-key bucket:
"Rate limit exceeded for this API key.".
{
"code": "RATE_LIMITED",
"message": "Rate limit exceeded for this API key.",
"request_id": "req_a1b2c3d4",
"documentation_url": "https://www.captaindns.com/en/docs/api/errors"
}
No details field. The Retry-After header always accompanies this code, expressed in seconds. Wait at least this duration before retrying. See the rate limiting guide for backoff strategies.
Server codes (5xx)
INTERNAL_ERROR
Status: 500 or 503.
Cause: unexpected error on the CaptainDNS side. Two variants:
- 500: transient application error (handler panic, DB dependency unavailable, tier lookup error, etc.).
messagespecifies the subsystem. - 503: the public API is not configured or the
/public/v1/*group is explicitly disabled on the platform side (missing pepper,PUBLICAPI_ENABLED=falseflag). There is no separateSERVICE_UNAVAILABLEcode: unavailability shares theINTERNAL_ERRORcode with a 503 status.
{
"code": "INTERNAL_ERROR",
"message": "public api is not configured",
"request_id": "req_a1b2c3d4"
}
No details field.
Action: keep the request_id and open a support ticket. 503s are usually tied to planned maintenance or an incident: check the official status channel before retrying. Retry after a few minutes for transient 500s; if the rate exceeds 1 % over a five-minute window, alert the CaptainDNS team.
Validation codes (400)
The API also returns 400 BAD_REQUEST for validation errors specific to each endpoint. These errors are not covered by the standardized envelope above because their structure varies per endpoint.
Example for a DNS resolve with an invalid domain:
{
"error": "invalid domain: must be a valid FQDN",
"field": "domain"
}
For endpoints that return validation errors, see the OpenAPI reference which documents the exact schemas.
Synthetic table
| Status | Code | Description | Action |
|---|---|---|---|
| 400 | Varies | Endpoint validation error | Fix the request body |
| 400 | INVALID_REQUEST | Unreadable Idempotency-Key or body | Fix the header or the body |
| 401 | INVALID_API_KEY | Missing, malformed or unknown key | Check the Authorization header |
| 401 | EXPIRED_API_KEY | Expired key | Create a new key |
| 401 | REVOKED_API_KEY | Revoked key | Use an active key |
| 403 | INSUFFICIENT_SCOPE | Missing scope on key | Create a key with the right scope |
| 403 | IP_NOT_ALLOWED | IP not in allowlist | Add IP or use an authorized egress |
| 403 | QUOTA_EXCEEDED | Monthly quota reached (hard cap) | Wait or upgrade plan |
| 403 | OVERAGE_BUDGET_EXCEEDED | Overage cap reached | Raise the cap or upgrade |
| 409 | IDEMPOTENCY_CONFLICT | Same key with different body | Generate a new key |
| 429 | RATE_LIMITED | Per-IP or per-key rate limit exceeded | Wait Retry-After |
| 500 | INTERNAL_ERROR | Transient server error | Retry and open a ticket if persistent |
| 503 | INTERNAL_ERROR | Public API not configured | Check status, retry |
Error handling best practices
- Branch on the (status, code) pair. Status alone is not enough: two 403s can come from
INSUFFICIENT_SCOPEorIP_NOT_ALLOWED, each deserving a different action. - Log the
request_id. It is the unique identifier on the CaptainDNS side and is required for any investigation with support. - Do not retry blindly. 4xx codes other than 429 indicate a client-side problem: retrying without change will not succeed.
- Respect
Retry-After. 429 and 503 responses always include it. Do not bypass. - Alert on error rates. 1 % of 5xx over a 5-minute window is an incident signal. Surface it in your observability stack.
Jump to the OpenAPI reference to explore schemas endpoint by endpoint, or go back to the quickstart to restart from the beginning.