# Configuration versions

> Browse the version history of a zone, compare two versions, revert to an earlier one and follow its propagation to the edge.

Every change to a zone's configuration (records, edge settings, rules) creates a new numbered version with its author and reason. Versions can be compared and restored, and the propagation endpoint shows which version each edge node has applied. See [Configuration changes](https://developers.openwaf.dev/api/#configuration-changes).

| Operation | Endpoint |
|---|---|
| [List versions](https://developers.openwaf.dev/api/versions/#list-zone-versions) | `GET /v1/orgs/{org}/zones/{zone}/versions` |
| [Get version](https://developers.openwaf.dev/api/versions/#get-zone-version) | `GET /v1/orgs/{org}/zones/{zone}/versions/{version}` |
| [Compare versions](https://developers.openwaf.dev/api/versions/#get-zone-diff) | `GET /v1/orgs/{org}/zones/{zone}/diff` |
| [Revert to a version](https://developers.openwaf.dev/api/versions/#revert-zone) | `POST /v1/orgs/{org}/zones/{zone}/revert` |
| [Get propagation](https://developers.openwaf.dev/api/versions/#get-zone-propagation) | `GET /v1/orgs/{org}/zones/{zone}/propagation` |

## List versions

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

Lists the configuration versions of a zone, newest first, without their snapshots. It also works for a deleted zone, whose last version is `zone.delete`; the `ETag` is then its final 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)). |
| `limit` | query | integer | No | Maximum number of items. Default `50`. From 1 to 500. |
| `before` | query | integer | No | Only versions before this one (pagination). Minimum 0. |

#### Response

- `200`: Returns [ConfigVersionList](https://developers.openwaf.dev/api/schemas/#config-version-list).

| Field | Type | Description |
|---|---|---|
| `items[].version` | integer | Version number. |
| `items[].created_at` | string (date-time) | Creation time. |
| `items[].actor` | string | Who made the change: a token, a user or OpenWAF. |
| `items[].action` | string |  |
| `items[].reason` | string | Reason of the change. |
| `items[].restored_from` | integer or null | Version restored by a revert (null otherwise). |
| `items[].channel` | string | Channel of the change when it is neither the API nor the CLI (`mcp`); absent otherwise. |
| `items[].snapshot_sha256` | string | SHA-256 of the canonical form of the snapshot. |
| `items[].snapshot` | [ZoneSnapshot](https://developers.openwaf.dev/api/schemas/#zone-snapshot) |  |

Response headers: `ETag`.

#### Errors

| Status | Description |
|---|---|
| `400` | Malformed request (`bad_request`). |
| `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/versions?limit=20" \
  -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.listZoneVersions({
  zone: "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
  limit: 20
});
```

**Python**


```py
from openwaf import OpenWAF

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



```json
{
  "items": [
    {
      "version": 12,
      "created_at": "2026-10-27T09:30:00Z",
      "actor": "token:11111111-2222-4333-8444-555555555555",
      "action": "block",
      "reason": "Point www at the new origin",
      "restored_from": 0,
      "channel": "mcp",
      "snapshot_sha256": "example",
      "snapshot": {
        "schema": "openwaf-cloud/zone-config/v1",
        "zone": {},
        "settings": {},
        "rules": [],
        "records": [],
        "certificates": []
      }
    }
  ]
}
```

## Get version

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

Returns one version of the zone with its full configuration snapshot (`snapshot`).

**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)). |
| `version` | path | integer | Yes | Version number. Minimum 1. |

#### Response

- `200`: Returns [ConfigVersion](https://developers.openwaf.dev/api/schemas/#config-version).

| Field | Type | Description |
|---|---|---|
| `version` | integer | Version number. |
| `created_at` | string (date-time) | Creation time. |
| `actor` | string | Who made the change: a token, a user or OpenWAF. |
| `action` | string |  |
| `reason` | string | Reason of the change. |
| `restored_from` | integer or null | Version restored by a revert (null otherwise). |
| `channel` | string | Channel of the change when it is neither the API nor the CLI (`mcp`); absent otherwise. |
| `snapshot_sha256` | string | SHA-256 of the canonical form of the snapshot. |
| `snapshot` | [ZoneSnapshot](https://developers.openwaf.dev/api/schemas/#zone-snapshot) |  |

#### 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/versions/11" \
  -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.getZoneVersion({
  zone: "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
  version: 11
});
```

**Python**


```py
from openwaf import OpenWAF

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



```json
{
  "version": 12,
  "created_at": "2026-10-27T09:30:00Z",
  "actor": "token:11111111-2222-4333-8444-555555555555",
  "action": "block",
  "reason": "Point www at the new origin",
  "restored_from": 0,
  "channel": "mcp",
  "snapshot_sha256": "example",
  "snapshot": {
    "schema": "openwaf-cloud/zone-config/v1",
    "zone": {
      "id": "a5614527-0ce6-43be-ad1d-d012b7394867",
      "name": "example.com",
      "dns_mode": "delegated"
    },
    "settings": {
      "zone_id": "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
      "mode": "active",
      "waf_level": "medium",
      "rate_limit_rpm": 0,
      "timeout_ms": 0,
      "enable_feed": true,
      "stack": "generic",
      "block_page_mode": "branded",
      "fake_server_header": "",
      "block_page_html": "",
      "access_gate": {
        "enabled": false,
        "path": "/checkout",
        "ttl_hours": 0,
        "emails": []
      },
      "max_body_mb": 0,
      "redirect_allow_hosts": [
        "www.example.com"
      ],
      "cors_allow_origins": [
        "https://app.example.com"
      ],
      "hsts": {
        "max_age": 0,
        "include_subdomains": false,
        "preload": false
      },
      "compression": false,
      "origin": {
        "scheme": "https",
        "port": 0,
        "tls_verify": "full",
        "server_name": "",
        "client_cert": false
      },
      "hosts": {},
      "redirects": [
        {}
      ],
      "cache": {
        "enabled": false,
        "dev_mode_until": "2026-10-27T09:30:00Z",
        "query": {},
        "browser_ttl": 0,
        "deception_armor": true,
        "serve_stale": {},
        "max_object_mb": 100,
        "rules": []
      },
      "early_hints": {
        "enabled": false,
        "mode": "both",
        "extra_origins": []
      },
      "ai_agents": {
        "enabled": false,
        "mode": "log",
        "spoofed_challenge": "log",
        "template": "search_ok_block_training",
        "policy": {},
        "bot_overrides": [],
        "trap": {},
        "tarpit": {},
        "robots": {},
        "markdown": {},
        "block_response": {},
        "endpoints": []
      },
      "rum": {
        "enabled": false,
        "sample_rate": 1,
        "exclude_paths": []
      },
      "rate_rules": [
        {}
      ],
      "updated_at": "2026-10-27T09:30:00Z"
    },
    "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"
      }
    ],
    "records": [
      {
        "id": "a5614527-0ce6-43be-ad1d-d012b7394867",
        "seq": 0,
        "name": "example.com",
        "type": "A",
        "content": "203.0.113.10",
        "ttl": 0,
        "proxied": false,
        "priority": 0
      }
    ],
    "certificates": [
      {
        "id": "a5614527-0ce6-43be-ad1d-d012b7394867",
        "hostname": "www.example.com",
        "source": "example",
        "fingerprint_sha256": "example",
        "not_after": "2026-10-27T09:30:00Z"
      }
    ]
  }
}
```

## Compare versions

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

Returns what changed between two versions of the zone. By default, the current version against the one before it.

**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)). |
| `from` | query | integer | No | Source version (`to - 1` by default). Minimum 0. |
| `to` | query | integer | No | Target version (the current one by default). Minimum 0. |

#### Response

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

| Field | Type | Description |
|---|---|---|
| `from` | integer |  |
| `to` | integer |  |
| `changes` | array of [DiffChange](https://developers.openwaf.dev/api/schemas/#diff-change) |  |

#### Errors

| Status | Description |
|---|---|
| `400` | Malformed request (`bad_request`). |
| `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/diff?from=10&to=12" \
  -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.getZoneDiff({
  zone: "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
  from: 10,
  to: 12
});
```

**Python**


```py
from openwaf import OpenWAF

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



```json
{
  "from": 0,
  "to": 0,
  "changes": [
    {
      "path": "rules[id=12]",
      "op": "add",
      "from": "example",
      "to": "example"
    }
  ]
}
```

## Revert to a version

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

Restores the configuration (records, edge settings and rules) of `to_version` as a new version; changes made after it are undone. `If-Match` is required. If some site of the restored version would be left without a valid certificate, the answer is `409 missing_certs` with the `hostnames`, unless `allow_missing_certs` is `true`.

**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) (required) · [`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 | Yes | Expected configuration version, e.g. `"12"`. Required (428 `if_match_required`); 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 |
|---|---|---|---|
| `to_version` | integer | Yes | Version the change creates (the same if nothing changes). Minimum 1. |
| `allow_missing_certs` | boolean | No | Default `false`. |

#### Response

- `200`: Reverted (`RevertResult`, with X-Config-Version and ETag) or, with `dry_run`, the plan (`Applied`). Returns [RevertResult](https://developers.openwaf.dev/api/schemas/#revert-result) or [Applied](https://developers.openwaf.dev/api/schemas/#applied).

| Field | Type | Description |
|---|---|---|
| `version` | integer | New version created by the revert. |
| `restored_from` | integer | Version restored by a revert (null otherwise). |
| `diff` | [Diff](https://developers.openwaf.dev/api/schemas/#diff) | Changes. |
| `warnings` | array of string | Warnings (the change is still valid). |

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` | `missing_certs`: sites that would be left without a valid certificate · `conflict`: the zone is disabled (merged zones too). |
| `412` | `version_conflict`: If-Match does not match the current version. |
| `422` | Validation error (`invalid`) or missing or invalid reason (`reason_required`). |
| `428` | `if_match_required`: the operation requires If-Match. |

#### Example

**curl**


```sh
curl -X POST "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones/6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d/revert" \
  -H "Authorization: Bearer $OPENWAF_TOKEN" \
  -H "X-Change-Reason: Undo the last change" \
  -H 'If-Match: "12"' \
  -H "Content-Type: application/json" \
  --data '{
    "to_version": 11
  }'
```

**TypeScript**


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

const owf = new OpenWAF({ baseUrl: "https://dash.openwaf.dev" }); // token from OPENWAF_TOKEN
const result = await owf.revertZone({
  zone: "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
  body: {
    to_version: 11
  }
}, {
  reason: "Undo the last change",
  ifMatch: 12
});
```

**Python**


```py
from openwaf import OpenWAF

owf = OpenWAF(base_url="https://dash.openwaf.dev")  # token from OPENWAF_TOKEN
result = owf.revert_zone(
    zone="6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
    body={
        "to_version": 11
    },
    reason="Undo the last change",
    if_match=12,
)
```



```json
{
  "version": 12,
  "restored_from": 0,
  "diff": {
    "from": 0,
    "to": 0,
    "changes": [
      {
        "path": "rules[id=12]",
        "op": "add",
        "from": "example",
        "to": "example"
      }
    ]
  },
  "warnings": []
}
```

## Get propagation

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

Shows which configuration version each edge node serving the zone has applied, against the desired (current) version in the `ETag`. Nodes that are leaving the zone appear with status `retiring` until they apply a configuration without it.

**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`: OK (ETag = desired version). Returns [Propagation](https://developers.openwaf.dev/api/schemas/#propagation).

| Field | Type | Description |
|---|---|---|
| `zone_id` | string (uuid) | Zone id. |
| `desired_version` | integer |  |
| `nodes` | array of [NodePropagation](https://developers.openwaf.dev/api/schemas/#node-propagation) |  |

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/propagation" \
  -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.getZonePropagation({
  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_propagation(zone="6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d")
```



```json
{
  "zone_id": "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
  "desired_version": 12,
  "nodes": [
    {
      "node_id": "edge-node-1",
      "status": "converged",
      "applied_version": 12,
      "applied_serial": 0,
      "current_serial": 0,
      "pending_since": "2026-10-27T09:30:00Z",
      "lag_seconds": 0,
      "last_seen_at": "2026-10-27T09:30:00Z"
    }
  ]
}
```
