# Versiones de configuración

> Recorre el historial de versiones de una zona, compara dos versiones, vuelve a una anterior y sigue su propagación al borde.

Cada cambio de la configuración de una zona (registros, ajustes de borde, reglas) crea una versión numerada con su autor y su motivo. Las versiones se pueden comparar y restaurar, y la propagación muestra qué versión ha aplicado cada nodo del borde. Consulta [Cambios de configuración](https://developers.openwaf.dev/es/api/#configuration-changes).

| Operación | Ruta |
|---|---|
| [Listar versiones](https://developers.openwaf.dev/es/api/versions/#list-zone-versions) | `GET /v1/orgs/{org}/zones/{zone}/versions` |
| [Ver una versión](https://developers.openwaf.dev/es/api/versions/#get-zone-version) | `GET /v1/orgs/{org}/zones/{zone}/versions/{version}` |
| [Comparar versiones](https://developers.openwaf.dev/es/api/versions/#get-zone-diff) | `GET /v1/orgs/{org}/zones/{zone}/diff` |
| [Volver a una versión](https://developers.openwaf.dev/es/api/versions/#revert-zone) | `POST /v1/orgs/{org}/zones/{zone}/revert` |
| [Ver la propagación](https://developers.openwaf.dev/es/api/versions/#get-zone-propagation) | `GET /v1/orgs/{org}/zones/{zone}/propagation` |

## Listar versiones

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

Lista las versiones de configuración de una zona, de la más reciente a la más antigua, sin sus instantáneas. También funciona con una zona borrada, cuya última versión es `zone.delete`; el `ETag` es entonces su versión final.

**Rol necesario:** `readonly` o superior.

#### Parámetros

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `org` | ruta | string (uuid) | Sí | Id de la organización. Cada ruta comprueba que es la del token (si no, `404`). |
| `zone` | ruta | string (uuid) | Sí | Id de la zona (consulta [Listar zonas](https://developers.openwaf.dev/es/api/zones/#list-zones)). |
| `limit` | consulta | integer | No | Número máximo de elementos. Por defecto `50`. De 1 a 500. |
| `before` | consulta | integer | No | Solo versiones anteriores a esta (paginación). Mínimo 0. |

#### Respuesta

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

| Campo | Tipo | Descripción |
|---|---|---|
| `items[].version` | integer | Número de versión. |
| `items[].created_at` | string (date-time) | Fecha de creación. |
| `items[].actor` | string | Quién hizo el cambio: un token, un usuario u OpenWAF. |
| `items[].action` | string |  |
| `items[].reason` | string | Motivo del cambio. |
| `items[].restored_from` | integer o null | Versión restaurada por una reversión (null en otro caso). |
| `items[].channel` | string | Canal del cambio cuando no es la API ni la CLI (`mcp`); ausente si no hay. |
| `items[].snapshot_sha256` | string | SHA-256 de la forma canónica de la instantánea. |
| `items[].snapshot` | [ZoneSnapshot](https://developers.openwaf.dev/es/api/schemas/#zone-snapshot) |  |

Cabeceras de la respuesta: `ETag`.

#### Errores

| Estado | Descripción |
|---|---|
| `400` | Petición mal formada (`bad_request`). |
| `404` | No existe o el token no lo ve (`not_found`). |

#### Ejemplo

**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 de 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 de 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": []
      }
    }
  ]
}
```

## Ver una versión

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

Devuelve una versión de la zona con la instantánea completa de su configuración (`snapshot`).

**Rol necesario:** `readonly` o superior.

#### Parámetros

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `org` | ruta | string (uuid) | Sí | Id de la organización. Cada ruta comprueba que es la del token (si no, `404`). |
| `zone` | ruta | string (uuid) | Sí | Id de la zona (consulta [Listar zonas](https://developers.openwaf.dev/es/api/zones/#list-zones)). |
| `version` | ruta | integer | Sí | Número de versión. Mínimo 1. |

#### Respuesta

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

| Campo | Tipo | Descripción |
|---|---|---|
| `version` | integer | Número de versión. |
| `created_at` | string (date-time) | Fecha de creación. |
| `actor` | string | Quién hizo el cambio: un token, un usuario u OpenWAF. |
| `action` | string |  |
| `reason` | string | Motivo del cambio. |
| `restored_from` | integer o null | Versión restaurada por una reversión (null en otro caso). |
| `channel` | string | Canal del cambio cuando no es la API ni la CLI (`mcp`); ausente si no hay. |
| `snapshot_sha256` | string | SHA-256 de la forma canónica de la instantánea. |
| `snapshot` | [ZoneSnapshot](https://developers.openwaf.dev/es/api/schemas/#zone-snapshot) |  |

#### Errores

| Estado | Descripción |
|---|---|
| `404` | No existe o el token no lo ve (`not_found`). |

#### Ejemplo

**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 de 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 de 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"
      }
    ]
  }
}
```

## Comparar versiones

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

Devuelve qué cambió entre dos versiones de la zona. Por defecto, la versión actual frente a la anterior.

**Rol necesario:** `readonly` o superior.

#### Parámetros

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `org` | ruta | string (uuid) | Sí | Id de la organización. Cada ruta comprueba que es la del token (si no, `404`). |
| `zone` | ruta | string (uuid) | Sí | Id de la zona (consulta [Listar zonas](https://developers.openwaf.dev/es/api/zones/#list-zones)). |
| `from` | consulta | integer | No | Versión origen (por defecto `to - 1`). Mínimo 0. |
| `to` | consulta | integer | No | Versión destino (por defecto la actual). Mínimo 0. |

#### Respuesta

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

| Campo | Tipo | Descripción |
|---|---|---|
| `from` | integer |  |
| `to` | integer |  |
| `changes` | array de [DiffChange](https://developers.openwaf.dev/es/api/schemas/#diff-change) |  |

#### Errores

| Estado | Descripción |
|---|---|
| `400` | Petición mal formada (`bad_request`). |
| `404` | No existe o el token no lo ve (`not_found`). |

#### Ejemplo

**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 de 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 de 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"
    }
  ]
}
```

## Volver a una versión

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

Restaura la configuración (registros, ajustes de borde y reglas) de `to_version` como una versión nueva; se deshacen los cambios posteriores. `If-Match` es obligatorio. Si algún sitio de la versión restaurada quedaría sin certificado válido, la respuesta es `409 missing_certs` con los `hostnames`, salvo con `allow_missing_certs` a `true`.

**Rol necesario:** `tech` o superior.

**Controles de cambio:** [`dry_run`](https://developers.openwaf.dev/es/api/#configuration-changes) · [`If-Match`](https://developers.openwaf.dev/es/api/#configuration-changes) (obligatorio) · [`X-Change-Reason`](https://developers.openwaf.dev/es/api/#configuration-changes) (obligatorio)

#### Parámetros

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `org` | ruta | string (uuid) | Sí | Id de la organización. Cada ruta comprueba que es la del token (si no, `404`). |
| `zone` | ruta | string (uuid) | Sí | Id de la zona (consulta [Listar zonas](https://developers.openwaf.dev/es/api/zones/#list-zones)). |
| `X-Change-Reason` | cabecera | string | Sí | Motivo del cambio: 1-500 caracteres UTF-8 tras recortar espacios, sin caracteres de control. Si falta o no es válido: 422 `reason_required`. De 1 a 500 caracteres. |
| `If-Match` | cabecera | string | Sí | Versión de configuración esperada, p. ej. `"12"`. Obligatorio (428 `if_match_required`); si no es la actual: 412 `version_conflict`. |
| `dry_run` | consulta | string | No | `1`/`true` simula el cambio: responde 200 con el plan (`Applied`) y no guarda nada. Admite `0`/`false`; otro valor: 400. Uno de `1`, `true`, `0`, `false`. |

#### Cuerpo de la petición

Tipo de contenido `application/json`.

| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| `to_version` | integer | Sí | Versión que crea el cambio (la misma si no cambia nada). Mínimo 1. |
| `allow_missing_certs` | boolean | No | Por defecto `false`. |

#### Respuesta

- `200`: Revertida (`RevertResult`, con X-Config-Version y ETag) o, con `dry_run`, el plan (`Applied`). Devuelve [RevertResult](https://developers.openwaf.dev/es/api/schemas/#revert-result) o [Applied](https://developers.openwaf.dev/es/api/schemas/#applied).

| Campo | Tipo | Descripción |
|---|---|---|
| `version` | integer | Versión nueva creada por la reversión. |
| `restored_from` | integer | Versión restaurada por una reversión (null en otro caso). |
| `diff` | [Diff](https://developers.openwaf.dev/es/api/schemas/#diff) | Cambios. |
| `warnings` | array de string | Avisos (el cambio sigue siendo válido). |

Cabeceras de la respuesta: `ETag`, `X-Config-Version`.

#### Errores

| Estado | Descripción |
|---|---|
| `400` | Petición mal formada (`bad_request`). |
| `403` | El rol o los límites del token no lo permiten (`forbidden`). |
| `404` | No existe o el token no lo ve (`not_found`). |
| `409` | `missing_certs`: sitios que quedarían sin certificado válido · `conflict`: la zona está desactivada (también una zona fusionada). |
| `412` | `version_conflict`: If-Match no coincide con la versión actual. |
| `422` | Error de validación (`invalid`) o motivo ausente/no válido (`reason_required`). |
| `428` | `if_match_required`: la operación exige If-Match. |

#### Ejemplo

**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 de 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 de 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": []
}
```

## Ver la propagación

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

Muestra qué versión de configuración ha aplicado cada nodo del borde que sirve la zona, frente a la versión deseada (la actual) del `ETag`. Los nodos que están dejando de servir la zona aparecen con estado `retiring` hasta que aplican una configuración sin ella.

**Rol necesario:** `readonly` o superior.

#### Parámetros

| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
| `org` | ruta | string (uuid) | Sí | Id de la organización. Cada ruta comprueba que es la del token (si no, `404`). |
| `zone` | ruta | string (uuid) | Sí | Id de la zona (consulta [Listar zonas](https://developers.openwaf.dev/es/api/zones/#list-zones)). |

#### Respuesta

- `200`: OK (ETag = versión deseada). Devuelve [Propagation](https://developers.openwaf.dev/es/api/schemas/#propagation).

| Campo | Tipo | Descripción |
|---|---|---|
| `zone_id` | string (uuid) | Id de la zona. |
| `desired_version` | integer |  |
| `nodes` | array de [NodePropagation](https://developers.openwaf.dev/es/api/schemas/#node-propagation) |  |

Cabeceras de la respuesta: `ETag`.

#### Errores

| Estado | Descripción |
|---|---|
| `404` | No existe o el token no lo ve (`not_found`). |

#### Ejemplo

**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 de 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 de 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"
    }
  ]
}
```
