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 | GET /v1/orgs/{org}/zones |
| Create zone | POST /v1/orgs/{org}/zones |
| Create zones in bulk | POST /v1/orgs/{org}/zones/bulk |
| Get zone | GET /v1/orgs/{org}/zones/{zone} |
| Delete zone | DELETE /v1/orgs/{org}/zones/{zone} |
| Get delegation status | GET /v1/orgs/{org}/zones/{zone}/delegation |
| Check delegation now | POST /v1/orgs/{org}/zones/{zone}/delegation/check |
| Check external zone names | GET /v1/orgs/{org}/zones/{zone}/hosts |
| Merge zone into its parent | POST /v1/orgs/{org}/zones/{zone}/merge |
| Unmerge zone | POST /v1/orgs/{org}/zones/{zone}/unmerge |
List zones
GET /v1/orgs/{org}/zonesLists 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: Returnsitems, an array of 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 or VerifyResult | 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 | |
items[].cname_target | string | External zones only: CNAME target for each site. |
items[].edge_addresses | EdgeAddresses | |
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 "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones" \
-H "Authorization: Bearer $OPENWAF_TOKEN"import { OpenWAF } from "./openwaf";
const owf = new OpenWAF({ baseUrl: "https://dash.openwaf.dev" }); // token from OPENWAF_TOKEN
const result = await owf.listZones();from openwaf import OpenWAF
owf = OpenWAF(base_url="https://dash.openwaf.dev") # token from OPENWAF_TOKEN
result = owf.list_zones(){
"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
POST /v1/orgs/{org}/zonesAdds 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 stayspendinguntil 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 tocname_target(or A/AAAA records toedge_addressesat the apex) and publish the TXT record inverificationto prove ownership; once verified, the zone becomesactive. 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 · If-Match (optional) · X-Change-Reason (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: Withdry_run=1, the plan of the change (Applied); nothing is saved.201: Created. Returns CreatedZone.
| 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 or VerifyResult | 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 | |
cname_target | string | External zones only: CNAME target for each site. |
edge_addresses | EdgeAddresses | |
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 -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"
}'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"
});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",
){
"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
POST /v1/orgs/{org}/zones/bulkAdds 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; 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 (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 | |
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 -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'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"
});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",
){
"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
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). |
Response
200: Returns 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 or VerifyResult | 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 | |
cname_target | string | External zones only: CNAME target for each site. |
edge_addresses | EdgeAddresses | |
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 "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones/6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d" \
-H "Authorization: Bearer $OPENWAF_TOKEN"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"
});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"){
"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
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.
Required role: tech or higher.
Change controls: dry_run · If-Match (optional) · X-Change-Reason (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). |
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: Withdry_run=1, the plan of the change (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 -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"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"
});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
GET /v1/orgs/{org}/zones/{zone}/delegationReturns 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). |
Response
200: Returns 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 | |
observation | CheckResult or VerifyResult | CheckResult for delegated zones, VerifyResult for external ones. |
Example
curl "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones/6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d/delegation" \
-H "Authorization: Bearer $OPENWAF_TOKEN"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"
});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"){
"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
POST /v1/orgs/{org}/zones/{zone}/delegation/checkChecks 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). |
Response
200: Returns 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 | |
observation | CheckResult or VerifyResult | CheckResult for delegated zones, VerifyResult for external ones. |
Example
curl -X POST "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones/6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d/delegation/check" \
-H "Authorization: Bearer $OPENWAF_TOKEN"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"
});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"){
"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
GET /v1/orgs/{org}/zones/{zone}/hostsFor an external zone, checks on public resolvers whether each proxied name points at the OpenWAF edge:
ok: the CNAME chain reachescname_targetor 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 (erroris 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). |
Response
200: Returns HostsReport.
| 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 "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones/6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d/hosts" \
-H "Authorization: Bearer $OPENWAF_TOKEN"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"
});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"){
"checked_at": "2026-10-27T09:30:00Z",
"items": [
{
"name": "example.com",
"status": "ok",
"via": "cname",
"answers": [
"example"
],
"error": ""
}
]
}Merge zone into its parent
POST /v1/orgs/{org}/zones/{zone}/mergeMerges 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 · X-Change-Reason (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). |
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.
| 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.
| Field | Type | Description |
|---|---|---|
dry_run | boolean | true in a simulation. |
equivalent | boolean | Every name is served the same before and after. |
child | MergeZoneRef | |
parent | MergeZoneRef | |
moved | object | |
moved.records | array of object | |
moved.certificates | array of object | |
moved.rules | array of ZoneRule | Rules created in the parent (only when applied). |
moved.hosts | HostSettingsMap | |
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 | 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 -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
}'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"
});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",
){
"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
POST /v1/orgs/{org}/zones/{zone}/unmergeUndoes 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 · X-Change-Reason (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). |
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.
| 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, withdry_run, the plan. Returns UnmergeResult.
| Field | Type | Description |
|---|---|---|
dry_run | boolean | true in a simulation. |
child | MergeZoneRef | |
parent | MergeZoneRef | |
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 -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
}'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"
});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",
){
"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": []
}