# WAF API

> Manage web application firewall policies, browse managed rulesets, test a policy against requests before saving it, and read firewall analytics.

Source: https://www.apitraffic.io/docs/api-reference/waf

The web application firewall runs inside your application through the SDK. A WAF policy decides what it inspects, what it blocks and how: managed rulesets, your own rules, allow and deny lists, rate limits, outbound (egress) controls and behavioral scoring. Each policy applies to the buckets and environments you choose, and connected SDKs receive changes without a restart. See [WAF policies](https://www.apitraffic.io/docs/waf/policies) for building a policy in the app.

**Plan:** needs a plan that includes the **Web application firewall**. Listing, reading and deleting existing policies, and reading analytics, keep working if the plan changes; servers stop enforcing policies when the plan no longer includes the firewall. The number of policies depends on the plan. **Permissions:** any member, or a `user` or `administrator` API token, can read policies, rulesets and analytics and run the simulator. Creating, changing and deleting a policy needs an owner or admin, or an `administrator` API token.

## The WAF policy object

- **sid (string, required)**:
  The policy's identifier (`waf_` followed by 27 letters and digits).

- **accountSid (string, required)**:
  The account.

- **description (string | null)**:
  A short description.

- **enabled (boolean)**:
  Whether the policy is active. Default `true`.

- **mode (string)**:
  `off`, `detect` (record what would be blocked, block nothing) or `block` (enforce). Default `detect`.

- **paranoiaLevel (integer)**:
  1 to 4. Higher levels turn on stricter rules, catching more attacks with more false positives. Default `1`.

- **anomalyThreshold (integer)**:
  The total rule score at or above which a request is blocked. Default `5`.

- **failMode (string)**:
  What happens if the firewall itself errors: `open` (let the request through, the default) or `closed` (refuse it).

- **blockResponseSid (string | null)**:
  The [block response](https://www.apitraffic.io/docs/waf/block-responses) the policy uses, or `null` for a response of its own. While set, `defaultBlock` follows that response. On a write, omit it to keep what the policy has, or send `null` with a `defaultBlock` to give the policy its own.

- **defaultBlock (object)**:
  The response a blocked request gets, which your servers enforce: `status` (default `403`), `body` (default `Forbidden`, no `<`), `contentType` (`auto`, `json`, `problem+json` or `text`; omitted means `auto`) and `headers` (only `Retry-After`, `Cache-Control` and `RateLimit-*`). For a policy that names a `blockResponseSid`, it is that response's content and is ignored on a write.

- **managedRuleset (object)**:
  The managed ruleset the policy uses: `id` (default `apitraffic-core`), `version` (`latest` follows new releases; a version number pins one) and `enabled`.

- **managedRulesetStatus (object | null)**:
  What the ruleset currently resolves to: `id`, `version`, `resolvedVersion`, `name`, `enabled` and `activeRuleCount` (managed rules running after paranoia and tuning).

- **disabledRules (string[])**:
  Managed rule ids turned off for this policy.

- **ruleExclusions (object[])**:
  Scoped exceptions for false positives. Each names a `ruleId` or `category`, and can be limited to `targets`, argument, header or cookie `names` (case-insensitive) and a `pathPattern` regular expression.

- **rules (object[])**:
  Your own rules: `id`, `category`, `message`, `severity` (default `4`), `score` (default `5`), `paranoia` (1 to 4), `enabled`, `action` (`score`, `block`, `pass` or `tag`; default `score`), `targets`, `operator`, `negate`, `transforms` and `tags`. A `criteria` operator's `query` uses the [search syntax](https://www.apitraffic.io/docs/traffic/search) with the request fields the firewall sees, at most 4,096 characters and no more than 256 spaces in a row. A query your SDK can't evaluate as written is refused with `400`: one that doesn't parse, has a `like` pattern that isn't a valid regular expression, or that older SDKs can't parse or read differently (for example a bare value starting with `AND` or `OR`, which they cut in two: quote the value). One stored before is kept while unchanged and sent to your servers turned off, with a `criteria-rule-sent-disabled` warning.

- **lists (object[])**:
  Allow and deny lists: `id`, `kind` (`ip`, `country`, `asn`, `userAgent` or `path`), `effect` (`allow` or `deny`), `values` and `message`.

- **rateLimits (object[])**:
  Rate limits.

**rate limit**

- **id (string)**: Identifier.

- **by (string)**: What is counted: `ip`, `header`, `path`, `tag` or `composite`, with `key` naming the header or tag.

- **pathPattern (string)**: Only paths matching this regular expression.

- **limit (integer)**: Requests allowed per window.

- **windowSeconds (integer)**: The window.

- **mode (string)**: `distributed` (default), `strict` or `local`.

- **scope (string)**: `instance` (default): each server counts on its own. `fleet-approx`: each server allows its share of `limit` across the servers running the firewall, assuming load is spread evenly.

- **status (integer)**: Response status when the limit is hit. Default `429`.

- **message (string)**: Response message.

- **limits (object)**:
  Request size limits: `maxUrlLength`, `maxHeaderCount`, `maxHeaderValueLength`, `maxBodyBytes`, `maxBodyInspectBytes` (8192 to 1048576, default 131072), `inspectionLimitScore` (0 to 1000, default 0), `customerPatternBudgetMs` (5 to 500, default 25) and `allowedMethods`.

- **geo (object)**:
  The headers your edge sets with the client's country (`countryHeader`, for example `cf-ipcountry`) and network (`asnHeader`), for country and ASN lists. A country header is preferred where a request has it; without it, servers look the client IP up in the IP ranges of the policy's countries (see [Block or allow countries](https://www.apitraffic.io/docs/waf/country-lists)).

- **egress (object)**:
  Outbound controls: what your application may send, and to where.

**egress**

- **mode (string)**: `off` (default), `detect` or `block`.

- **inspectScope (string)**: Which calls have their payloads inspected: `model` (recognized AI providers and vector stores, the default) or `all`. `inspectDestinations` adds hosts (exact, or `*.suffix`), up to 200.

- **allowedDestinations (string[])**: When set, calls in `allowScope` (`model` or `all`) may only go to these hosts. Up to 200.

- **blockedDestinations (string[])**: Hosts that are always refused. Up to 200.

- **blockSecrets (boolean)**: Detect credentials in outbound payloads. Default `true`.

- **blockBulkPii (boolean)**: Detect bulk personal data, at `bulkPiiThreshold` (default 25) distinct records. Default `false`.

- **maxBodyBytes (integer)**: Refuse outbound payloads larger than this. Default 5242880 (5 MB); `0` turns the cap off.

- **inspectResponses (boolean)**: Scan model and tool responses for indirect prompt injection (detection only). Default `false`.

- **failMode (string)**: `open` (default) or `closed`.

- **behavior (object)**:
  Scoring of client behavior for authorization abuse (object-id enumeration, probing, fan-out), evaluated outside the request path.

**behavior**

- **enabled (boolean)**: Default `false`.

- **action (string)**: `detect` (record findings, the default) or `block` (deny the client on every server).

- **threshold (integer)**: Score at which a client is denied, 1 to 500. Default 70.

- **windowSeconds (integer)**: The window each client is scored over, 30 to 86400. Default 300.

- **denySeconds (integer)**: How long a deny lasts, 30 to 86400. Default 900.

- **enumerationThreshold (integer)**: Distinct object ids on one endpoint before enumeration scores. Default 30.

- **authFailureThreshold (integer)**: 401 and 403 responses in the window before authorization failures score. Default 15.

- **notFoundThreshold (integer)**: 404 responses in the window before id probing scores. Default 40.

- **fanOutThreshold (integer)**: Distinct endpoints in the window before fan-out scores. Default 40.

- **velocityThreshold (integer)**: Requests in the window before volume scores. Default 600.

- **sensitiveVelocityThreshold (integer)**: Requests to sensitive paths before velocity scores. Default 30.

- **sensitivePaths (string[])**: Path prefixes treated as sensitive, for example `/v1/admin`. Up to 100.

- **testCases (object[])**:
  Example requests with the outcome you expect (`expect`: `block` or `allow`), up to 50, run by the [simulator](#simulate-waf-policy). Never sent to SDKs. Each has `id`, `name`, `expect` and `request` (`method`, `url`, `headers`, `body`, `ip`).

- **buckets (string[])**:
  The buckets the policy applies to, or `["*"]`.

- **environments (string[])**:
  The environments the policy applies to, or `["*"]`.

- **configVersion (number)**:
  Goes up with every change. Servers report the version they run, so you can see when a change has reached the fleet.

- **warnings (object[])**:
  Present when older SDK versions enforce part of the policy differently, for example IPv6 egress entries that SDK 0.1.0 and 0.1.1 do not enforce. The policy is saved as sent. A `criteria-rule-sent-disabled` warning names an enabled criteria rule, stored before the API checked such rules, that your SDK can't evaluate as written (for example, it uses a field that isn't known before your API responds, its query doesn't parse, a `like` pattern isn't a valid regular expression, or older SDKs read it differently): the rule is sent to your servers turned off, although it still reads as enabled. A `criteria-or-runs-as-and` warning names an enabled criteria rule saved with ` OR ` between conditions before OR was refused: it runs as AND (every condition must match). A rule carries at most one of the two. Saving a policy refuses a new or changed criteria rule with either problem; an unchanged one (same query, `negate` and `action`) is kept. Country lookups add `country-older-sdks` (lookups by client IP need `@apitraffic/waf` 2.1.0 or later), `country-ranges-too-large` (the policy's countries' IP ranges are more than a policy can carry, so none are sent) and `country-ranges-unavailable` (the country data is not available yet, and the policy names no country header).

- **countryLookup (object)**:
  Whether servers can look countries up by client IP (`available`), and, when they can, whose data the IP ranges are: `provider`, `providerVersion`, `license` and `attribution` (`text` and `url`, the credit to show with it: "IP Geolocation by DB-IP", `https://db-ip.com`).

- **createdAt (string, required)**:
  When the policy was created (ISO 8601).

## List WAF policies

`GET https://api.apitraffic.io/v1/accounts/{accountSid}/waf-policies`

Returns every WAF policy in the account.

### Path parameters

- **accountSid (path, string, required)**:
  The account identifier.

### Response

- **hasMore (boolean, required)**:
  Always `false`.

- **records (object[], required)**:
  The policies, as [WAF policy objects](#the-waf-policy-object).

```bash cURL
curl https://api.apitraffic.io/v1/accounts/acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa/waf-policies \
  -H "Authorization: Bearer $APITRAFFIC_TOKEN"
```

```js Node.js
const accountSid = 'acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa';
const response = await fetch(`https://api.apitraffic.io/v1/accounts/${accountSid}/waf-policies`, {
  headers: { Authorization: `Bearer ${process.env.APITRAFFIC_TOKEN}` },
});
const { records } = await response.json();
```

```json 200
{
  "hasMore": false,
  "records": [
    {
      "sid": "waf_2qR5sT7uV9wX1yZ3aB6cD8eF4gH",
      "accountSid": "acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa",
      "description": "Acme Store WAF",
      "enabled": true,
      "mode": "block",
      "paranoiaLevel": 1,
      "anomalyThreshold": 5,
      "failMode": "open",
      "defaultBlock": { "status": 403, "body": "Blocked by ApiTraffic WAF" },
      "managedRuleset": { "id": "apitraffic-core", "version": "latest", "enabled": true },
      "managedRulesetStatus": { "id": "apitraffic-core", "version": "latest", "resolvedVersion": "1.1.2", "name": "ApiTraffic Core Rule Set", "enabled": true, "activeRuleCount": 212 },
      "disabledRules": [],
      "ruleExclusions": [],
      "rules": [],
      "lists": [
        { "id": "known-attackers", "kind": "ip", "effect": "deny", "values": ["203.0.113.66"], "message": "Known credential-stuffing source" }
      ],
      "rateLimits": [
        { "id": "login-per-ip", "by": "ip", "pathPattern": "^/v1/auth/login$", "limit": 10, "windowSeconds": 60, "mode": "local", "status": 429, "scope": "instance", "message": "Too many sign-in attempts from this address" }
      ],
      "limits": {},
      "geo": {},
      "egress": { "mode": "off" },
      "behavior": { "enabled": false },
      "testCases": [],
      "buckets": ["*"],
      "environments": ["*"],
      "configVersion": 7,
      "createdAt": "2026-03-02T16:20:00.000Z"
    }
  ]
}
```

## Get WAF policy

`GET https://api.apitraffic.io/v1/accounts/{accountSid}/waf-policies/{wafSid}`

Returns one WAF policy.

### Path parameters

- **accountSid (path, string, required)**:
  The account identifier.

- **wafSid (path, string, required)**:
  The policy's identifier (`waf_…`).

### Response

A [WAF policy object](#the-waf-policy-object).

```bash cURL
curl https://api.apitraffic.io/v1/accounts/acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa/waf-policies/waf_2qR5sT7uV9wX1yZ3aB6cD8eF4gH \
  -H "Authorization: Bearer $APITRAFFIC_TOKEN"
```

```js Node.js
const accountSid = 'acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa';
const wafSid = 'waf_2qR5sT7uV9wX1yZ3aB6cD8eF4gH';
const response = await fetch(`https://api.apitraffic.io/v1/accounts/${accountSid}/waf-policies/${wafSid}`, {
  headers: { Authorization: `Bearer ${process.env.APITRAFFIC_TOKEN}` },
});
const policy = await response.json();
```

```json 200
{
  "sid": "waf_2qR5sT7uV9wX1yZ3aB6cD8eF4gH",
  "accountSid": "acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa",
  "description": "Acme Store WAF",
  "enabled": true,
  "mode": "block",
  "paranoiaLevel": 1,
  "anomalyThreshold": 5,
  "failMode": "open",
  "managedRuleset": { "id": "apitraffic-core", "version": "latest", "enabled": true },
  "buckets": ["*"],
  "environments": ["*"],
  "configVersion": 7,
  "createdAt": "2026-03-02T16:20:00.000Z"
}
```

The example shortens the policy; see [List WAF policies](#list-waf-policies) for a full one.

## Create WAF policy

`POST https://api.apitraffic.io/v1/accounts/{accountSid}/waf-policies`

Creates a WAF policy. Every field of the [WAF policy object](#the-waf-policy-object) except `sid`, `accountSid`, `managedRulesetStatus`, `configVersion`, `warnings`, `countryLookup` and `createdAt` can be sent; anything you leave out takes its default. Start with `mode: "detect"`, check what it would block in [Get firewall overview](#get-firewall-overview), then switch to `block`.

**Permissions:** owner or admin, or an `administrator` API token. **Limit:** past the plan's policy limit, the API answers `409` with code `2213`.

### Path parameters

- **accountSid (path, string, required)**:
  The account identifier.

### Body parameters

- **mode (body, string, default detect)**:
  `off`, `detect` or `block`.

- **buckets (body, string[])**:
  Bucket sids, or `["*"]`.

- **environments (body, string[])**:
  Environment sids, or `["*"]`.

- **description (body, string | null)**:
  A short description.

- **… (body, various)**:
  Any other policy field, as described in [The WAF policy object](#the-waf-policy-object). Custom patterns (in rules, lists and exclusions) are checked when you save: they must compile and must not be able to backtrack catastrophically.

### Response

The new [WAF policy object](#the-waf-policy-object).

```bash cURL
curl -X POST https://api.apitraffic.io/v1/accounts/acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa/waf-policies \
  -H "Authorization: Bearer $APITRAFFIC_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Acme Store WAF",
    "mode": "detect",
    "paranoiaLevel": 1,
    "anomalyThreshold": 5,
    "rateLimits": [
      { "id": "login-per-ip", "by": "ip", "pathPattern": "^/v1/auth/login$", "limit": 10, "windowSeconds": 60, "status": 429 }
    ],
    "buckets": ["k3m9p2x"],
    "environments": ["prd7k2"]
  }'
```

```js Node.js
const accountSid = 'acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa';
const response = await fetch(`https://api.apitraffic.io/v1/accounts/${accountSid}/waf-policies`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.APITRAFFIC_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    description: 'Acme Store WAF',
    mode: 'detect',
    rateLimits: [
      { id: 'login-per-ip', by: 'ip', pathPattern: '^/v1/auth/login$', limit: 10, windowSeconds: 60, status: 429 },
    ],
    buckets: ['k3m9p2x'],
    environments: ['prd7k2'],
  }),
});
const policy = await response.json();
```

```json 200
{
  "sid": "waf_2rS6tU8vW1xY3zA5bC7dE9fG2hJ",
  "accountSid": "acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa",
  "description": "Acme Store WAF",
  "enabled": true,
  "mode": "detect",
  "paranoiaLevel": 1,
  "anomalyThreshold": 5,
  "failMode": "open",
  "defaultBlock": { "status": 403, "body": "Forbidden" },
  "managedRuleset": { "id": "apitraffic-core", "version": "latest", "enabled": true },
  "rateLimits": [
    { "id": "login-per-ip", "by": "ip", "pathPattern": "^/v1/auth/login$", "limit": 10, "windowSeconds": 60, "mode": "distributed", "status": 429, "scope": "instance" }
  ],
  "buckets": ["k3m9p2x"],
  "environments": ["prd7k2"],
  "configVersion": 1,
  "createdAt": "2026-10-20T12:00:00.000Z"
}
```

The example shortens the response.

## Update WAF policy

`PUT https://api.apitraffic.io/v1/accounts/{accountSid}/waf-policies/{wafSid}`

Replaces a WAF policy. Send the whole policy, as for [Create WAF policy](#create-waf-policy): fields you leave out return to their defaults, except `geo` and `testCases`, which keep their values when omitted. The new version reaches connected SDKs without a restart, and `configVersion` goes up.

**Permissions:** owner or admin, or an `administrator` API token.

### Path parameters

- **accountSid (path, string, required)**:
  The account identifier.

- **wafSid (path, string, required)**:
  The policy's identifier.

### Body parameters

The same as [Create WAF policy](#create-waf-policy).

### Response

The updated [WAF policy object](#the-waf-policy-object).

```bash cURL
curl -X PUT https://api.apitraffic.io/v1/accounts/acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa/waf-policies/waf_2rS6tU8vW1xY3zA5bC7dE9fG2hJ \
  -H "Authorization: Bearer $APITRAFFIC_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Acme Store WAF",
    "mode": "block",
    "rateLimits": [
      { "id": "login-per-ip", "by": "ip", "pathPattern": "^/v1/auth/login$", "limit": 10, "windowSeconds": 60, "status": 429 }
    ],
    "buckets": ["k3m9p2x"],
    "environments": ["prd7k2"]
  }'
```

```js Node.js
const accountSid = 'acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa';
const wafSid = 'waf_2rS6tU8vW1xY3zA5bC7dE9fG2hJ';
const current = await (await fetch(`https://api.apitraffic.io/v1/accounts/${accountSid}/waf-policies/${wafSid}`, {
  headers: { Authorization: `Bearer ${process.env.APITRAFFIC_TOKEN}` },
})).json();
// Send back every field you can set, changing only the mode.
const { sid, accountSid: _account, managedRulesetStatus, configVersion, warnings, countryLookup, createdAt, ...policy } = current;
const response = await fetch(`https://api.apitraffic.io/v1/accounts/${accountSid}/waf-policies/${wafSid}`, {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${process.env.APITRAFFIC_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ ...policy, mode: 'block' }),
});
const updated = await response.json();
```

```json 200
{
  "sid": "waf_2rS6tU8vW1xY3zA5bC7dE9fG2hJ",
  "accountSid": "acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa",
  "description": "Acme Store WAF",
  "enabled": true,
  "mode": "block",
  "buckets": ["k3m9p2x"],
  "environments": ["prd7k2"],
  "configVersion": 2,
  "createdAt": "2026-10-20T12:00:00.000Z"
}
```

The example shortens the response.

## Delete WAF policy

`DELETE https://api.apitraffic.io/v1/accounts/{accountSid}/waf-policies/{wafSid}`

