# Site security

For sites on Cloudflare: "I'm Under Attack" mode, the site's own firewall rules and blocking AI crawlers.

## Security settings of a site

`GET /sites/{site_id}/security` · scope `security:read`

The same as the site's Security tab: whether "I'm Under Attack" is on (and when it switches itself off), the site's firewall rules and rate-limiting rules with the Cloudflare plan's limits, bot protection (AI crawlers blocked, bot fight mode), and the fields, operators and actions a new rule may use. 409 for a site that is not on Cloudflare.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/sites/123/security" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/sites/123/security", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/security", {
  method: "GET",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "site_id": 123,
    "under_attack": {
      "available": true,
      "on": false,
      "external": false,
      "auto_off_at": null,
      "enabled_at": null,
      "level": "medium",
      "error": ""
    },
    "auto_off_choices": [
      "1h",
      "6h",
      "24h",
      "never"
    ],
    "bot_protection": {
      "ai": true,
      "fight": false,
      "status": "active",
      "label": "Active"
    },
    "rules": {
      "status": "ready",
      "plan": "free",
      "limits": {
        "custom": 5,
        "ratelimit": 1
      },
      "http_request_firewall_custom": [
        {
          "id": "a1b2",
          "name": "Block bad country",
          "action": "block",
          "enabled": true,
          "expression": "(ip.src.country in {\"XX\"})"
        }
      ],
      "http_ratelimit": []
    },
    "fields": {
      "country": {
        "label": "Country",
        "ops": [
          "in",
          "not_in"
        ]
      }
    },
    "ratelimit_fields": [
      "path",
      "method",
      "wp_login",
      "wp_admin",
      "verified_bot"
    ],
    "actions": {
      "http_request_firewall_custom": {
        "block": "Block"
      },
      "http_ratelimit": {
        "block": "Block"
      }
    },
    "login_path": "/wp-login.php",
    "changes_paused": false
  }
}
```

Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`

## "I'm Under Attack" on or off

`PUT /sites/{site_id}/security/under-attack` · scope `security:write`

Every visitor gets a short browser check before the site loads while it is on - use it while the site is being hammered, then switch it off.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `on` | body | boolean | yes | true = on, false = off. |
| `auto_off` | body | string (one of: 1h, 6h, 24h, never) | no | When switching on: switch it off again by itself after this long. Default: `never`. |

### Example

```bash
curl -s -X PUT "https://app.pbn.ltd/api/v1/sites/123/security/under-attack" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.put("https://app.pbn.ltd/api/v1/sites/123/security/under-attack", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/security/under-attack", {
  method: "PUT",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "available": true,
    "on": false,
    "external": false,
    "auto_off_at": null,
    "enabled_at": null,
    "level": "medium",
    "error": ""
  }
}
```

Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `unavailable`, `validation_failed`

## Change bot protection

`PUT /sites/{site_id}/security/bots` · scope `security:write`

The same as the two bot switches on the Security tab. The change is saved at once and applied at Cloudflare within a few minutes (`status` reads "applying", then "active").

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `setting` | body | string (one of: ai, fight) | yes | "ai" = block the AI crawlers; "fight" = Cloudflare bot fight mode. |
| `on` | body | boolean | yes | true = on, false = off. |

### Example

