# REST API overview

SafeDNS Shield filtering configuration is managed through its REST API. The API address is deployment-specific. The default management API port is `8080`, but the host, protocol, and port may be changed during deployment.

Use the following placeholder throughout this guide:

```text
<base-url> = http[s]://<shield-management-host>:<management-api-port>
```

For example:

```text
http://192.0.2.10:8080
```

Access is normally restricted to source addresses approved during deployment.

## Using the API

**Authentication**

All client-facing API requests require an HTTP bearer token.

```http
Authorization: Bearer <token>
Content-Type: application/json
```

Example:

```bash
curl \
  --header "Authorization: Bearer <token>" \
  --header "Content-Type: application/json" \
  "<base-url>/profiles/"
```

Use the token and management API address assigned to the Shield deployment.

**General behavior**

- Successfully completed configuration changes take effect immediately. No service restart or separate activation request is required.
- Successful create operations normally return `201 Created`.
- Successful update and retrieval operations normally return `200 OK`.
- Successful deletion and initialization operations return `204 No Content` with no response body.
- Invalid request data may return `422 Unprocessable Entity` with validation details.
- IPv4 addresses are supplied as dotted-decimal strings in request bodies, but some resource paths identify an IPv4 address by its unsigned integer representation.
- The examples use trailing slashes where they appear in the API specification.

## Profiles and filtering

A filtering profile combines category rules, application rules, block-page behavior, and several profile-level options.

**Profile fields**

| Field | Type | Description |
|---|---|---|
| `id` | integer | Client-supplied profile identifier. Required when creating a profile. |
| `page_id` | integer or `null` | Identifier of the block page assigned to the profile. |
| `white_list_only` | boolean | When enabled, only domains on the allowlist and explicitly allowed applications are permitted. Default: `false`. |
| `empty_dns_answer` | boolean | When enabled, Shield returns an empty DNS answer for a blocked domain instead of directing the client to a block page. Default: `false`. |
| `appaware_priority_over_bwlists` | boolean | When enabled, application rules take priority over explicit domain allow/deny-list rules. Default: `false`. |

A profile request also supports:

| Field | Type | Description |
|---|---|---|
| `cat_ids` | array of integers | Complete collection of filtering category IDs assigned to the profile. |
| `apps` | array of objects | Complete collection of application rules assigned to the profile. |

Each application rule contains:

```json
{
  "app_id": 12,
  "status": "deny"
}
```

Supported statuses are:

- `deny` — blocks domains associated with the application.
- `allow` — explicitly permits resolution of domains associated with the application, including when those domains would otherwise be blocked by the profile's category rules.

Application IDs can be obtained from:

```text
GET /app_aware/application/
```