Deletes a WAF policy. Servers stop enforcing it when they next receive their configuration.

**Permissions:** owner or admin, or an `administrator` API token.

### Path parameters

- **accountSid (path, string, required)**:
  The account identifier.

- **wafSid (path, string, required)**:
  The policy's identifier.

### Response

- **sid (string, required)**:
  The deleted policy's identifier.

- **deleted (boolean, required)**:
  `true`.

```bash cURL
curl -X DELETE https://api.apitraffic.io/v1/accounts/acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa/waf-policies/waf_2rS6tU8vW1xY3zA5bC7dE9fG2hJ \
  -H "Authorization: Bearer $APITRAFFIC_TOKEN"
```

```js Node.js
const accountSid = 'acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa';
const wafSid = 'waf_2rS6tU8vW1xY3zA5bC7dE9fG2hJ';
await fetch(`https://api.apitraffic.io/v1/accounts/${accountSid}/waf-policies/${wafSid}`, {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${process.env.APITRAFFIC_TOKEN}` },
});
```

```json 200
{
  "sid": "waf_2rS6tU8vW1xY3zA5bC7dE9fG2hJ",
  "deleted": true
}
```

## Simulate WAF policy

`POST https://api.apitraffic.io/v1/accounts/{accountSid}/waf-policies/simulate`

