# Zones

> Add, list, check and delete zones (domains), in delegated or external DNS mode, and merge a zone into its parent.

A zone is a domain added to OpenWAF. In `delegated` mode OpenWAF serves its DNS once the registrar delegates it to the zone's name servers; in `external` mode the DNS stays where it is and each proxied name points at the edge with a CNAME to `edge.openwaf.dev`. A zone stays `pending` until its delegation, or the ownership TXT record of an external zone, is verified.

| Operation | Endpoint |
|---|---|
| [List zones](https://developers.openwaf.dev/api/zones/#list-zones) | `GET /v1/orgs/{org}/zones` |
| [Create zone](https://developers.openwaf.dev/api/zones/#create-zone) | `POST /v1/orgs/{org}/zones` |
| [Create zones in bulk](https://developers.openwaf.dev/api/zones/#bulk-create-zones) | `POST /v1/orgs/{org}/zones/bulk` |
| [Get zone](https://developers.openwaf.dev/api/zones/#get-zone) | `GET /v1/orgs/{org}/zones/{zone}` |
| [Delete zone](https://developers.openwaf.dev/api/zones/#delete-zone) | `DELETE /v1/orgs/{org}/zones/{zone}` |
| [Get delegation status](https://developers.openwaf.dev/api/zones/#get-delegation) | `GET /v1/orgs/{org}/zones/{zone}/delegation` |
| [Check delegation now](https://developers.openwaf.dev/api/zones/#check-delegation) | `POST /v1/orgs/{org}/zones/{zone}/delegation/check` |
| [Check external zone names](https://developers.openwaf.dev/api/zones/#list-zone-hosts) | `GET /v1/orgs/{org}/zones/{zone}/hosts` |
| [Merge zone into its parent](https://developers.openwaf.dev/api/zones/#merge-zone) | `POST /v1/orgs/{org}/zones/{zone}/merge` |
| [Unmerge zone](https://developers.openwaf.dev/api/zones/#unmerge-zone) | `POST /v1/orgs/{org}/zones/{zone}/unmerge` |

## List zones

```http
GET /v1/orgs/{org}/zones
```

Lists the zones of the organization. Zones merged into another one are hidden unless `include=merged`.

**Required role:** `readonly` or higher. A token limited to a zone only sees that zone; one limited to a client only sees its client's zones.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `org` | path | string (uuid) | Yes | Organization id. Every route checks that it is the token's organization (otherwise `404`). |
| `include` | query | string | No | `merged` also lists the zones merged into another one (hidden by default). One of `merged`. |

#### Response

- `200`: Returns `items`, an array of [Zone](https://developers.openwaf.dev/api/schemas/#zone).

| Field | Type | Description |
|---|---|---|
| `items[].id` | string (uuid) | Id. |
| `items[].org_id` | string (uuid) | Organization id. |
| `items[].name` | string | Name. |
| `items[].status` | string | One of `pending`, `active`, `moved`, `disabled`. |
| `items[].dns_mode` | string | `delegated`: OpenWAF name servers. `external`: the customer keeps their DNS and points their sites at the edge (CNAME/A). One of `delegated`, `external`. Default `delegated`. |
| `items[].ns_pair` | array of string | Name servers the delegated zones use. Exactly 2 items. |
| `items[].delegation_checked_at` | string (date-time) | Last delegation check. |
| `items[].delegation` | [CheckResult](https://developers.openwaf.dev/api/schemas/#check-result) or [VerifyResult](https://developers.openwaf.dev/api/schemas/#verify-result) | Last check: CheckResult (delegated) or VerifyResult (external). |
| `items[].activated_at` | string (date-time) | When the zone became active. |
| `items[].moved_at` | string (date-time) | When the delegation stopped pointing to OpenWAF. |
| `items[].disabled_at` | string (date-time) | When the zone was disabled. |
| `items[].verification` | [ZoneVerification](https://developers.openwaf.dev/api/schemas/#zone-verification) |  |
| `items[].cname_target` | string | External zones only: CNAME target for each site. |
| `items[].edge_addresses` | [EdgeAddresses](https://developers.openwaf.dev/api/schemas/#edge-addresses) |  |
| `items[].config_version` | integer | Current configuration version (also in ETag). |
| `items[].proxied_names` | integer | Names with a proxied record. |
| `items[].merged_into` | string (uuid) | Merged zones only (status disabled): the parent zone. |
| `items[].merged_at` | string (date-time) | Merged zones only; they are purged after 14 days. |
| `items[].created_at` | string (date-time) | Creation time. |
| `items[].updated_at` | string (date-time) | Last change. |
| `items[].client_id` | string (uuid) or null | Client of the organization the zone belongs to; null = none. |

#### Example

**curl**


```sh
curl "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones" \
  -H "Authorization: Bearer $OPENWAF_TOKEN"
```

**TypeScript**


```ts
import { OpenWAF } from "./openwaf";

const owf = new OpenWAF({ baseUrl: "https://dash.openwaf.dev" }); // token from OPENWAF_TOKEN
const result = await owf.listZones();
```

**Python**


```py
from openwaf import OpenWAF

owf = OpenWAF(base_url="https://dash.openwaf.dev")  # token from OPENWAF_TOKEN
result = owf.list_zones()
```



```json
{
  "items": [
    {
      "id": "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
      "org_id": "1e7c9a3b-5d2f-4b8a-9c6e-0f2a4b6c8d1e",
      "name": "example.com",
      "status": "active",
      "dns_mode": "delegated",
      "ns_pair": [
        "ns1.example-dns.net",
        "ns2.example-dns.net"
      ],
      "delegation_checked_at": "2026-10-27T09:00:00Z",
      "activated_at": "2026-03-02T11:40:00Z",
      "config_version": 12,
      "proxied_names": 3,
      "client_id": null,
      "created_at": "2026-03-02T10:20:00Z",
      "updated_at": "2026-10-27T09:00:00Z"
    }
  ]
}
```

## Create zone

```http
POST /v1/orgs/{org}/zones
```

Adds a zone and creates version 1 of its configuration (the change reason defaults to the creation of the zone).

- `delegated` (default): OpenWAF serves the zone's DNS. The zone stays `pending` until the registrar delegates it to the zone's name servers (`ns_pair`). By default the records found on the public DNS are imported right away (`import`).
- `external`: the DNS stays with its current provider. Point each proxied name at the edge with a CNAME to `cname_target` (or A/AAAA records to `edge_addresses` at the apex) and publish the TXT record in `verification` to prove ownership; once verified, the zone becomes `active`. External zones only accept proxied A/AAAA/CNAME records.

A domain that is a parent or a subdomain of a zone of another organization (or, for a client-limited credential, of another client) is refused. A zone that stays `pending` too long is deleted automatically.

**Required role:** `tech` or higher. Not available to tokens limited to a zone.

**Change controls:** [`dry_run`](https://developers.openwaf.dev/api/#configuration-changes) · [`If-Match`](https://developers.openwaf.dev/api/#configuration-changes) (optional) · [`X-Change-Reason`](https://developers.openwaf.dev/api/#configuration-changes) (optional)

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `org` | path | string (uuid) | Yes | Organization id. Every route checks that it is the token's organization (otherwise `404`). |
| `X-Change-Reason` | header | string | No | Reason of the change (1-500 UTF-8 characters, no control characters); defaults to the creation of the zone. 1 to 500 characters. |
| `If-Match` | header | string | No | Expected configuration version, e.g. `"12"` (`W/` accepted). If it is not the current one: 412 `version_conflict`. |
| `dry_run` | query | string | No | `1`/`true` simulates the change: answers 200 with the plan (`Applied`) and saves nothing. `0`/`false` accepted; any other value: 400. One of `1`, `true`, `0`, `false`. |

#### Request body

Content type `application/json`.

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | Name. |
| `dns_mode` | string | No | `delegated`: OpenWAF name servers. `external`: the customer keeps their DNS and points their sites at the edge (CNAME/A). One of `delegated`, `external`. Default `delegated`. |
| `client_id` | string (uuid) | No | Assigns the zone to a client. With a client-limited credential, the zone always belongs to that client. |
| `import` | boolean | No | Delegated zones: create the records found on the public DNS right after the zone. Default `true`. |

#### Response

- `200`: With `dry_run=1`, the plan of the change ([Applied](https://developers.openwaf.dev/api/schemas/#applied)); nothing is saved.
- `201`: Created. Returns [CreatedZone](https://developers.openwaf.dev/api/schemas/#created-zone).

| Field | Type | Description |
|---|---|---|
| `id` | string (uuid) | Id. |
| `org_id` | string (uuid) | Organization id. |
| `name` | string | Name. |
| `status` | string | One of `pending`, `active`, `moved`, `disabled`. |
| `dns_mode` | string | `delegated`: OpenWAF name servers. `external`: the customer keeps their DNS and points their sites at the edge (CNAME/A). One of `delegated`, `external`. Default `delegated`. |
| `ns_pair` | array of string | Name servers the delegated zones use. Exactly 2 items. |
| `delegation_checked_at` | string (date-time) | Last delegation check. |
| `delegation` | [CheckResult](https://developers.openwaf.dev/api/schemas/#check-result) or [VerifyResult](https://developers.openwaf.dev/api/schemas/#verify-result) | Last check: CheckResult (delegated) or VerifyResult (external). |
| `activated_at` | string (date-time) | When the zone became active. |
| `moved_at` | string (date-time) | When the delegation stopped pointing to OpenWAF. |
| `disabled_at` | string (date-time) | When the zone was disabled. |
| `verification` | [ZoneVerification](https://developers.openwaf.dev/api/schemas/#zone-verification) |  |
| `cname_target` | string | External zones only: CNAME target for each site. |
| `edge_addresses` | [EdgeAddresses](https://developers.openwaf.dev/api/schemas/#edge-addresses) |  |
| `config_version` | integer | Current configuration version (also in ETag). |
| `proxied_names` | integer | Names with a proxied record. |
| `merged_into` | string (uuid) | Merged zones only (status disabled): the parent zone. |
| `merged_at` | string (date-time) | Merged zones only; they are purged after 14 days. |
| `created_at` | string (date-time) | Creation time. |
| `updated_at` | string (date-time) | Last change. |
| `client_id` | string (uuid) or null | Client of the organization the zone belongs to; null = none. |
| `detected_records` | integer | Records found on the public DNS. |
| `imported_records` | integer | Records created in the zone. |
| `import_warnings` | array of string | Warnings of the import. |

Response headers: `ETag`, `X-Config-Version`.

#### Errors

| Status | Description |
|---|---|
| `400` | Malformed request (`bad_request`). |
| `403` | Not allowed (`forbidden`) or the organization reached its zone limit (`zone_limit`). |
| `409` | The domain is already added (`zone_exists`) or is a parent or subdomain of a zone of another organization or another client (`zone_overlaps`). |
| `412` | `version_conflict`: If-Match does not match the current version. |
| `422` | Validation error (`invalid`) or missing or invalid reason (`reason_required`). |
| `503` | Delegated zones are not available right now (`unavailable`): only `external` zones are accepted. |

#### Example

**curl**


```sh
curl -X POST "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones" \
  -H "Authorization: Bearer $OPENWAF_TOKEN" \
  -H "X-Change-Reason: Add example.com" \
  -H "Content-Type: application/json" \
  --data '{
    "name": "example.com",
    "dns_mode": "delegated"
  }'
```

**TypeScript**


```ts
import { OpenWAF } from "./openwaf";

const owf = new OpenWAF({ baseUrl: "https://dash.openwaf.dev" }); // token from OPENWAF_TOKEN
const result = await owf.createZone({
  body: {
    name: "example.com",
    dns_mode: "delegated"
  }
}, {
  reason: "Add example.com"
});
```

**Python**


```py
from openwaf import OpenWAF

owf = OpenWAF(base_url="https://dash.openwaf.dev")  # token from OPENWAF_TOKEN
result = owf.create_zone(
    body={
        "name": "example.com",
        "dns_mode": "delegated"
    },
    reason="Add example.com",
)
```



```json
{
  "id": "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
  "org_id": "1e7c9a3b-5d2f-4b8a-9c6e-0f2a4b6c8d1e",
  "name": "example.com",
  "status": "pending",
  "dns_mode": "delegated",
  "ns_pair": [
    "ns1.example-dns.net",
    "ns2.example-dns.net"
  ],
  "delegation_checked_at": "2026-10-27T09:00:00Z",
  "activated_at": null,
  "config_version": 1,
  "proxied_names": 2,
  "client_id": null,
  "created_at": "2026-03-02T10:20:00Z",
  "updated_at": "2026-10-27T09:00:00Z",
  "detected_records": 6,
  "imported_records": 6,
  "import_warnings": []
}
```

## Create zones in bulk

```http
POST /v1/orgs/{org}/zones/bulk
```

Adds up to 1000 zones from a CSV body: one domain per line in the first column and, optionally, a client name in the second (clients that do not exist are created). A header line `domain` or `name` is optional and `#` starts a comment.

Each line is independent and follows the rules of [Create zone](https://developers.openwaf.dev/api/zones/#create-zone); the answer reports the result of every line. `dry_run` is not available. Public records are imported only for uploads of up to 50 domains.

**Required role:** `tech` or higher. Not available to tokens limited to a zone.

**Change controls:** [`X-Change-Reason`](https://developers.openwaf.dev/api/#configuration-changes) (optional)

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `org` | path | string (uuid) | Yes | Organization id. Every route checks that it is the token's organization (otherwise `404`). |
| `X-Change-Reason` | header | string | No | Reason of the change (1-500 UTF-8 characters, no control characters); defaults to the creation of the zone. 1 to 500 characters. |
| `import` | query | boolean | No | Import the public records of each delegated zone (only for uploads of up to 50 domains). Default `true`. |

#### Request body

Content type `text/csv`.

#### Response

- `200`: Result per line. Returns an object with the fields below.

| Field | Type | Description |
|---|---|---|
| `created` | integer |  |
| `failed` | integer |  |
| `results` | array of object |  |
| `results[].line` | integer |  |
| `results[].name` | string | Name. |
| `results[].zone` | [CreatedZone](https://developers.openwaf.dev/api/schemas/#created-zone) |  |
| `results[].error` | string |  |
| `results[].code` | string | Error code of the line. One of `invalid`, `zone_exists`, `zone_overlaps`, `zone_limit`, `forbidden`, `not_found`. |
| `results[].client` | string | Client of the line (second column). |
| `clients_created` | array of string | Clients created from the second column. |

#### Errors

| Status | Description |
|---|---|
| `400` | Invalid CSV, or `dry_run` (not available for bulk creation). |
| `422` | Validation error (`invalid`) or missing or invalid reason (`reason_required`). |

#### Example

**curl**


```sh
curl -X POST "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones/bulk" \
  -H "Authorization: Bearer $OPENWAF_TOKEN" \
  -H "X-Change-Reason: Onboard new domains" \
  -H "Content-Type: text/csv" \
  --data-binary $'domain,client\nexample.com,Acme Retail\nexample.org\n'
```

**TypeScript**


```ts
import { OpenWAF } from "./openwaf";

const owf = new OpenWAF({ baseUrl: "https://dash.openwaf.dev" }); // token from OPENWAF_TOKEN
const result = await owf.bulkCreateZones({
  body: "domain,client\nexample.com,Acme Retail\nexample.org\n"
}, {
  reason: "Onboard new domains"
});
```

**Python**


```py
from openwaf import OpenWAF

owf = OpenWAF(base_url="https://dash.openwaf.dev")  # token from OPENWAF_TOKEN
result = owf.bulk_create_zones(
    body="domain,client\nexample.com,Acme Retail\nexample.org\n",
    reason="Onboard new domains",
)
```



```json
{
  "created": 0,
  "failed": 0,
  "results": [
    {
      "line": 0,
      "name": "example.com",
      "zone": {
        "id": "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
        "org_id": "1e7c9a3b-5d2f-4b8a-9c6e-0f2a4b6c8d1e",
        "name": "example.com",
        "status": "pending",
        "dns_mode": "delegated",
        "ns_pair": [
          "ns1.example-dns.net",
          "ns2.example-dns.net"
        ],
        "delegation_checked_at": "2026-10-27T09:00:00Z",
        "activated_at": null,
        "config_version": 1,
        "proxied_names": 2,
        "client_id": null,
        "created_at": "2026-03-02T10:20:00Z",
        "updated_at": "2026-10-27T09:00:00Z",
        "detected_records": 6,
        "imported_records": 6,
        "import_warnings": []
      },
      "error": "",
      "code": "invalid",
      "client": "example"
    }
  ],
  "clients_created": []
}
```

## Get zone

```http
GET /v1/orgs/{org}/zones/{zone}
```

Returns one zone. The `ETag` header carries its current configuration version.

**Required role:** `readonly` or higher.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `org` | path | string (uuid) | Yes | Organization id. Every route checks that it is the token's organization (otherwise `404`). |
| `zone` | path | string (uuid) | Yes | Zone id (see [List zones](https://developers.openwaf.dev/api/zones/#list-zones)). |

#### Response

- `200`: Returns [Zone](https://developers.openwaf.dev/api/schemas/#zone).

| Field | Type | Description |
|---|---|---|
| `id` | string (uuid) | Id. |
| `org_id` | string (uuid) | Organization id. |
| `name` | string | Name. |
| `status` | string | One of `pending`, `active`, `moved`, `disabled`. |
| `dns_mode` | string | `delegated`: OpenWAF name servers. `external`: the customer keeps their DNS and points their sites at the edge (CNAME/A). One of `delegated`, `external`. Default `delegated`. |
| `ns_pair` | array of string | Name servers the delegated zones use. Exactly 2 items. |
| `delegation_checked_at` | string (date-time) | Last delegation check. |
| `delegation` | [CheckResult](https://developers.openwaf.dev/api/schemas/#check-result) or [VerifyResult](https://developers.openwaf.dev/api/schemas/#verify-result) | Last check: CheckResult (delegated) or VerifyResult (external). |
| `activated_at` | string (date-time) | When the zone became active. |
| `moved_at` | string (date-time) | When the delegation stopped pointing to OpenWAF. |
| `disabled_at` | string (date-time) | When the zone was disabled. |
| `verification` | [ZoneVerification](https://developers.openwaf.dev/api/schemas/#zone-verification) |  |
| `cname_target` | string | External zones only: CNAME target for each site. |
| `edge_addresses` | [EdgeAddresses](https://developers.openwaf.dev/api/schemas/#edge-addresses) |  |
| `config_version` | integer | Current configuration version (also in ETag). |
| `proxied_names` | integer | Names with a proxied record. |
| `merged_into` | string (uuid) | Merged zones only (status disabled): the parent zone. |
| `merged_at` | string (date-time) | Merged zones only; they are purged after 14 days. |
| `created_at` | string (date-time) | Creation time. |
| `updated_at` | string (date-time) | Last change. |
| `client_id` | string (uuid) or null | Client of the organization the zone belongs to; null = none. |

Response headers: `ETag`.

#### Errors

| Status | Description |
|---|---|
| `404` | Not found, or not visible to the token (`not_found`). |

#### Example

**curl**


```sh
curl "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones/6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d" \
  -H "Authorization: Bearer $OPENWAF_TOKEN"
```

**TypeScript**


```ts
import { OpenWAF } from "./openwaf";

const owf = new OpenWAF({ baseUrl: "https://dash.openwaf.dev" }); // token from OPENWAF_TOKEN
const result = await owf.getZone({
  zone: "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d"
});
```

**Python**


```py
from openwaf import OpenWAF

owf = OpenWAF(base_url="https://dash.openwaf.dev")  # token from OPENWAF_TOKEN
result = owf.get_zone(zone="6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d")
```



```json
{
  "id": "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
  "org_id": "1e7c9a3b-5d2f-4b8a-9c6e-0f2a4b6c8d1e",
  "name": "example.com",
  "status": "active",
  "dns_mode": "delegated",
  "ns_pair": [
    "ns1.example-dns.net",
    "ns2.example-dns.net"
  ],
  "delegation_checked_at": "2026-10-27T09:00:00Z",
  "activated_at": "2026-03-02T11:40:00Z",
  "config_version": 12,
  "proxied_names": 3,
  "client_id": null,
  "created_at": "2026-03-02T10:20:00Z",
  "updated_at": "2026-10-27T09:00:00Z"
}
```

## Delete zone

```http
DELETE /v1/orgs/{org}/zones/{zone}
```

Deletes a zone with its records, edge settings and rules; OpenWAF stops serving its DNS and its sites. A last version is recorded first, so the history of the deleted zone stays readable through [List versions](https://developers.openwaf.dev/api/versions/#list-zone-versions).

**Required role:** `tech` or higher.

**Change controls:** [`dry_run`](https://developers.openwaf.dev/api/#configuration-changes) · [`If-Match`](https://developers.openwaf.dev/api/#configuration-changes) (optional) · [`X-Change-Reason`](https://developers.openwaf.dev/api/#configuration-changes) (required)

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `org` | path | string (uuid) | Yes | Organization id. Every route checks that it is the token's organization (otherwise `404`). |
| `zone` | path | string (uuid) | Yes | Zone id (see [List zones](https://developers.openwaf.dev/api/zones/#list-zones)). |
| `X-Change-Reason` | header | string | Yes | Reason of the change: 1-500 UTF-8 characters after trimming spaces, no control characters. If it is missing or invalid: 422 `reason_required`. 1 to 500 characters. |
| `If-Match` | header | string | No | Expected configuration version, e.g. `"12"` (`W/` accepted). If it is not the current one: 412 `version_conflict`. |
| `dry_run` | query | string | No | `1`/`true` simulates the change: answers 200 with the plan (`Applied`) and saves nothing. `0`/`false` accepted; any other value: 400. One of `1`, `true`, `0`, `false`. |

#### Response

- `200`: With `dry_run=1`, the plan of the change ([Applied](https://developers.openwaf.dev/api/schemas/#applied)); nothing is saved.
- `204`: Deleted. No body.

Response headers: `ETag`, `X-Config-Version`.

#### Errors

| Status | Description |
|---|---|
| `400` | Malformed request (`bad_request`). |
| `404` | Not found, or not visible to the token (`not_found`). |
| `412` | `version_conflict`: If-Match does not match the current version. |
| `422` | Validation error (`invalid`) or missing or invalid reason (`reason_required`). |

#### Example

**curl**


```sh
curl -X DELETE "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones/6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d" \
  -H "Authorization: Bearer $OPENWAF_TOKEN" \
  -H "X-Change-Reason: Domain no longer used"
```

**TypeScript**


```ts
import { OpenWAF } from "./openwaf";

const owf = new OpenWAF({ baseUrl: "https://dash.openwaf.dev" }); // token from OPENWAF_TOKEN
const result = await owf.deleteZone({
  zone: "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d"
}, {
  reason: "Domain no longer used"
});
```

**Python**


```py
from openwaf import OpenWAF

owf = OpenWAF(base_url="https://dash.openwaf.dev")  # token from OPENWAF_TOKEN
result = owf.delete_zone(
    zone="6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
    reason="Domain no longer used",
)
```



## Get delegation status

```http
GET /v1/orgs/{org}/zones/{zone}/delegation
```

Returns the delegation status of a delegated zone (the name servers to set at the registrar) or, for an external zone, the status of its ownership verification.

**Required role:** `readonly` or higher.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `org` | path | string (uuid) | Yes | Organization id. Every route checks that it is the token's organization (otherwise `404`). |
| `zone` | path | string (uuid) | Yes | Zone id (see [List zones](https://developers.openwaf.dev/api/zones/#list-zones)). |

#### Response

- `200`: Returns [Delegation](https://developers.openwaf.dev/api/schemas/#delegation).

| Field | Type | Description |
|---|---|---|
| `zone_id` | string (uuid) | Zone id. |
| `name` | string | Name. |
| `status` | string | One of `pending`, `active`, `moved`, `disabled`. |
| `dns_mode` | string | `delegated`: OpenWAF name servers. `external`: the customer keeps their DNS and points their sites at the edge (CNAME/A). One of `delegated`, `external`. Default `delegated`. |
| `nameservers` | array of string | The name servers to set at the registrar. |
| `checked_at` | string (date-time) | Last check. |
| `activated_at` | string (date-time) | When the zone became active. |
| `moved_at` | string (date-time) | When the delegation stopped pointing to OpenWAF. |
| `verification` | [ZoneVerification](https://developers.openwaf.dev/api/schemas/#zone-verification) |  |
| `observation` | [CheckResult](https://developers.openwaf.dev/api/schemas/#check-result) or [VerifyResult](https://developers.openwaf.dev/api/schemas/#verify-result) | CheckResult for delegated zones, VerifyResult for external ones. |

#### Example

**curl**


```sh
curl "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones/6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d/delegation" \
  -H "Authorization: Bearer $OPENWAF_TOKEN"
```

**TypeScript**


```ts
import { OpenWAF } from "./openwaf";

const owf = new OpenWAF({ baseUrl: "https://dash.openwaf.dev" }); // token from OPENWAF_TOKEN
const result = await owf.getDelegation({
  zone: "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d"
});
```

**Python**


```py
from openwaf import OpenWAF

owf = OpenWAF(base_url="https://dash.openwaf.dev")  # token from OPENWAF_TOKEN
result = owf.get_delegation(zone="6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d")
```



```json
{
  "zone_id": "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
  "name": "example.com",
  "status": "pending",
  "dns_mode": "delegated",
  "nameservers": [
    "ns1.example-dns.net",
    "ns2.example-dns.net"
  ],
  "checked_at": "2026-10-27T09:30:00Z",
  "activated_at": "2026-10-27T09:30:00Z",
  "moved_at": "2026-10-27T09:30:00Z",
  "verification": {
    "name": "_openwaf.example.com",
    "type": "A",
    "value": "openwaf-verify=3f9c0a1e5b7d4c2a8e6f1b0d9c7a5e3f"
  },
  "observation": {
    "zone": "example.com",
    "expected": [
      "ns1.example-dns.net",
      "ns2.example-dns.net"
    ],
    "parent_zone": "com",
    "verdict": "delegated",
    "error": "",
    "checked_at": "2026-10-27T09:30:00Z",
    "sources": [
      {
        "kind": "parent",
        "server": "example",
        "host": "www.example.com",
        "ns": [
          "ns1.example-dns.net",
          "ns2.example-dns.net"
        ],
        "rcode": "NOERROR",
        "error": "",
        "match": false
      }
    ]
  }
}
```

## Check delegation now

```http
POST /v1/orgs/{org}/zones/{zone}/delegation/check
```

Checks now the delegation of a delegated zone (at the parent zone's servers and public resolvers), or the verification TXT record of an external zone, and activates the zone when it is verified. The observation is returned in `observation`.

**Required role:** `tech` or higher.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `org` | path | string (uuid) | Yes | Organization id. Every route checks that it is the token's organization (otherwise `404`). |
| `zone` | path | string (uuid) | Yes | Zone id (see [List zones](https://developers.openwaf.dev/api/zones/#list-zones)). |

#### Response

- `200`: Returns [Delegation](https://developers.openwaf.dev/api/schemas/#delegation).

| Field | Type | Description |
|---|---|---|
| `zone_id` | string (uuid) | Zone id. |
| `name` | string | Name. |
| `status` | string | One of `pending`, `active`, `moved`, `disabled`. |
| `dns_mode` | string | `delegated`: OpenWAF name servers. `external`: the customer keeps their DNS and points their sites at the edge (CNAME/A). One of `delegated`, `external`. Default `delegated`. |
| `nameservers` | array of string | The name servers to set at the registrar. |
| `checked_at` | string (date-time) | Last check. |
| `activated_at` | string (date-time) | When the zone became active. |
| `moved_at` | string (date-time) | When the delegation stopped pointing to OpenWAF. |
| `verification` | [ZoneVerification](https://developers.openwaf.dev/api/schemas/#zone-verification) |  |
| `observation` | [CheckResult](https://developers.openwaf.dev/api/schemas/#check-result) or [VerifyResult](https://developers.openwaf.dev/api/schemas/#verify-result) | CheckResult for delegated zones, VerifyResult for external ones. |

#### Example

**curl**


```sh
curl -X POST "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones/6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d/delegation/check" \
  -H "Authorization: Bearer $OPENWAF_TOKEN"
```

**TypeScript**


```ts
import { OpenWAF } from "./openwaf";

const owf = new OpenWAF({ baseUrl: "https://dash.openwaf.dev" }); // token from OPENWAF_TOKEN
const result = await owf.checkDelegation({
  zone: "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d"
});
```

**Python**


```py
from openwaf import OpenWAF

owf = OpenWAF(base_url="https://dash.openwaf.dev")  # token from OPENWAF_TOKEN
result = owf.check_delegation(zone="6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d")
```



```json
{
  "zone_id": "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
  "name": "example.com",
  "status": "pending",
  "dns_mode": "delegated",
  "nameservers": [
    "ns1.example-dns.net",
    "ns2.example-dns.net"
  ],
  "checked_at": "2026-10-27T09:30:00Z",
  "activated_at": "2026-10-27T09:30:00Z",
  "moved_at": "2026-10-27T09:30:00Z",
  "verification": {
    "name": "_openwaf.example.com",
    "type": "A",
    "value": "openwaf-verify=3f9c0a1e5b7d4c2a8e6f1b0d9c7a5e3f"
  },
  "observation": {
    "zone": "example.com",
    "expected": [
      "ns1.example-dns.net",
      "ns2.example-dns.net"
    ],
    "parent_zone": "com",
    "verdict": "delegated",
    "error": "",
    "checked_at": "2026-10-27T09:30:00Z",
    "sources": [
      {
        "kind": "parent",
        "server": "example",
        "host": "www.example.com",
        "ns": [
          "ns1.example-dns.net",
          "ns2.example-dns.net"
        ],
        "rcode": "NOERROR",
        "error": "",
        "match": false
      }
    ]
  }
}
```

## Check external zone names

```http
GET /v1/orgs/{org}/zones/{zone}/hosts
```

For an external zone, checks on public resolvers whether each proxied name points at the OpenWAF edge:

- `ok`: the CNAME chain reaches `cname_target` or every address belongs to the edge.
- `proxied_elsewhere`: every address belongs to another proxy (for example Cloudflare); it works if that proxy's origin is the OpenWAF edge, but it cannot be confirmed.
- `missing`: the name does not exist or has no data (`error` is set when no resolver answered).
- `wrong`: anything else.

The result is informative: it does not change how a verified zone is served. It is cached for at least 30 seconds per zone.

**Required role:** `readonly` or higher.

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `org` | path | string (uuid) | Yes | Organization id. Every route checks that it is the token's organization (otherwise `404`). |
| `zone` | path | string (uuid) | Yes | Zone id (see [List zones](https://developers.openwaf.dev/api/zones/#list-zones)). |

#### Response

- `200`: Returns [HostsReport](https://developers.openwaf.dev/api/schemas/#hosts-report).

| Field | Type | Description |
|---|---|---|
| `checked_at` | string (date-time) | Last check. |
| `items` | array of object |  |
| `items[].name` | string | Name. |
| `items[].status` | string | One of `ok`, `proxied_elsewhere`, `wrong`, `missing`. |
| `items[].via` | string | One of `cname`, `a`, `""`. |
| `items[].answers` | array of string | Targets of the CNAME chain, then the addresses. |
| `items[].error` | string |  |

#### Errors

| Status | Description |
|---|---|
| `404` | Not found, or not visible to the token (`not_found`). |
| `409` | `zone_not_external`: the zone is delegated (OpenWAF DNS serves its names). |
| `503` | The check is not available right now. |

#### Example

**curl**


```sh
curl "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones/6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d/hosts" \
  -H "Authorization: Bearer $OPENWAF_TOKEN"
```

**TypeScript**


```ts
import { OpenWAF } from "./openwaf";

const owf = new OpenWAF({ baseUrl: "https://dash.openwaf.dev" }); // token from OPENWAF_TOKEN
const result = await owf.listZoneHosts({
  zone: "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d"
});
```

**Python**


```py
from openwaf import OpenWAF

owf = OpenWAF(base_url="https://dash.openwaf.dev")  # token from OPENWAF_TOKEN
result = owf.list_zone_hosts(zone="6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d")
```



```json
{
  "checked_at": "2026-10-27T09:30:00Z",
  "items": [
    {
      "name": "example.com",
      "status": "ok",
      "via": "cname",
      "answers": [
        "example"
      ],
      "error": ""
    }
  ]
}
```

## Merge zone into its parent

```http
POST /v1/orgs/{org}/zones/{zone}/merge
```

Merges a zone (for example `shop.example.com`) into its parent zone (`into`, for example `example.com`): its records (same ids) and certificates move to the parent, settings that differ become per-name settings under `hosts` of the parent, and its rules move to the parent limited to the child's names. Both zones must be active and have the same DNS mode. The parent inherits the child's client if it has none; zones of different clients are not merged.

The merge is applied only if every name keeps being served exactly the same; otherwise it answers `409 merge_not_equivalent` with the plan. The child zone is then disabled (`merged_into`) and purged after 14 days; the parent gets a new version. In delegated zones the names are published in the parent before the child zone is removed, so they never stop resolving. `reason` and `dry_run` in the body take precedence over the header and the query parameter.

**Required role:** `admin` or higher. Organization-wide.

**Change controls:** [`dry_run`](https://developers.openwaf.dev/api/#configuration-changes) · [`X-Change-Reason`](https://developers.openwaf.dev/api/#configuration-changes) (required)

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `org` | path | string (uuid) | Yes | Organization id. Every route checks that it is the token's organization (otherwise `404`). |
| `zone` | path | string (uuid) | Yes | Zone id (see [List zones](https://developers.openwaf.dev/api/zones/#list-zones)). |
| `X-Change-Reason` | header | string | Yes | Reason of the change: 1-500 UTF-8 characters after trimming spaces, no control characters. If it is missing or invalid: 422 `reason_required`. 1 to 500 characters. |
| `dry_run` | query | string | No | `1`/`true` simulates the change: answers 200 with the plan (`Applied`) and saves nothing. `0`/`false` accepted; any other value: 400. One of `1`, `true`, `0`, `false`. |

#### Request body

Content type `application/json`. Schema: [MergeInput](https://developers.openwaf.dev/api/schemas/#merge-input).

| Field | Type | Required | Description |
|---|---|---|---|
| `into` | string | Yes | Id or name of the parent zone. |
| `reason` | string | No | Takes precedence over X-Change-Reason. |
| `dry_run` | boolean | No | Takes precedence over ?dry_run. Default `false`. |
| `expected_versions` | map of integer | No | Zone id or name → expected version (like If-Match). |

#### Response

- `200`: Plan (simulation, also when it cannot be applied) or merge applied (X-Config-Version and ETag = the parent's new version). Returns [MergeResult](https://developers.openwaf.dev/api/schemas/#merge-result).

| Field | Type | Description |
|---|---|---|
| `dry_run` | boolean | `true` in a simulation. |
| `equivalent` | boolean | Every name is served the same before and after. |
| `child` | [MergeZoneRef](https://developers.openwaf.dev/api/schemas/#merge-zone-ref) |  |
| `parent` | [MergeZoneRef](https://developers.openwaf.dev/api/schemas/#merge-zone-ref) |  |
| `moved` | object |  |
| `moved.records` | array of object |  |
| `moved.certificates` | array of object |  |
| `moved.rules` | array of [ZoneRule](https://developers.openwaf.dev/api/schemas/#zone-rule) | Rules created in the parent (only when applied). |
| `moved.hosts` | [HostSettingsMap](https://developers.openwaf.dev/api/schemas/#host-settings-map) |  |
| `moved.client` | object |  |
| `hosts` | array of object | Equivalence report per name. |
| `hosts[].host` | string |  |
| `hosts[].from_zone` | string | One of `child`, `parent`. |
| `hosts[].equivalent` | boolean |  |
| `hosts[].nodes` | object |  |
| `hosts[].certificates` | object |  |
| `problems` | array of string | Reasons why it cannot be applied. |
| `warnings` | array of string | Warnings (the change is still valid). |
| `diff` | [Diff](https://developers.openwaf.dev/api/schemas/#diff) | Changes. |

Response headers: `ETag`, `X-Config-Version`.

#### Errors

| Status | Description |
|---|---|
| `400` | Malformed request (`bad_request`). |
| `403` | The token's role or limits do not allow it (`forbidden`). |
| `404` | Not found, or not visible to the token (`not_found`). |
| `409` | `merge_not_equivalent`: not applied; `plan` carries the report. |
| `412` | `version_conflict`: If-Match does not match the current version. |
| `422` | Validation error (`invalid`) or missing or invalid reason (`reason_required`). |

#### Example

**curl**


```sh
curl -X POST "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones/6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d/merge" \
  -H "Authorization: Bearer $OPENWAF_TOKEN" \
  -H "X-Change-Reason: Merge shop.example.com into example.com" \
  -H "Content-Type: application/json" \
  --data '{
    "into": "example.com",
    "dry_run": true
  }'
```

**TypeScript**


```ts
import { OpenWAF } from "./openwaf";

const owf = new OpenWAF({ baseUrl: "https://dash.openwaf.dev" }); // token from OPENWAF_TOKEN
const result = await owf.mergeZone({
  zone: "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
  body: {
    into: "example.com",
    dry_run: true
  }
}, {
  reason: "Merge shop.example.com into example.com"
});
```

**Python**


```py
from openwaf import OpenWAF

owf = OpenWAF(base_url="https://dash.openwaf.dev")  # token from OPENWAF_TOKEN
result = owf.merge_zone(
    zone="6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
    body={
        "into": "example.com",
        "dry_run": True
    },
    reason="Merge shop.example.com into example.com",
)
```



```json
{
  "dry_run": false,
  "equivalent": false,
  "child": {
    "zone_id": "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
    "name": "example.com",
    "from_version": 12,
    "to_version": 12
  },
  "parent": {
    "zone_id": "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
    "name": "example.com",
    "from_version": 12,
    "to_version": 12
  },
  "moved": {
    "records": [
      {
        "id": "a5614527-0ce6-43be-ad1d-d012b7394867",
        "type": "A",
        "name": "example.com",
        "content": "203.0.113.10",
        "proxied": false
      }
    ],
    "certificates": [
      {
        "id": "a5614527-0ce6-43be-ad1d-d012b7394867",
        "hostname": "www.example.com",
        "source": "example",
        "fingerprint_sha256": "example",
        "not_after": "2026-10-27T09:30:00Z",
        "moved": false
      }
    ],
    "rules": [
      {
        "id": 3,
        "zone_id": "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
        "kind": "ip",
        "ip": "203.0.113.0/24",
        "action": "block",
        "note": "Abusive scanner",
        "hosts": [],
        "created_at": "2026-10-27T09:30:00Z"
      }
    ],
    "hosts": {
      "www.example.com": {
        "mode": "active",
        "waf_level": "off",
        "rate_limit_rpm": 0,
        "timeout_ms": 0,
        "enable_feed": false,
        "stack": "generic",
        "block_page_mode": "nginx",
        "fake_server_header": "example",
        "block_page_html": "",
        "access_gate": {},
        "max_body_mb": 0,
        "redirect_allow_hosts": [
          "www.example.com"
        ],
        "cors_allow_origins": [
          "https://app.example.com"
        ],
        "hsts": {},
        "compression": false,
        "origin": {},
        "cache": {},
        "early_hints": {},
        "rum": {},
        "ai_agents": {},
        "rate_rules": []
      }
    },
    "client": {
      "from": "75857a45-8999-45be-ac4d-941e90b6b396",
      "to": "663ea1bf-ffe5-438f-af0c-f667f14c4257"
    }
  },
  "hosts": [
    {
      "host": "www.example.com",
      "from_zone": "child",
      "equivalent": false,
      "nodes": {
        "before": [],
        "after": [],
        "lost": [],
        "gained": []
      },
      "certificates": {
        "before": [],
        "after": []
      }
    }
  ],
  "problems": [],
  "warnings": [],
  "diff": {
    "from": 0,
    "to": 0,
    "changes": [
      {
        "path": "rules[id=12]",
        "op": "add",
        "from": "example",
        "to": "example"
      }
    ]
  }
}
```

## Unmerge zone

```http
POST /v1/orgs/{org}/zones/{zone}/unmerge
```

Undoes a merge: the parent goes back to its version before the merge and the child zone to its own (records and certificates return to the child, which becomes active again). If the parent changed after the merge, revert those changes first; otherwise the answer is `409 merge_not_reversible`.

**Required role:** `admin` or higher. Organization-wide.

**Change controls:** [`dry_run`](https://developers.openwaf.dev/api/#configuration-changes) · [`X-Change-Reason`](https://developers.openwaf.dev/api/#configuration-changes) (required)

#### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `org` | path | string (uuid) | Yes | Organization id. Every route checks that it is the token's organization (otherwise `404`). |
| `zone` | path | string (uuid) | Yes | Zone id (see [List zones](https://developers.openwaf.dev/api/zones/#list-zones)). |
| `X-Change-Reason` | header | string | Yes | Reason of the change: 1-500 UTF-8 characters after trimming spaces, no control characters. If it is missing or invalid: 422 `reason_required`. 1 to 500 characters. |
| `dry_run` | query | string | No | `1`/`true` simulates the change: answers 200 with the plan (`Applied`) and saves nothing. `0`/`false` accepted; any other value: 400. One of `1`, `true`, `0`, `false`. |

#### Request body

Content type `application/json`. Schema: [UnmergeInput](https://developers.openwaf.dev/api/schemas/#unmerge-input).

| Field | Type | Required | Description |
|---|---|---|---|
| `reason` | string | No | Reason of the change. |
| `expected_version` | integer | No | Version of the parent left by the merge. |
| `dry_run` | boolean | No | `true` in a simulation. Default `false`. |

#### Response

- `200`: Unmerged or, with `dry_run`, the plan. Returns [UnmergeResult](https://developers.openwaf.dev/api/schemas/#unmerge-result).

| Field | Type | Description |
|---|---|---|
| `dry_run` | boolean | `true` in a simulation. |
| `child` | [MergeZoneRef](https://developers.openwaf.dev/api/schemas/#merge-zone-ref) |  |
| `parent` | [MergeZoneRef](https://developers.openwaf.dev/api/schemas/#merge-zone-ref) |  |
| `warnings` | array of string | Warnings (the change is still valid). |

#### Errors

| Status | Description |
|---|---|
| `400` | Malformed request (`bad_request`). |
| `403` | The token's role or limits do not allow it (`forbidden`). |
| `404` | Not found, or not visible to the token (`not_found`). |
| `409` | `merge_not_reversible`. |
| `422` | Validation error (`invalid`) or missing or invalid reason (`reason_required`). |

#### Example

**curl**


```sh
curl -X POST "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones/6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d/unmerge" \
  -H "Authorization: Bearer $OPENWAF_TOKEN" \
  -H "X-Change-Reason: Undo the merge" \
  -H "Content-Type: application/json" \
  --data '{
    "expected_version": 14
  }'
```

**TypeScript**


```ts
import { OpenWAF } from "./openwaf";

const owf = new OpenWAF({ baseUrl: "https://dash.openwaf.dev" }); // token from OPENWAF_TOKEN
const result = await owf.unmergeZone({
  zone: "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
  body: {
    expected_version: 14
  }
}, {
  reason: "Undo the merge"
});
```

**Python**


```py
from openwaf import OpenWAF

owf = OpenWAF(base_url="https://dash.openwaf.dev")  # token from OPENWAF_TOKEN
result = owf.unmerge_zone(
    zone="6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
    body={
        "expected_version": 14
    },
    reason="Undo the merge",
)
```



```json
{
  "dry_run": false,
  "child": {
    "zone_id": "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
    "name": "example.com",
    "from_version": 12,
    "to_version": 12
  },
  "parent": {
    "zone_id": "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
    "name": "example.com",
    "from_version": 12,
    "to_version": 12
  },
  "warnings": []
}
```