Category IDs are listed in the [SafeDNS category reference](https://docs.safedns.com/books/installation-guides/page/list-of-safedns-categories).

**Replacement semantics**

When `cat_ids` or `apps` is included in:

```text
PATCH /profiles/{profile_id}
```

it replaces the complete existing collection. It is not merged with the current configuration.

- Omitting `cat_ids` leaves the category collection unchanged.
- Sending `"cat_ids": []` removes all category assignments.
- Omitting `apps` leaves the application-rule collection unchanged.
- Sending `"apps": []` removes all application rules.

**Initial configuration example**

The `/init/` operation creates the initial Shield configuration in one request.

- **Method:** `POST`
- **URL:** `<base-url>/init/`

```json
{
  "blockpages": [
    {
      "id": 1,
      "type": 0
    },
    {
      "id": 2,
      "type": 1
    }
  ],
  "profiles": [
    {
      "profile": {
        "id": 1,
        "page_id": 1,
        "white_list_only": false,
        "empty_dns_answer": false,
        "appaware_priority_over_bwlists": false
      },
      "cat_ids": [3, 4, 12],
      "apps": [
        {
          "app_id": 1,
          "status": "deny"
        },
        {
          "app_id": 12,
          "status": "deny"
        },
        {
          "app_id": 93,
          "status": "deny"
        }
      ]
    }
  ],
  "bw_lists": [
    {
      "profile_id": 1,
      "type": "deny",
      "domains": [
        "example1.com",
        "example2.com",
        "example3.com"
      ]
    }
  ],
  "napts": [],
  "nets": [
    {
      "ip": "100.100.100.100",
      "profile_id": 1,
      "prefix_len": 32
    },
    {
      "ip": "100.110.110.0",
      "profile_id": 1,
      "prefix_len": 24
    },
    {
      "ip": "100.120.0.0",
      "profile_id": 1,
      "prefix_len": 16
    }
  ],
  "nets6": []
}
```

All six top-level arrays are required. Include an empty array when a resource type is not used:

- `blockpages`
- `profiles`
- `bw_lists`
- `napts`
- `nets`
- `nets6`

The example creates:

1. Two block-page records:
   - Type `0`: default block page.
   - Type `1`: custom block page.
2. Profile `1`, assigned to block page `1`.
3. Three blocked content/security categories.
4. Three denied applications.
5. Three explicitly denied domains.
6. One IPv4 host assignment and two IPv4 subnet assignments.

Custom block-page content is configured outside the Shield REST API. The API only creates and assigns the block-page record.

A successful initialization returns:

```text
204 No Content
```

## Profile configuration examples

**Modifying filtering categories**

To change categories assigned to a profile, send the complete desired category collection.

The following request replaces the existing categories of profile `1` with six categories:

- **Method:** `PATCH`
- **URL:** `<base-url>/profiles/1`

```json
{
  "cat_ids": [3, 4, 12, 66, 70, 71]
}
```

Because `apps` and `profile` are omitted, only the category collection is changed.

To remove all category assignments:

```json
{
  "cat_ids": []
}
```

**Replacing application rules for a profile**

Application rules can be replaced together through the profile endpoint.

- **Method:** `PATCH`
- **URL:** `<base-url>/profiles/1`

```json
{
  "apps": [
    {
      "app_id": 1,
      "status": "deny"
    },
    {
      "app_id": 12,
      "status": "allow"
    }
  ]
}
```

This request removes any other application rules previously assigned to profile `1`. It denies application `1` and explicitly permits domains associated with application `12`, even if category rules would otherwise block them.

To remove all application rules:

```json
{
  "apps": []
}
```

**Managing individual application rules**

**Retrieve the application catalogue**

- **Method:** `GET`
- **URL:** `<base-url>/app_aware/application/`

Example response:

```json
[
  {
    "id": 1,
    "name": "Application name"
  }
]
```

**Retrieve all application rules for a profile**

- **Method:** `GET`
- **URL:** `<base-url>/profile/1/app_aware`

Example response:

```json
[
  {
    "profile_id": 1,
    "app_id": 12,
    "status": "allow"
  }
]
```

**Add one application rule**

- **Method:** `POST`
- **URL:** `<base-url>/profile/1/app_aware`

```json
{
  "app_id": 12,
  "status": "allow"
}
```

**Add several application rules with the same status**

- **Method:** `POST`
- **URL:** `<base-url>/profile/1/app_aware/batch`

```json
{
  "app_ids": [1, 12, 93],
  "status": "deny"
}
```

**Delete an application rule**

The final path value is the application ID. The current OpenAPI parameter name is `app_aware_id`, but it identifies the same value as `app_id`.

- **Method:** `DELETE`
- **URL:** `<base-url>/profile/1/app_aware/12`

No request body is required.

**Adding a domain to the allowlist**

To exempt a domain from category-based blocking, add it to the allowlist of the relevant profile.

- **Method:** `POST`
- **URL:** `<base-url>/profile/1/bw_list`

```json
{
  "type": "allow",
  "domain": "example4.com"
}
```

**Adding domains to the denylist**

To block several domains directly, use the batch operation.

- **Method:** `POST`
- **URL:** `<base-url>/profile/1/bw_list/batch`

```json
{
  "type": "deny",
  "domains": [
    "example5.com",
    "example6.com",
    "example7.com",
    "example8.com",
    "example9.com"
  ]
}
```

**Allow/deny-list storage limitation**

Shield stores allowlist and denylist domains as hashes. The API therefore cannot retrieve or enumerate the original domain names after they have been submitted.

API clients should retain their own copy of configured allowlists and denylists when later inspection, reconciliation, or synchronization is required.

Update and delete operations still accept the original domain name. Shield calculates its hash internally to locate the stored rule.

**Creating a new filtering profile**

- **Method:** `POST`
- **URL:** `<base-url>/profiles/`

```json
{
  "profile": {
    "id": 2,
    "page_id": 1,
    "white_list_only": false,
    "empty_dns_answer": false,
    "appaware_priority_over_bwlists": false
  },
  "cat_ids": [3, 4, 12, 13, 66, 70, 71],
  "apps": [
    {
      "app_id": 1,
      "status": "deny"
    },
    {
      "app_id": 12,
      "status": "deny"
    },
    {
      "app_id": 93,
      "status": "deny"
    }
  ]
}
```

This creates profile `2`, assigns block page `1`, enables the listed filtering categories, and adds three denied application rules.

## Subscriber assignments

**Assigning an IPv4 address or subnet**

To assign an IPv4 host to profile `1`, create a `/32` network record.

- **Method:** `POST`
- **URL:** `<base-url>/net/`

```json
{
  "ip": "100.100.100.101",
  "profile_id": 1,
  "prefix_len": 32
}
```

To assign a `/24` subnet:

```json
{
  "ip": "100.100.101.0",
  "profile_id": 1,
  "prefix_len": 24
}
```

Supported IPv4 prefix lengths are `/10` through `/32`.

**Retrieve all IPv4 assignments**

- **Method:** `GET`
- **URL:** `<base-url>/net/`

Example response:

```json
[
  {
    "ip": "100.100.100.101",
    "profile_id": 1,
    "prefix_len": 32
  },
  {
    "ip": "100.100.101.0",
    "profile_id": 1,
    "prefix_len": 24
  }
]
```

**IPv4 integer representation**

Individual IPv4 resource paths identify an address by its unsigned 32-bit integer representation.

For example:

```text
100.100.100.101 = 1684300901
100.100.101.0   = 1684301056
```

Python conversion example:

```python
from ipaddress import IPv4Address

int_ip = int(IPv4Address("100.100.100.101"))
print(int_ip)  # 1684300901
```

**Reassign an IPv4 address or subnet**

- **Method:** `PATCH`
- **URL:** `<base-url>/net/1684301056`

```json
{
  "ip": "100.100.101.0",
  "profile_id": 2,
  "prefix_len": 24
}
```

**Remove an IPv4 address or subnet**

- **Method:** `DELETE`
- **URL:** `<base-url>/net/1684301056`

No request body is required.

**Assigning an IPv6 address or subnet**

- **Method:** `POST`
- **URL:** `<base-url>/net6/`

```json
{
  "ip": "2001:db8:100::",
  "profile_id": 1,
  "prefix_len": 64
}
```

Supported IPv6 prefix lengths are `/16` through `/128`.

**Retrieve all IPv6 assignments**

- **Method:** `GET`
- **URL:** `<base-url>/net6/`

Example response:

```json
[
  {
    "ip": "2001:db8:100::",
    "profile_id": 1,
    "prefix_len": 64
  }
]
```

**Identifying subscribers through NAPT port ranges**

NAPT records assign different source-port ranges on a shared IPv4 address to different filtering profiles.

Port boundaries are inclusive. A range from `10000` to `19999` includes both port `10000` and port `19999`.

**Create a NAPT assignment**

- **Method:** `POST`
- **URL:** `<base-url>/napt/`

```json
{
  "ip": "203.0.113.10",
  "lower_port_bound": 10000,
  "upper_port_bound": 19999,
  "profile_id": 1
}
```

Port values must be between `0` and `65535`. The complete port range is `0` through `65535`.

**Create several NAPT assignments**

- **Method:** `POST`
- **URL:** `<base-url>/napt/batch`

```json
[
  {
    "ip": "203.0.113.10",
    "lower_port_bound": 10000,
    "upper_port_bound": 19999,
    "profile_id": 1
  },
  {
    "ip": "203.0.113.10",
    "lower_port_bound": 20000,
    "upper_port_bound": 29999,
    "profile_id": 2
  }
]
```

**Retrieve all NAPT assignments for an IPv4 address**

The path uses the integer representation of the IPv4 address.

- **Method:** `GET`
- **URL:** `<base-url>/napt/3405803786`

`3405803786` is the integer representation of `203.0.113.10`.

**Delete several NAPT assignments**

Unlike individual NAPT resource paths, the batch-deletion body uses dotted-decimal IPv4 strings.

- **Method:** `DELETE`
- **URL:** `<base-url>/napt/batch`

```json
[
  {
    "ip": "203.0.113.10",
    "lower_port_bound": 10000,
    "upper_port_bound": 19999
  },
  {
    "ip": "203.0.113.10",
    "lower_port_bound": 20000,
    "upper_port_bound": 29999
  }
]
```

`profile_id` is not included in the batch-deletion request.

## Profile schedules

A schedule temporarily switches one profile to another during specified periods.

The profile identified in the URL is the source profile. `target_profile_id` identifies the profile used during scheduled periods. A profile cannot target itself.

**Supported day formats**

Both weekday names and integers are accepted:

| Integer | Weekday |
|---:|---|
| `0` | Monday |
| `1` | Tuesday |
| `2` | Wednesday |
| `3` | Thursday |
| `4` | Friday |
| `5` | Saturday |
| `6` | Sunday |

Weekday names must be lowercase:

```text
monday, tuesday, wednesday, thursday, friday, saturday, sunday
```

Examples in this guide use weekday names for readability.

Times use `HH:MM` format and must fall on 30-minute boundaries, such as `08:00`, `12:30`, or `18:00`. `00:00` represents the start of a day and `24:00` represents the end of a day.

**Create a schedule**

- **Method:** `POST`
- **URL:** `<base-url>/profile/1/schedule`

```json
{
  "target_profile_id": 2,
  "periods": [
    {
      "start": {
        "day": "monday",
        "time": "18:00"
      },
      "end": {
        "day": "tuesday",
        "time": "08:00"
      }
    },
    {
      "start": {
        "day": 5,
        "time": "00:00"
      },
      "end": {
        "day": 5,
        "time": "24:00"
      }
    }
  ]
}
```

This example switches profile `1` to profile `2` from Monday evening through Tuesday morning and for the whole of Saturday.

**Update a schedule**

- **Method:** `PATCH`
- **URL:** `<base-url>/profile/1/schedule`

To change the target profile for all existing periods:

```json
{
  "target_profile_id": 3
}
```

To replace all existing periods:

```json
{
  "periods": [
    {
      "start": {
        "day": "friday",
        "time": "18:00"
      },
      "end": {
        "day": "monday",
        "time": "08:00"
      }
    }
  ]
}
```

When `periods` is supplied, all previously configured periods are deleted and replaced by the submitted collection.

**Delete a schedule**

- **Method:** `DELETE`
- **URL:** `<base-url>/profile/1/schedule`

No request body is required.

## Block pages

Block-page records use two types:

| Type | Meaning |
|---:|---|
| `0` | Default block page |
| `1` | Custom block page |

Example creation request:

- **Method:** `POST`
- **URL:** `<base-url>/blockpage/`

```json
{
  "id": 2,
  "type": 1
}
```

Custom block-page content is configured outside the Shield REST API.