Evaluates a policy, saved or not, against up to 250 requests and says what it would do with each. Nothing is saved or sent to servers. Pass `baselineWafSid` to compare against a saved policy and see which requests a change would newly block or let through. Rate limits are not counted in a simulation.

**Plan:** needs the **Web application firewall**.

### Path parameters

- **accountSid (path, string, required)**:
  The account identifier.

### Body parameters

- **policy (body, object, required)**:
  The policy to evaluate, with the same fields as [Create WAF policy](#create-waf-policy).

- **requests (body, object[], required)**:
  1 to 250 requests: `method` (default `GET`), `url` (path and query string, for example `/v1/search?q=1`), `headers`, `body` (raw; JSON is parsed when the content type says so), `ip` (for IP lists) and `id` (echoed back).

- **baselineWafSid (body, string)**:
  A saved policy to compare with.

### Response

- **summary (object, required)**:
  `evaluated`, `notEvaluated`, `blocked`, `matched` (rules matched, below the threshold), `allowed`, `newlyBlocked` and `noLongerBlocked` (with a baseline, otherwise `null`), and `truncated` (`true` when the time limit stopped the run early).

- **results (object[], required)**:
  One entry per evaluated request: `id`, `method`, `path`, `verdict` (`block`, `match` or `allow`), `score`, `threshold`, `ruleId` and `reason` (what decided it), `status` (the block response status), `matches` (each rule that matched, with `ruleId`, `category`, `message`, `severity`, `score`, `target`, `data`, `source` and `action`), and with a baseline, `baseline` (`verdict`, `score`, `ruleId`) and `changed`.

```bash cURL
curl -X POST https://api.apitraffic.io/v1/accounts/acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa/waf-policies/simulate \
  -H "Authorization: Bearer $APITRAFFIC_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "policy": { "mode": "block", "paranoiaLevel": 2 },
    "baselineWafSid": "waf_2qR5sT7uV9wX1yZ3aB6cD8eF4gH",
    "requests": [
      { "id": "traversal", "url": "/v1/products/..%2F..%2F..%2F..%2Fetc%2Fpasswd" },
      { "id": "normal", "url": "/v1/search?q=running+shoes" }
    ]
  }'
```

```js Node.js
const accountSid = 'acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa';
const response = await fetch(`https://api.apitraffic.io/v1/accounts/${accountSid}/waf-policies/simulate`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.APITRAFFIC_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    policy: { mode: 'block', paranoiaLevel: 2 },
    baselineWafSid: 'waf_2qR5sT7uV9wX1yZ3aB6cD8eF4gH',
    requests: [
      { id: 'traversal', url: '/v1/products/..%2F..%2F..%2F..%2Fetc%2Fpasswd' },
      { id: 'normal', url: '/v1/search?q=running+shoes' },
    ],
  }),
});
const { summary, results } = await response.json();
```

```json 200
{
  "summary": {
    "evaluated": 2,
    "notEvaluated": 0,
    "blocked": 1,
    "matched": 0,
    "allowed": 1,
    "newlyBlocked": 0,
    "noLongerBlocked": 0,
    "truncated": false
  },
  "results": [
    {
      "id": "traversal",
      "method": "GET",
      "path": "/v1/products/..%2F..%2F..%2F..%2Fetc%2Fpasswd",
      "verdict": "block",
      "score": 10,
      "threshold": 5,
      "ruleId": "AT-LFI-001",
      "reason": "anomaly-threshold",
      "status": 403,
      "matches": [
        { "ruleId": "AT-LFI-001", "category": "lfi", "message": "Path traversal detected in the request path", "severity": 2, "score": 5, "target": "path", "data": "../", "source": "managed", "action": "score" }
      ],
      "baseline": { "verdict": "block", "score": 10, "ruleId": "AT-LFI-001" },
      "changed": false
    },
    {
      "id": "normal",
      "method": "GET",
      "path": "/v1/search",
      "verdict": "allow",
      "score": 0,
      "threshold": 5,
      "ruleId": null,
      "reason": null,
      "status": null,
      "matches": [],
      "baseline": { "verdict": "allow", "score": 0, "ruleId": null },
      "changed": false
    }
  ]
}
```

## List managed rulesets

`GET https://api.apitraffic.io/v1/accounts/{accountSid}/waf-rulesets`