```bash
curl -s -X PUT "https://app.pbn.ltd/api/v1/sites/123/security/bots" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.put("https://app.pbn.ltd/api/v1/sites/123/security/bots", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/security/bots", {
  method: "PUT",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "ai": true,
    "fight": false,
    "status": "active",
    "label": "Active"
  }
}
```

Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`

## Add a firewall rule

`POST /sites/{site_id}/security/rules` · scope `security:write`

Adds a firewall rule (or a rate-limiting rule) to the site at Cloudflare, within the plan's limit. Returns every rule of the site.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `rule` | body | object | yes | The rule, exactly as the tab builds it: {"phase": "http_request_firewall_custom" (default) or "http_ratelimit", "name": "...", "action": one of `actions[phase]`, "enabled": true, and EITHER "builder": {"match": "all"|"any", "conditions": [{"field": one of `fields`, "op": one of that field's ops, "value": "..." }]} OR "mode": "advanced" with "expression": a Cloudflare rule expression; a rate-limiting rule also takes "requests_per_period" (per 10 seconds per visitor)}. The same checks as the tab apply. |

### Example

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/security/rules" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/sites/123/security/rules", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/security/rules", {
  method: "POST",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`201`

```json
{
  "data": {
    "status": "ready",
    "plan": "free",
    "limits": {
      "custom": 5,
      "ratelimit": 1
    },
    "http_request_firewall_custom": [
      {
        "id": "a1b2",
        "name": "Block bad country",
        "action": "block",
        "enabled": true,
        "expression": "(ip.src.country in {\"XX\"})"
      }
    ],
    "http_ratelimit": []
  }
}
```

Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `unavailable`, `validation_failed`

## Change a firewall rule

`PUT /sites/{site_id}/security/rules/{rule_id}` · scope `security:write`

Replaces one rule (same shape as when adding it). Only rules made on this dashboard can be changed here.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `rule_id` | path | string | yes | The rule id (see GET .../security). |
| `rule` | body | object | yes | The rule, exactly as the tab builds it: {"phase": "http_request_firewall_custom" (default) or "http_ratelimit", "name": "...", "action": one of `actions[phase]`, "enabled": true, and EITHER "builder": {"match": "all"|"any", "conditions": [{"field": one of `fields`, "op": one of that field's ops, "value": "..." }]} OR "mode": "advanced" with "expression": a Cloudflare rule expression; a rate-limiting rule also takes "requests_per_period" (per 10 seconds per visitor)}. The same checks as the tab apply. |

### Example

```bash
curl -s -X PUT "https://app.pbn.ltd/api/v1/sites/123/security/rules/rule_id" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.put("https://app.pbn.ltd/api/v1/sites/123/security/rules/rule_id", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/security/rules/rule_id", {
  method: "PUT",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "status": "ready",
    "plan": "free",
    "limits": {
      "custom": 5,
      "ratelimit": 1
    },
    "http_request_firewall_custom": [
      {
        "id": "a1b2",
        "name": "Block bad country",
        "action": "block",
        "enabled": true,
        "expression": "(ip.src.country in {\"XX\"})"
      }
    ],
    "http_ratelimit": []
  }
}
```

Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `unavailable`, `validation_failed`

## Switch a rule on or off, or move it

`POST /sites/{site_id}/security/rules/{rule_id}/{action}` · scope `security:write`

Rules run in order: the first that matches decides.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `rule_id` | path | string | yes | The rule id (see GET .../security). |
| `action` | path | string (one of: enable, disable, up, down) | yes | enable / disable the rule, or move it up / down the order. |

### Example

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/security/rules/rule_id/scan" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/sites/123/security/rules/rule_id/scan", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/security/rules/rule_id/scan", {
  method: "POST",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "status": "ready",
    "plan": "free",
    "limits": {
      "custom": 5,
      "ratelimit": 1
    },
    "http_request_firewall_custom": [
      {
        "id": "a1b2",
        "name": "Block bad country",
        "action": "block",
        "enabled": true,
        "expression": "(ip.src.country in {\"XX\"})"
      }
    ],
    "http_ratelimit": []
  }
}
```

Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `unavailable`, `validation_failed`

## Delete a firewall rule

`DELETE /sites/{site_id}/security/rules/{rule_id}` · scope `security:write` · **destructive**

Removes one rule from the site at Cloudflare.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `rule_id` | path | string | yes | The rule id (see GET .../security). |

### Example

```bash
curl -s -X DELETE "https://app.pbn.ltd/api/v1/sites/123/security/rules/rule_id" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.delete("https://app.pbn.ltd/api/v1/sites/123/security/rules/rule_id", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/security/rules/rule_id", {
  method: "DELETE",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "status": "ready",
    "plan": "free",
    "limits": {
      "custom": 5,
      "ratelimit": 1
    },
    "http_request_firewall_custom": [
      {
        "id": "a1b2",
        "name": "Block bad country",
        "action": "block",
        "enabled": true,
        "expression": "(ip.src.country in {\"XX\"})"
      }
    ],
    "http_ratelimit": []
  }
}
```

Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized`, `unavailable`
