UniFi OS 4.x introduced a content filtering feature (Cybersecure menu) that supports custom domain block lists per network. The API endpoint is not officially documented but works with the standard API key.
- UniFi OS 4.x (tested on UniFi Express 7, firmware 4.4.x)
- API key with network access (
Settings → Control Plane → API Keys) - The content filter profile ID (see below)
The easiest way is to open the UniFi UI, navigate to Cybersecure → Content Filtering, enable a rule, and intercept the request in browser devtools (Network tab). Look for a PUT request to:
/proxy/network/v2/api/site/default/content-filtering/<profile-id>
Copy the profile ID from the URL.
Alternatively, you can try GET on the collection — though this endpoint may require cookie auth depending on your firmware version:
curl -sk https://<unifi-ip>/proxy/network/v2/api/site/default/content-filtering \
-H "X-API-KEY: <your-api-key>"{
"_id": "<profile-id>",
"enabled": true,
"name": "Default",
"categories": ["ADVERTISEMENT"],
"client_macs": [],
"network_ids": ["<network-id>"],
"allow_list": [],
"block_list": ["example.com", "ads.example.com"],
"safe_search": [],
"schedule": { "mode": "ALWAYS" }
}Fields:
categories— built-in DNS Shield categories (e.g.ADVERTISEMENT,ADULT,GAMBLING,MALWARE)block_list— array of domains to block (custom, applied on top of categories)allow_list— domains to always allow, overriding blocksnetwork_ids— which networks this profile applies toclient_macs— limit to specific devices (empty = all devices on the network)schedule.mode—ALWAYSor time-based
This example uses Peter Lowe's Ad/Tracker list (~3.5k domains, hosts format):
curl -s "https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=0&mimetype=plaintext" \
-o /tmp/blocklist.txtParse to extract plain domains:
domains = []
with open('/tmp/blocklist.txt') as f:
for line in f:
line = line.strip()
if line.startswith('#') or not line:
continue
# format: "127.0.0.1 domain.com"
parts = line.split()
if len(parts) >= 2 and parts[1] != 'localhost':
domains.append(parts[1])
print(f"Parsed {len(domains)} domains")Other lists in hosts format that work with the same parser:
import json
import subprocess
UNIFI_IP = "192.168.1.1"
API_KEY = "<your-api-key>"
PROFILE_ID = "<your-profile-id>"
NETWORK_ID = "<your-network-id>"
payload = {
"_id": PROFILE_ID,
"enabled": True,
"name": "Default",
"categories": ["ADVERTISEMENT"],
"client_macs": [],
"network_ids": [NETWORK_ID],
"allow_list": [],
"block_list": domains,
"safe_search": [],
"schedule": {"mode": "ALWAYS"}
}
with open('/tmp/payload.json', 'w') as f:
json.dump(payload, f)
result = subprocess.run([
"curl", "-sk", "-X", "PUT",
f"https://{UNIFI_IP}/proxy/network/v2/api/site/default/content-filtering/{PROFILE_ID}",
"-H", "content-type: application/json",
"-H", f"X-API-KEY: {API_KEY}",
"-d", "@/tmp/payload.json"
], capture_output=True, text=True)
data = json.loads(result.stdout)
print(f"Applied {len(data.get('block_list', []))} domains")Or as a one-liner with bash:
UNIFI_IP="192.168.1.1"
API_KEY="<your-api-key>"
PROFILE_ID="<your-profile-id>"
curl -sk -X PUT "https://$UNIFI_IP/proxy/network/v2/api/site/default/content-filtering/$PROFILE_ID" \
-H "content-type: application/json" \
-H "X-API-KEY: $API_KEY" \
-d @/tmp/payload.json \
| python3 -c "import json,sys; d=json.load(sys.stdin); print('Applied:', len(d.get('block_list',[])))"- The API accepts the full list in a single PUT — no pagination needed
- Tested with ~3.500 domains (~70KB payload), worked without issues
- Larger lists (50k+) may hit payload size limits — test before assuming they work
- The content filter operates at the network level, before DNS resolvers like AdGuard
- UniFi does not expose logs or stats for content filter blocks via API key — only through the UI or authenticated browser sessions
- Built-in
categoriesand customblock_listare additive — both apply simultaneously