Returns the managed rulesets a policy can use.

**Plan:** needs the **Web application firewall**.

### Path parameters

- **accountSid (path, string, required)**:
  The account identifier.

### Response

- **hasMore (boolean, required)**:
  Always `false`.

- **records (object[], required)**:
  One entry per ruleset: `id`, `version`, `name`, `description`, `released`, `total` (rules), `byCategory` and `byParanoia` (rule counts).

```bash cURL
curl https://api.apitraffic.io/v1/accounts/acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa/waf-rulesets \
  -H "Authorization: Bearer $APITRAFFIC_TOKEN"
```

```js Node.js
const accountSid = 'acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa';
const response = await fetch(`https://api.apitraffic.io/v1/accounts/${accountSid}/waf-rulesets`, {
  headers: { Authorization: `Bearer ${process.env.APITRAFFIC_TOKEN}` },
});
const { records } = await response.json();
```

```json 200
{
  "hasMore": false,
  "records": [
    {
      "id": "apitraffic-core",
      "version": "1.1.2",
      "name": "ApiTraffic Core Rule Set",
      "description": "Managed protection for the OWASP Top 10 injection families: SQLi, XSS, OS command execution, path traversal/LFI/RFI, SSRF, template injection, NoSQL injection, XXE, plus scanner, probe and protocol hygiene rules.",
      "released": "2026-10-07",
      "total": 240,
      "byCategory": { "sqli": 48, "xss": 41, "lfi": 22 },
      "byParanoia": { "1": 140, "2": 62, "3": 28, "4": 10 }
    }
  ]
}
```

## Get managed ruleset

`GET https://api.apitraffic.io/v1/accounts/{accountSid}/waf-rulesets/{rulesetId}`

