# Error codes

> The HTTP status and error codes the API returns for zones, records, delegation checks, batches, merges and the email check.

Errors come as JSON with a stable `code`, a human-readable `message` and, for validation errors, the `field` at fault:

```json
{"error": {"code": "invalid", "message": "must be between 60 and 86400 seconds", "field": "ttl"}}
```

Program against `code`, not `message`. The CLI exits with `1` on errors, `2` on usage errors and `3` on a version
conflict.

## Request and permission errors

| Status | Code | When |
|---|---|---|
| 400 | `bad_request` | Malformed JSON, unknown fields, bad `If-Match` or `dry_run` values |
| 401 | `unauthorized` | Missing, invalid, expired or revoked token |
| 403 | `forbidden` | The role or scope does not allow the action (for example a `readonly` token writing records, or a domain-scoped token adding zones) |
| 403 | `token_scope` | A purge-only token called anything other than the cache purge |
| 404 | `not_found` | The zone, record or resource does not exist or is outside your scope |
| 413 | `too_large` | A BIND zone file over 1 MiB |

## Validation

| Status | Code | When |
|---|---|---|
| 422 | `invalid` | A field is not valid: see `field` and `message`. Examples: an unsupported record type, a TTL outside 60–86,400, a priority on an `A` record, a proxied wildcard, a [coexistence rule](https://developers.openwaf.dev/dns/record-types/#coexistence), a public suffix as zone name, more than 500 operations in a batch (`field` names the operation, such as `create[2].content`) |
| 422 | `reason_required` | A configuration change without `X-Change-Reason` (1–500 characters) |

## Zones

| Status | Code | When |
|---|---|---|
| 403 | `zone_limit` | The organization has reached its domain limit. Delete unused domains or ask support to raise it |
| 409 | `zone_exists` | The domain is already added in OpenWAF |
| 409 | `zone_overlaps` | The domain is a parent or a subdomain of a domain managed by another organization or client |
| 409 | `zone_not_external` | An external-only operation (such as `GET …/hosts`) on a delegated zone |
| 409 | `conflict` | The zone is disabled, the operation does not apply to an external zone (for example importing records), or a name is already taken |
| 409 | `zone_merged` | The zone was merged into another and is read-only |
| 409 | `merge_not_equivalent` | A merge would change what some name serves, or cannot be expressed; see `plan.problems` |
| 409 | `merge_not_reversible` | The parent zone changed after the merge |
| 409 | `missing_certs` | The change would leave a site without a valid certificate; `hostnames` lists them |
| 412 | `version_conflict` | `If-Match` (or `expected_version`) does not match the zone's current version; `current_version` has it |
| 428 | `if_match_required` | A revert without `If-Match` |

## Upstream and availability

| Status | Code | When |
|---|---|---|
| 502 | `dns_backend` | The DNS servers rejected the change; nothing was applied. Retry, and contact support if it persists |
| 502 | `dns_out_of_sync` | The change was rolled back because it could not be published; contact support |
| 502 | `dns_unavailable` | The email check could not get answers from public DNS |
| 503 | `busy` | Too many email checks running; retry in a few seconds |
| 503 | `unavailable` | The feature is temporarily unavailable |

## Clients and white label

| Status | Code | When |
|---|---|---|
| 409 | `client_has_zones` | Deleting a client that still has domains |
| 409 | `host_taken` | The dashboard domain or nameserver name belongs to another organization |
| 409 | `conflict` | Removing or demoting the last owner, or a client name already in use |
