# Rules

> Add, list and delete the edge rules of a zone, IP and country rules, WAF exceptions and rate limits.

Edge rules of a zone, applied by the edge to every proxied name or only to the names listed in `hosts`. Four kinds exist, `ip`, `geo`, `waf_exception` and `rate_limit`. See [WAF](https://developers.openwaf.dev/waf/) and [Rules](https://developers.openwaf.dev/rules/).

| Operation | Endpoint |
|---|---|
| [List rules](https://developers.openwaf.dev/api/rules/#list-rules) | `GET /v1/orgs/{org}/zones/{zone}/rules` |
| [Create rule](https://developers.openwaf.dev/api/rules/#create-rule) | `POST /v1/orgs/{org}/zones/{zone}/rules` |
| [Delete rule](https://developers.openwaf.dev/api/rules/#delete-rule) | `DELETE /v1/orgs/{org}/zones/{zone}/rules/{rule}` |

## List rules

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

Lists the edge rules of the zone with their ids and the names each one is limited to (`hosts`; empty means every name).

**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 `items`, an array of [ZoneRule](https://developers.openwaf.dev/api/schemas/#zone-rule).

| Field | Type | Description |
|---|---|---|
| `items[].kind` | string | One of `ip`, `geo`, `waf_exception`, `rate_limit`. |
| `items[].ip` | string | IP or CIDR. |
| `items[].action` | string | One of `block`, `allow`. |
| `items[].country_code` | string | ISO 3166-1 alpha-2 country code. Pattern `^[A-Za-z]{2}$`. |
| `items[].rule_id` | integer | Id of the rule. Minimum 0. |
| `items[].path_pattern` | string | "" or * = all; ending in * = prefix; otherwise exact. |
| `items[].method` | string |  |
| `items[].rpm` | integer | Minimum 0. |
| `items[].burst` | integer | Default `0`. Minimum 0. |
| `items[].note` | string | At most 500 characters. |
| `items[].hosts` | array of string | Names it applies to; absent = every name of the zone. |
| `items[].id` | integer | Id. |
| `items[].zone_id` | string (uuid) | Zone id. |
| `items[].created_at` | string (date-time) | Creation time. |

#### Example

**curl**


```sh
curl "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones/6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d/rules" \
  -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.listRules({
  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_rules(zone="6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d")
```



```json
{
  "items": [
    {
      "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"
    }
  ]
}
```

## Create rule

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

Adds an edge rule. The fields depend on `kind`:

| `kind` | Fields |
|---|---|
| `ip` | `ip` (address or CIDR), `action` |
| `geo` | `country_code`, `action` |
| `waf_exception` | `rule_id` (`0` = every WAF rule), `path_pattern` |
| `rate_limit` | `path_pattern`, `method`, `rpm`, `burst` |

`rate_limit` rules are the advanced, per-path rate limits; the basic per-IP limit is `rate_limit_rpm` in the [edge settings](https://developers.openwaf.dev/api/edge-settings/#update-zone-edge). `hosts` limits the rule to those names (the zone or below it); a name without a proxied record gives a warning.

**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`. |

#### Request body

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

| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | string | Yes | One of `ip`, `geo`, `waf_exception`, `rate_limit`. |
| `ip` | string | No | IP or CIDR. |
| `action` | string | No | One of `block`, `allow`. |
| `country_code` | string | No | ISO 3166-1 alpha-2 country code. Pattern `^[A-Za-z]{2}$`. |
| `rule_id` | integer | No | Id of the rule. Minimum 0. |
| `path_pattern` | string | No | "" or * = all; ending in * = prefix; otherwise exact. |
| `method` | string | No |  |
| `rpm` | integer | No | Minimum 0. |
| `burst` | integer | No | Default `0`. Minimum 0. |
| `note` | string | No | At most 500 characters. |
| `hosts` | array of string | No | Names of the zone (the zone or below it) it applies to; absent or empty = all. |

#### 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 [ZoneRule](https://developers.openwaf.dev/api/schemas/#zone-rule).

| Field | Type | Description |
|---|---|---|
| `kind` | string | One of `ip`, `geo`, `waf_exception`, `rate_limit`. |
| `ip` | string | IP or CIDR. |
| `action` | string | One of `block`, `allow`. |
| `country_code` | string | ISO 3166-1 alpha-2 country code. Pattern `^[A-Za-z]{2}$`. |
| `rule_id` | integer | Id of the rule. Minimum 0. |
| `path_pattern` | string | "" or * = all; ending in * = prefix; otherwise exact. |
| `method` | string |  |
| `rpm` | integer | Minimum 0. |
| `burst` | integer | Default `0`. Minimum 0. |
| `note` | string | At most 500 characters. |
| `hosts` | array of string | Names it applies to; absent = every name of the zone. |
| `id` | integer | Id. |
| `zone_id` | string (uuid) | Zone id. |
| `created_at` | string (date-time) | Creation time. |

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

#### Errors

| Status | Description |
|---|---|
| `400` | Malformed request (`bad_request`). |
| `409` | `zone_merged`: the zone is merged into another one. |
| `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/rules" \
  -H "Authorization: Bearer $OPENWAF_TOKEN" \
  -H "X-Change-Reason: Block an abusive network" \
  -H 'If-Match: "12"' \
  -H "Content-Type: application/json" \
  --data '{
    "kind": "ip",
    "ip": "203.0.113.0/24",
    "action": "block",
    "note": "Abusive scanner"
  }'
```

**TypeScript**


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

const owf = new OpenWAF({ baseUrl: "https://dash.openwaf.dev" }); // token from OPENWAF_TOKEN
const result = await owf.createRule({
  zone: "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
  body: {
    kind: "ip",
    ip: "203.0.113.0/24",
    action: "block",
    note: "Abusive scanner"
  }
}, {
  reason: "Block an abusive network",
  ifMatch: 12
});
```

**Python**


```py
from openwaf import OpenWAF

owf = OpenWAF(base_url="https://dash.openwaf.dev")  # token from OPENWAF_TOKEN
result = owf.create_rule(
    zone="6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
    body={
        "kind": "ip",
        "ip": "203.0.113.0/24",
        "action": "block",
        "note": "Abusive scanner"
    },
    reason="Block an abusive network",
    if_match=12,
)
```



```json
{
  "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"
}
```

## Delete rule

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

Removes an edge rule of the zone.

**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)). |
| `rule` | path | integer | Yes | Rule id. |
| `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`). |
| `409` | `zone_merged`: the zone is merged into another one. |
| `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/rules/3" \
  -H "Authorization: Bearer $OPENWAF_TOKEN" \
  -H "X-Change-Reason: The network is no longer abusive" \
  -H 'If-Match: "12"'
```

**TypeScript**


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

const owf = new OpenWAF({ baseUrl: "https://dash.openwaf.dev" }); // token from OPENWAF_TOKEN
const result = await owf.deleteRule({
  zone: "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
  rule: 3
}, {
  reason: "The network is no longer abusive",
  ifMatch: 12
});
```

**Python**


```py
from openwaf import OpenWAF

owf = OpenWAF(base_url="https://dash.openwaf.dev")  # token from OPENWAF_TOKEN
result = owf.delete_rule(
    zone="6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
    rule=3,
    reason="The network is no longer abusive",
    if_match=12,
)
```