Returns a managed ruleset with every rule in it, so you can choose rules to turn off or exclude.

**Plan:** needs the **Web application firewall**.

### Path parameters

- **accountSid (path, string, required)**:
  The account identifier.

- **rulesetId (path, string, required)**:
  The ruleset id, for example `apitraffic-core`.

### Query parameters

- **version (query, string)**:
  The version. Defaults to the newest.

### Response

The ruleset's fields, as in [List managed rulesets](#list-managed-rulesets), plus `rules`: each with `id`, `category`, `message`, `severity`, `score`, `paranoia`, `action`, `targets` and `tags`.

```bash cURL
curl https://api.apitraffic.io/v1/accounts/acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa/waf-rulesets/apitraffic-core \
  -H "Authorization: Bearer $APITRAFFIC_TOKEN"
```

```js Node.js
const accountSid = 'acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa';
const response = await fetch(`https://api.apitraffic.io/v1/accounts/${accountSid}/waf-rulesets/apitraffic-core`, {
  headers: { Authorization: `Bearer ${process.env.APITRAFFIC_TOKEN}` },
});
const ruleset = await response.json();
```

```json 200
{
  "id": "apitraffic-core",
  "version": "1.1.2",
  "name": "ApiTraffic Core Rule Set",
  "description": "Managed protection for the OWASP Top 10 injection families: SQLi, XSS, OS command execution, path traversal/LFI/RFI, SSRF, template injection, NoSQL injection, XXE, plus scanner, probe and protocol hygiene rules.",
  "released": "2026-10-07",
  "total": 240,
  "byCategory": { "sqli": 48, "xss": 41, "lfi": 22 },
  "byParanoia": { "1": 140, "2": 62, "3": 28, "4": 10 },
  "rules": [
    { "id": "AT-LFI-001", "category": "lfi", "message": "Path traversal detected in the request path", "severity": 2, "score": 5, "paranoia": 1, "action": "score", "targets": ["path", "args"], "tags": [] }
  ]
}
```

The examples are shortened, and the rule counts are illustrative.

## Firewall analytics filters

The four analytics operations below take the same filters:

- **startTime (query, string, required)**:
  Start of the range (ISO 8601, UTC, ending in `Z`).

- **endTime (query, string, required)**:
  End of the range.

- **environmentSid (query, string)**:
  Only this environment.

- **bucketSid (query, string)**:
  Only this bucket.

- **policySid (query, string)**:
  Only detections made by this policy (the policy that actually ran).

- **ruleId (query, string)**:
  Only detections this rule contributed to.

- **category (query, string)**:
  Only detections whose deciding rule is in this category.

- **action (query, string)**:
  `tag` (matched below the threshold), `would-block` (would have been blocked in `detect` mode) or `block`.

- **endpoint (query, string)**:
  Only this endpoint. Also narrows the inspected traffic, so `matchRate` stays a share of that endpoint's own requests.

## Get firewall overview

`GET https://api.apitraffic.io/v1/accounts/{accountSid}/waf-analytics/overview`

