# Security events

> Query the security events of a zone, with filters, summaries, histograms and a live Server-Sent Events stream.

Security events are the requests the edge blocked, challenged or logged (WAF, rules, rate limits, shields). All endpoints share the same filters; every filter can be repeated and different filters combine with AND.

| Operation | Endpoint |
|---|---|
| [List events](https://developers.openwaf.dev/api/security-events/#list-events) | `GET /v1/orgs/{org}/zones/{zone}/events` |
| [Summarize events](https://developers.openwaf.dev/api/security-events/#get-events-summary) | `GET /v1/orgs/{org}/zones/{zone}/events/summary` |
| [Events histogram](https://developers.openwaf.dev/api/security-events/#get-events-histogram) | `GET /v1/orgs/{org}/zones/{zone}/events/histogram` |
| [Stream events](https://developers.openwaf.dev/api/security-events/#stream-events) | `GET /v1/orgs/{org}/zones/{zone}/events/stream` |

## List events

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

Lists the security events of the zone, newest first. Every filter can be repeated (`action=block&action=log` matches any of the values) with at most 20 values; different filters combine with AND, and the `not_…` filters exclude events with any of their values. `q` searches text (case-insensitive) in host, path, IP, method, rule, attack type, message, user agent, country, rule id and status. Page with `cursor` set to `next_cursor`.

**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)). |
| `since` | query | string (date-time) | No | RFC 3339; until minus 24 hours by default. |
| `until` | query | string (date-time) | No | RFC 3339, exclusive; now by default. At most 31 days of range. |
| `action` | query | string | No | Repeatable (any of the values, at most 20). One of `block`, `log`, `challenge`, `allow`. |
| `rule_id` | query | integer | No | Repeatable. From 0 to 4294967295. |
| `ip` | query | string | No | IPv4 or IPv6 (normalized); repeatable. |
| `country` | query | string | No | ISO 3166-1 alpha-2 (lowercase accepted); repeatable. Pattern `^[A-Za-z]{2}$`. |
| `host` | query | string | No | Exact name (case-insensitive); repeatable. 1 to 253 characters. |
| `path` | query | string | No | Exact path (1-2048 bytes); repeatable. 1 to 2048 characters. |
| `status` | query | integer | No | HTTP status of the response; repeatable. From 100 to 599. |
| `rate_rule` | query | string | No | Events of that rate limiting rule (rule_name `rate_rule:<id>`, attack_type `rate_limit`); repeatable. Pattern `^[a-z0-9][a-z0-9-]{0,39}$`. |
| `rule_name` | query | string | No | Exact rule_name (the code of an egress block, e.g. `injection_detected`); repeatable. 1 to 200 characters. |
| `not_action` | query | string | No | Excludes those actions; repeatable. One of `block`, `log`, `challenge`, `allow`. |
| `not_rule_id` | query | integer | No | Excludes those rules; repeatable. From 0 to 4294967295. |
| `not_ip` | query | string | No | Excludes those IPs; repeatable. |
| `not_country` | query | string | No | Excludes those countries; repeatable. Pattern `^[A-Za-z]{2}$`. |
| `not_host` | query | string | No | Excludes those names; repeatable. 1 to 253 characters. |
| `not_path` | query | string | No | Excludes those exact paths; repeatable. 1 to 2048 characters. |
| `not_status` | query | integer | No | Excludes those HTTP statuses; repeatable. From 100 to 599. |
| `not_rate_rule` | query | string | No | Excludes the events of those rate limiting rules; repeatable. Pattern `^[a-z0-9][a-z0-9-]{0,39}$`. |
| `not_rule_name` | query | string | No | Excludes those rule_name values; repeatable. 1 to 200 characters. |
| `q` | query | string | No | Free text (case-insensitive) in host, path, IP, method, rule_name, attack_type, msg, user agent, country, rule_id and status. At most 200 characters. |
| `limit` | query | integer | No | Maximum number of items. Default `100`. From 1 to 1000. |
| `cursor` | query | string | No | next_cursor of the previous page. |

#### Response

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

| Field | Type | Description |
|---|---|---|
| `items` | array of [EdgeEvent](https://developers.openwaf.dev/api/schemas/#edge-event) |  |
| `next_cursor` | string |  |

#### Errors

| Status | Description |
|---|---|
| `400` | Malformed request (`bad_request`). |
| `404` | Not found, or not visible to the token (`not_found`). |
| `503` | Not available right now (`unavailable`); retry later. |

#### Example

**curl**


```sh
curl "https://dash.openwaf.dev/v1/orgs/$OPENWAF_ORG/zones/6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d/events?action=block&limit=50" \
  -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.listEvents({
  zone: "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
  action: "block",
  limit: 50
});
```

**Python**


```py
from openwaf import OpenWAF

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



```json
{
  "items": [
    {
      "event_id": "8b3e5f7a-1c2d-4e6f-9a0b-3c5d7e9f1a2b",
      "zone_id": "6f1d2c3b-8a4e-4b7d-9c21-5e3f7a8b9c0d",
      "node_id": "edge-node",
      "ts": "2026-10-27T09:29:58Z",
      "host": "www.example.com",
      "ip": "203.0.113.10",
      "method": "POST",
      "path": "/login",
      "status": 429,
      "action": "block",
      "rule_id": 0,
      "rule_name": "rate_rule:login",
      "attack_type": "rate_limit",
      "msg": "",
      "country_code": "ES",
      "asn": 64500,
      "user_agent": "Mozilla/5.0",
      "tls_fingerprint": "",
      "received_at": "2026-10-27T09:29:59Z",
      "rate_rule": "login"
    }
  ],
  "next_cursor": ""
}
```

## Summarize events

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

Counts the events that match the filters (the same as [List events](https://developers.openwaf.dev/api/security-events/#list-events)) by action, with the 10 rules, the 10 countries and the 10 rate limiting rules with most events.

**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)). |
| `since` | query | string (date-time) | No | RFC 3339; until minus 24 hours by default. |
| `until` | query | string (date-time) | No | RFC 3339, exclusive; now by default. At most 31 days of range. |
| `action` | query | string | No | Repeatable (any of the values, at most 20). One of `block`, `log`, `challenge`, `allow`. |
| `rule_id` | query | integer | No | Repeatable. From 0 to 4294967295. |
| `ip` | query | string | No | IPv4 or IPv6 (normalized); repeatable. |
| `country` | query | string | No | ISO 3166-1 alpha-2 (lowercase accepted); repeatable. Pattern `^[A-Za-z]{2}$`. |
| `host` | query | string | No | Exact name (case-insensitive); repeatable. 1 to 253 characters. |
| `path` | query | string | No | Exact path (1-2048 bytes); repeatable. 1 to 2048 characters. |
| `status` | query | integer | No | HTTP status of the response; repeatable. From 100 to 599. |
| `rate_rule` | query | string | No | Events of that rate limiting rule (rule_name `rate_rule:<id>`, attack_type `rate_limit`); repeatable. Pattern `^[a-z0-9][a-z0-9-]{0,39}$`. |
| `rule_name` | query | string | No | Exact rule_name (the code of an egress block, e.g. `injection_detected`); repeatable. 1 to 200 characters. |
| `not_action` | query | string | No | Excludes those actions; repeatable. One of `block`, `log`, `challenge`, `allow`. |
| `not_rule_id` | query | integer | No | Excludes those rules; repeatable. From 0 to 4294967295. |
| `not_ip` | query | string | No | Excludes those IPs; repeatable. |
| `not_country` | query | string | No | Excludes those countries; repeatable. Pattern `^[A-Za-z]{2}$`. |
| `not_host` | query | string | No | Excludes those names; repeatable. 1 to 253 characters. |
| `not_path` | query | string | No | Excludes those exact paths; repeatable. 1 to 2048 characters. |
| `not_status` | query | integer | No | Excludes those HTTP statuses; repeatable. From 100 to 599. |
| `not_rate_rule` | query | string | No | Excludes the events of those rate limiting rules; repeatable. Pattern `^[a-z0-9][a-z0-9-]{0,39}$`. |
| `not_rule_name` | query | string | No | Excludes those rule_name values; repeatable. 1 to 200 characters. |
| `q` | query | string | No | Free text (case-insensitive) in host, path, IP, method, rule_name, attack_type, msg, user agent, country, rule_id and status. At most 200 characters. |

#### Response

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

| Field | Type | Description |
|---|---|---|
| `since` | string (date-time) |  |
| `until` | string (date-time) |  |
| `total` | integer |  |
| `by_action` | array of [KeyCount](https://developers.openwaf.dev/api/schemas/#key-count) |  |
| `top_rules` | array of object |  |
| `top_rules[].rule_id` | integer | Id of the rule. |
| `top_rules[].rule_name` | string | Name or code of the rule. |
| `top_rules[].count` | integer |  |
| `top_countries` | array of [KeyCount](https://developers.openwaf.dev/api/schemas/#key-count) |  |
| `top_rate_rules` | array of [KeyCount](https://developers.openwaf.dev/api/schemas/#key-count) | Events of rate limiting rules per rule id (key). |

#### Errors

| Status | Description |
|---|---|
| `400` | Malformed request (`bad_request`). |
| `404` | Not found, or not visible to the token (`not_found`). |
| `503` | Not available right now (`unavailable`); retry later. |

#### Example

**curl**


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



```json
{
  "since": "2026-10-27T09:30:00Z",
  "until": "2026-10-27T09:30:00Z",
  "total": 128,
  "by_action": [
    {
      "key": "/pricing",
      "count": 128
    }
  ],
  "top_rules": [
    {
      "rule_id": 0,
      "rule_name": "example",
      "count": 128
    }
  ],
  "top_countries": [
    {
      "key": "/pricing",
      "count": 128
    }
  ],
  "top_rate_rules": [
    {
      "key": "/pricing",
      "count": 128
    }
  ]
}
```

## Events histogram

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

Counts the events that match the filters per time interval and action. Bars of `step` seconds are aligned to multiples of `step` since the Unix epoch, with no gaps (empty bars count 0); the first one contains `since` and the last one starts before `until`.

**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)). |
| `since` | query | string (date-time) | No | RFC 3339; until minus 24 hours by default. |
| `until` | query | string (date-time) | No | RFC 3339, exclusive; now by default. At most 31 days of range. |
| `step` | query | integer | No | Seconds per bar (1-86400). By default the smallest of 1, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600, 10800, 21600, 43200 and 86400 that gives at most 60 bars; with an explicit step, more than 1000 bars answers 400. From 1 to 86400. |
| `action` | query | string | No | Repeatable (any of the values, at most 20). One of `block`, `log`, `challenge`, `allow`. |
| `rule_id` | query | integer | No | Repeatable. From 0 to 4294967295. |
| `ip` | query | string | No | IPv4 or IPv6 (normalized); repeatable. |
| `country` | query | string | No | ISO 3166-1 alpha-2 (lowercase accepted); repeatable. Pattern `^[A-Za-z]{2}$`. |
| `host` | query | string | No | Exact name (case-insensitive); repeatable. 1 to 253 characters. |
| `path` | query | string | No | Exact path (1-2048 bytes); repeatable. 1 to 2048 characters. |
| `status` | query | integer | No | HTTP status of the response; repeatable. From 100 to 599. |
| `rate_rule` | query | string | No | Events of that rate limiting rule (rule_name `rate_rule:<id>`, attack_type `rate_limit`); repeatable. Pattern `^[a-z0-9][a-z0-9-]{0,39}$`. |
| `rule_name` | query | string | No | Exact rule_name (the code of an egress block, e.g. `injection_detected`); repeatable. 1 to 200 characters. |
| `not_action` | query | string | No | Excludes those actions; repeatable. One of `block`, `log`, `challenge`, `allow`. |
| `not_rule_id` | query | integer | No | Excludes those rules; repeatable. From 0 to 4294967295. |
| `not_ip` | query | string | No | Excludes those IPs; repeatable. |
| `not_country` | query | string | No | Excludes those countries; repeatable. Pattern `^[A-Za-z]{2}$`. |
| `not_host` | query | string | No | Excludes those names; repeatable. 1 to 253 characters. |
| `not_path` | query | string | No | Excludes those exact paths; repeatable. 1 to 2048 characters. |
| `not_status` | query | integer | No | Excludes those HTTP statuses; repeatable. From 100 to 599. |
| `not_rate_rule` | query | string | No | Excludes the events of those rate limiting rules; repeatable. Pattern `^[a-z0-9][a-z0-9-]{0,39}$`. |
| `not_rule_name` | query | string | No | Excludes those rule_name values; repeatable. 1 to 200 characters. |
| `q` | query | string | No | Free text (case-insensitive) in host, path, IP, method, rule_name, attack_type, msg, user agent, country, rule_id and status. At most 200 characters. |

#### Response

- `200`: Returns [EventsHistogram](https://developers.openwaf.dev/api/schemas/#events-histogram).

| Field | Type | Description |
|---|---|---|
| `since` | string (date-time) |  |
| `until` | string (date-time) |  |
| `step` | integer | Seconds per bar. |
| `total` | integer | Events in [since, until). |
| `buckets` | array of object |  |
| `buckets[].ts` | string (date-time) | Start of the bar. |
| `buckets[].count` | integer |  |
| `buckets[].block` | integer |  |
| `buckets[].log` | integer |  |
| `buckets[].challenge` | integer |  |
| `buckets[].allow` | integer |  |

#### Errors

| Status | Description |
|---|---|
| `400` | Malformed request (`bad_request`). |
| `404` | Not found, or not visible to the token (`not_found`). |
| `503` | Not available right now (`unavailable`); retry later. |

#### Example

**curl**


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



```json
{
  "since": "2026-10-27T09:30:00Z",
  "until": "2026-10-27T09:30:00Z",
  "step": 0,
  "total": 128,
  "buckets": [
    {
      "ts": "2026-10-27T09:30:00Z",
      "count": 128,
      "block": 0,
      "log": 0,
      "challenge": 0,
      "allow": 0
    }
  ]
}
```

## Stream events

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

Streams the zone's events that match the filters (the same as [List events](https://developers.openwaf.dev/api/security-events/#list-events); `since`, `until` and `limit` are not used) as Server-Sent Events, in the order OpenWAF receives them. Authentication, zone and filters are checked before the stream starts, so errors are JSON answers.

The stream starts after `Last-Event-ID` (sent by `EventSource` when it reconnects) or `cursor`; without them, 10 seconds before the request. Load `GET …/events` first and drop repeated `event_id` values. Frames:

- `event: ready` first, with `{"poll_ms": 2000, "heartbeat_ms": 15000, "max_age_s": 600}`.
- `event: event` for each event, with its cursor in `id` and the event in `data` (as in List events).
- `event: ping` after `heartbeat_ms` without traffic, to detect hung connections.
- `event: bye` after `max_age_s`, then the stream closes; `EventSource` reconnects with `Last-Event-ID` without losing events.
- `event: error` with `{"code": "unavailable"}` if the event store fails, then the stream closes.

An event received more than an hour after it happened is not streamed (it is listed by List events). At most 10 streams per organization.

**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)). |
| `Last-Event-ID` | header | string | No | `id` of the last event received from a stream (sent by EventSource when it reconnects); takes precedence over `cursor`. |
| `cursor` | query | string | No | `id` of an event of a previous stream: starts right after it. |
| `action` | query | string | No | Repeatable (any of the values, at most 20). One of `block`, `log`, `challenge`, `allow`. |
| `rule_id` | query | integer | No | Repeatable. From 0 to 4294967295. |
| `ip` | query | string | No | IPv4 or IPv6 (normalized); repeatable. |
| `country` | query | string | No | ISO 3166-1 alpha-2 (lowercase accepted); repeatable. Pattern `^[A-Za-z]{2}$`. |
| `host` | query | string | No | Exact name (case-insensitive); repeatable. 1 to 253 characters. |
| `path` | query | string | No | Exact path (1-2048 bytes); repeatable. 1 to 2048 characters. |
| `status` | query | integer | No | HTTP status of the response; repeatable. From 100 to 599. |
| `rate_rule` | query | string | No | Events of that rate limiting rule (rule_name `rate_rule:<id>`, attack_type `rate_limit`); repeatable. Pattern `^[a-z0-9][a-z0-9-]{0,39}$`. |
| `rule_name` | query | string | No | Exact rule_name (the code of an egress block, e.g. `injection_detected`); repeatable. 1 to 200 characters. |
| `not_action` | query | string | No | Excludes those actions; repeatable. One of `block`, `log`, `challenge`, `allow`. |
| `not_rule_id` | query | integer | No | Excludes those rules; repeatable. From 0 to 4294967295. |
| `not_ip` | query | string | No | Excludes those IPs; repeatable. |
| `not_country` | query | string | No | Excludes those countries; repeatable. Pattern `^[A-Za-z]{2}$`. |
| `not_host` | query | string | No | Excludes those names; repeatable. 1 to 253 characters. |
| `not_path` | query | string | No | Excludes those exact paths; repeatable. 1 to 2048 characters. |
| `not_status` | query | integer | No | Excludes those HTTP statuses; repeatable. From 100 to 599. |
| `not_rate_rule` | query | string | No | Excludes the events of those rate limiting rules; repeatable. Pattern `^[a-z0-9][a-z0-9-]{0,39}$`. |
| `not_rule_name` | query | string | No | Excludes those rule_name values; repeatable. 1 to 200 characters. |
| `q` | query | string | No | Free text (case-insensitive) in host, path, IP, method, rule_name, attack_type, msg, user agent, country, rule_id and status. At most 200 characters. |

#### Response

- `200`: Server-Sent Events stream (frames described above). Body of type `text/event-stream`.

#### Errors

| Status | Description |
|---|---|
| `400` | Malformed request (`bad_request`). |
| `401` | Missing, invalid or expired token (`unauthorized`). |
| `404` | Not found, or not visible to the token (`not_found`). |
| `429` | `too_many_streams`: too many open streams in the organization; `Retry-After` in seconds. |
| `503` | Not available right now (`unavailable`); retry later. |

#### Example

Not in the SDKs: open the stream with `EventSource` or any Server-Sent Events client.

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

```text
retry: 3000
event: ready
data: {"poll_ms":2000,"heartbeat_ms":15000,"max_age_s":600}

id: c2V0MTcz…
event: event
data: {"event_id":"…","action":"block","host":"www.example.com","path":"/wp-login.php","client_ip":"203.0.113.10"}
```