Returns firewall totals, a time series, the score distribution and the busiest rules.

### Path parameters

- **accountSid (path, string, required)**:
  The account identifier.

### Query parameters

The [analytics filters](#firewall-analytics-filters), plus:

- **ruleLimit (query, integer, default 10)**:
  Rules in `rules`, 1 to 1000.

### Response

- **summary (object, required)**:
  `matchedRequests`, `blockedRequests` (actually refused), `wouldBlockRequests` (would have been, if enforcing), `taggedRequests` (matched below the threshold), `ruleMatches`, `distinctRules`, `distinctCategories`, `distinctEndpoints`, `averageScore`, `maxScore`, `p95Score`, `firstSeen`, `lastSeen`, `inspectedRequests` (all inbound requests in the range), `matchRate` (percent of inspected requests that matched) and `enforcementRate` (percent of matches that blocked or would have).

- **timeseries (object[], required)**:
  Per period: `bucket`, the request counts above, `ruleMatches`, `averageScore` and `maxScore`.

- **scores (object[], required)**:
  The score distribution: `score`, `matchedRequests`, `blockedRequests` and `wouldBlockRequests`.

- **rules (object[], required)**:
  The busiest rules, as in [List firewall rule hits](#list-firewall-rule-hits).

- **degraded (boolean)**:
  Present and `true` when part of the answer could not be read; `failed` names the parts (`timeseries`, `scores`, `rules`).

```bash cURL
curl -G https://api.apitraffic.io/v1/accounts/acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa/waf-analytics/overview \
  -H "Authorization: Bearer $APITRAFFIC_TOKEN" \
  --data-urlencode 'startTime=2026-10-19T00:00:00.000Z' \
  --data-urlencode 'endTime=2026-10-20T00:00:00.000Z'
```

```js Node.js
const accountSid = 'acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa';
const params = new URLSearchParams({ startTime: '2026-10-19T00:00:00.000Z', endTime: '2026-10-20T00:00:00.000Z' });
const response = await fetch(`https://api.apitraffic.io/v1/accounts/${accountSid}/waf-analytics/overview?${params}`, {
  headers: { Authorization: `Bearer ${process.env.APITRAFFIC_TOKEN}` },
});
const { summary } = await response.json();
```

```json 200
{
  "summary": {
    "matchedRequests": 318,
    "blockedRequests": 296,
    "wouldBlockRequests": 0,
    "taggedRequests": 22,
    "ruleMatches": 604,
    "distinctRules": 19,
    "distinctCategories": 5,
    "distinctEndpoints": 7,
    "averageScore": 9.4,
    "maxScore": 35,
    "p95Score": 20,
    "firstSeen": "2026-10-19T00:12:40.000Z",
    "lastSeen": "2026-10-19T23:51:07.000Z",
    "inspectedRequests": 10412,
    "matchRate": 3.05,
    "enforcementRate": 93.08
  },
  "timeseries": [],
  "scores": [],
  "rules": []
}
```

## List firewall rule hits

`GET https://api.apitraffic.io/v1/accounts/{accountSid}/waf-analytics/rules`

Ranks rules by the requests they fired on, with how often each one decided the outcome. Use it to find rules worth tuning.

### Path parameters

- **accountSid (path, string, required)**:
  The account identifier.

### Query parameters

The [analytics filters](#firewall-analytics-filters), plus `limit` (1 to 1000, default 50).

### Response

- **points (object[], required)**:
  One entry per rule: `ruleId`, `category`, `message`, `severity`, `paranoia`, `ruleScore` (score per hit), `ruleAction`, `matchedRequests`, `blockedRequests`, `wouldBlockRequests`, `taggedRequests`, `decidedRequests` (requests where this rule decided the outcome), `averageScore`, `maxScore`, `distinctEndpoints`, `firstSeen` and `lastSeen`.

```bash cURL
curl -G https://api.apitraffic.io/v1/accounts/acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa/waf-analytics/rules \
  -H "Authorization: Bearer $APITRAFFIC_TOKEN" \
  --data-urlencode 'startTime=2026-10-19T00:00:00.000Z' \
  --data-urlencode 'endTime=2026-10-20T00:00:00.000Z' \
  --data-urlencode 'limit=10'
```

```js Node.js
const accountSid = 'acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa';
const params = new URLSearchParams({ startTime: '2026-10-19T00:00:00.000Z', endTime: '2026-10-20T00:00:00.000Z', limit: '10' });
const response = await fetch(`https://api.apitraffic.io/v1/accounts/${accountSid}/waf-analytics/rules?${params}`, {
  headers: { Authorization: `Bearer ${process.env.APITRAFFIC_TOKEN}` },
});
const { points } = await response.json();
```

```json 200
{
  "points": [
    {
      "ruleId": "AT-LFI-001",
      "category": "lfi",
      "message": "Path traversal detected in the request path",
      "severity": 2,
      "paranoia": 1,
      "ruleScore": 5,
      "ruleAction": "score",
      "matchedRequests": 74,
      "blockedRequests": 74,
      "wouldBlockRequests": 0,
      "taggedRequests": 0,
      "decidedRequests": 61,
      "averageScore": 12.3,
      "maxScore": 20,
      "distinctEndpoints": 2,
      "firstSeen": "2026-10-19T00:31:02.000Z",
      "lastSeen": "2026-10-19T23:40:19.000Z"
    }
  ]
}
```

## Get firewall breakdown

`GET https://api.apitraffic.io/v1/accounts/{accountSid}/waf-analytics/breakdown`

Groups firewall matches by one dimension, such as category, endpoint or policy.

### Path parameters

- **accountSid (path, string, required)**:
  The account identifier.

### Query parameters

The [analytics filters](#firewall-analytics-filters), plus:

- **dimension (query, string, required)**:
  `category`, `action`, `reason`, `severity`, `mode`, `policy`, `endpoint`, `path`, `method`, `statusCode`, `bucketSid` or `environmentSid`.

- **limit (query, integer, default 25)**:
  Groups to return, 1 to 1000.

### Response

- **dimension (string, required)**:
  The dimension used.

- **points (object[], required)**:
  One entry per group: `key`, `matchedRequests`, `blockedRequests`, `wouldBlockRequests`, `taggedRequests`, `distinctRules` and `lastSeen`.

```bash cURL
curl -G https://api.apitraffic.io/v1/accounts/acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa/waf-analytics/breakdown \
  -H "Authorization: Bearer $APITRAFFIC_TOKEN" \
  --data-urlencode 'dimension=reason' \
  --data-urlencode 'startTime=2026-10-19T00:00:00.000Z' \
  --data-urlencode 'endTime=2026-10-20T00:00:00.000Z'
```

```js Node.js
const accountSid = 'acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa';
const params = new URLSearchParams({
  dimension: 'reason',
  startTime: '2026-10-19T00:00:00.000Z',
  endTime: '2026-10-20T00:00:00.000Z',
});
const response = await fetch(`https://api.apitraffic.io/v1/accounts/${accountSid}/waf-analytics/breakdown?${params}`, {
  headers: { Authorization: `Bearer ${process.env.APITRAFFIC_TOKEN}` },
});
const { points } = await response.json();
```

```json 200
{
  "dimension": "reason",
  "points": [
    { "key": "anomaly-threshold", "matchedRequests": 211, "blockedRequests": 189, "wouldBlockRequests": 0, "taggedRequests": 22, "distinctRules": 17, "lastSeen": "2026-10-19T23:51:07.000Z" },
    { "key": "rate-limit", "matchedRequests": 84, "blockedRequests": 84, "wouldBlockRequests": 0, "taggedRequests": 0, "distinctRules": 1, "lastSeen": "2026-10-19T03:24:55.000Z" },
    { "key": "ip-deny", "matchedRequests": 23, "blockedRequests": 23, "wouldBlockRequests": 0, "taggedRequests": 0, "distinctRules": 1, "lastSeen": "2026-10-19T03:22:10.000Z" }
  ]
}
```

## List firewall matches

`GET https://api.apitraffic.io/v1/accounts/{accountSid}/waf-analytics/matches`

Returns the matched requests themselves, newest first, so you can open them with [Get request](https://www.apitraffic.io/docs/api-reference/requests#get-request).

### Path parameters

- **accountSid (path, string, required)**:
  The account identifier.

### Query parameters

The [analytics filters](#firewall-analytics-filters), plus `limit` (1 to 1000, default 50).

### Response

- **points (object[], required)**:
  One entry per request: `dt`, `sid` (the request), `method`, `path`, `endpoint`, `statusCode`, `durationMs`, `score`, `action`, `reason`, `ruleId` (the deciding rule), `category`, `ruleIds` (every rule that contributed), `bucketSid` and `environmentSid`.

```bash cURL
curl -G https://api.apitraffic.io/v1/accounts/acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa/waf-analytics/matches \
  -H "Authorization: Bearer $APITRAFFIC_TOKEN" \
  --data-urlencode 'action=block' \
  --data-urlencode 'startTime=2026-10-19T03:00:00.000Z' \
  --data-urlencode 'endTime=2026-10-19T04:00:00.000Z'
```

```js Node.js
const accountSid = 'acc_2mXq7RbT4kPz9WvN3cHd8LsYfQa';
const params = new URLSearchParams({
  action: 'block',
  startTime: '2026-10-19T03:00:00.000Z',
  endTime: '2026-10-19T04:00:00.000Z',
});
const response = await fetch(`https://api.apitraffic.io/v1/accounts/${accountSid}/waf-analytics/matches?${params}`, {
  headers: { Authorization: `Bearer ${process.env.APITRAFFIC_TOKEN}` },
});
const { points } = await response.json();
```

```json 200
{
  "points": [
    {
      "dt": "2026-10-19T03:21:44.000Z",
      "sid": "req_2lM7nP9qR2sT4uV6wX8yZ1aB3cD",
      "method": "POST",
      "path": "/v1/auth/login",
      "endpoint": "/v1/auth/login",
      "statusCode": 429,
      "durationMs": 1,
      "score": 0,
      "action": "block",
      "reason": "rate-limit",
      "ruleId": "login-per-ip",
      "category": "rate-limit",
      "ruleIds": ["login-per-ip"],
      "bucketSid": "k3m9p2x",
      "environmentSid": "prd7k2"
    }
  ]
}
```

## Block responses

A [block response](https://www.apitraffic.io/docs/waf/block-responses) is what a request blocked by a policy receives, shared by the policies that name it with `blockResponseSid`. Changing one updates and pushes every policy that uses it.

`GET https://api.apitraffic.io/v1/accounts/{accountSid}/waf-block-responses`

`POST https://api.apitraffic.io/v1/accounts/{accountSid}/waf-block-responses`

`GET https://api.apitraffic.io/v1/accounts/{accountSid}/waf-block-responses/{blockResponseSid}`

`PUT https://api.apitraffic.io/v1/accounts/{accountSid}/waf-block-responses/{blockResponseSid}`

`DELETE https://api.apitraffic.io/v1/accounts/{accountSid}/waf-block-responses/{blockResponseSid}`

**Permissions:** reading needs any account role; creating, changing and deleting need owner or admin, or an `administrator` API token. Creating and changing need a plan that includes the firewall; reading and deleting do not.

### The block response object

- **sid (string, required)**:
  The block response's identifier (`wbr_…`).

- **name (string, required)**:
  A name to pick it by, up to 100 characters.

- **description (string)**:
  What it is for, up to 500 characters.

- **status (integer, required)**:
  The HTTP status, 200 to 599 except `204`, `205` and `304` (default `403`).

- **contentType (string, required)**:
  `auto` (JSON when the body is a JSON object or array, else plain text), `json`, `problem+json` (RFC 9457) or `text`. A JSON type needs a body of that shape.

- **body (string, required)**:
  1 to 4,096 bytes, without `<`. Each `{context_id}` is replaced, as a block is sent, with the blocked request's context id (`ctx_…`, 31 characters, counted towards the limit), which finds the request with `context_sid == ctx_…`.

- **headers (object, required)**:
  Only `Retry-After`, `Cache-Control` and `RateLimit-*`, each once, with a value.

- **usedBy (object[], required)**:
  The policies that use it: `sid` and `description`.

- **createdAt (string, required)**:
  When it was created.

- **updatedAt (string | null, required)**:
  When it last changed. Send it back as `expectedUpdatedAt` on an update to have the update refused if someone changed the response since.

A create takes `name`, `description`, `status`, `contentType`, `body` and `headers`. An update (`PUT`) changes only the fields it sends, plus the optional `expectedUpdatedAt`. A delete is refused while a policy uses the response, and answers `sid` and `deleted`.

## Errors

| Status | Code | Meaning |
| --- | --- | --- |
| `400` | `1000` | A field is missing or invalid, for example an unknown `mode`, a custom pattern that is unsafe, or more than 250 requests to simulate. The message names the field. |
| `403` | `2010` | Creating, changing or deleting a policy without owner, admin or `administrator` access. |
| `403` | `2221` | The plan does not include the firewall. |
| `403` | `2509` | Changing a policy for the read-only sample bucket. |
| `404` | `4000` | No policy or ruleset with that id. |
| `409` | `2213` | The plan's WAF policy limit is reached. |
| `400` | `2684` | A policy names a block response that does not exist (or was deleted meanwhile). Pick another one. |
| `409` | `2680` | A block response in use cannot be deleted; `data.usedBy` lists the policies. Also returned when policies are moving to and from it at that moment: try again. |
| `409` | `2682` | The account keeps the most block responses it can (100). |
| `409` | `2683` | A block response update was refused: it changed since `expectedUpdatedAt` (`data.updatedAt` is the stored version), or other saves kept changing it. Load it again and retry. |
| `503` | `2681` | A block response was saved, but not every policy using it reached your servers. Save it again (with `data.updatedAt` as `expectedUpdatedAt`) to retry. |
| `503`, `504` | `5101` to `5104` | Analytics could not be read right now. Retry after `Retry-After` seconds. |

## Related

- [Firewall policies](https://www.apitraffic.io/docs/waf/policies):
    Build and tune a policy in the app.

- [Set up the firewall](https://www.apitraffic.io/docs/waf/setup):
    Turn on enforcement in your SDK.
