Sinkhole lists
Part of DNS filtering. Sinkhole lists decide what your clients may resolve. Three resources work together:
- a list — a
Blacklistor aWhitelist, owned by the user who creates it; - its rules — the names it matches;
- links — which client identities the list applies to.
Everything is allowed by default. A blacklist match blocks a name; a whitelist match lets a name through even if a blacklist of the same client blocks it. See How a lookup is decided.
Lists belong to the user who creates them, not to a zone: these routes need a personal key,
and a zone key gets 403 FORBIDDEN.
interface SinkholeList { uuid: string; name?: string; // a label, 1–100 characters; not unique type: 'Blacklist' | 'Whitelist'; is_shared: boolean; // others may link their identities to it owner_user_id?: string; created_at: number; updated_at: number; source?: SinkholeListSource | null; // only on Get a list; null = rules are managed by hand sinkholeRoles?: SinkholeRole[]; // only with ?include=roles, one page sinkholeRolesNext?: string | null; // cursor of the next page; null = last page roleCount?: number; // all rules of the list}
interface SinkholeListSource { // see Fill a list from a URL list_id: string; url: string; refresh_interval_s?: number; // absent = refreshed only on request last_fetched_at?: number; last_status?: 'Ok' | 'Failed'; // absent = never fetched last_error?: string; created_at: number; updated_at: number;}
interface SinkholeRole { uuid: string; list_id: string; sinkhole_type: 'Exact' | 'Wildcard' | 'Regex'; pattern: string; created_at: number; updated_at: number;}
interface SinkholeLink { // identified by its pair of uuids client_identify_id: string; sinkholelist_id: string; created_at: number; updated_at: number;}List lists
Section titled “List lists”/api/v1/sinkhole-listsYour lists. With shared=true: every shared list on the platform, from any owner.
Get a list
Section titled “Get a list”/api/v1/sinkhole-lists/:uuidThe owner can read their list; a shared list can be read by anyone — to see what a list
linked to their client blocks. The response includes source: where the rules come from, or
null for a list you manage by hand.
include=roles adds the list’s rules as sinkholeRoles, one page at a time — a list can have
hundreds of thousands:
| Query | |
|---|---|
limit |
Rules per page, 1–5 000, default 500. |
cursor |
sinkholeRolesNext of the previous page. Opaque: pass it back as it is. |
Rules come ordered by type (Exact, Wildcard, Regex), then by pattern.
sinkholeRolesNext is null on the last page, and roleCount is the number of rules in the
whole list. To get every rule in one response, export the list.
curl -H "Authorization: Bearer $DINIO_API_KEY" \ "https://api-dinio.bitelex.com/api/v1/sinkhole-lists/$LIST_ID?include=roles&limit=1000"Create a list
Section titled “Create a list”/api/v1/sinkhole-lists| Field | Type | |
|---|---|---|
type |
string | Blacklist or Whitelist. |
name |
string | Optional label, 1–100 characters (trimmed). |
is_shared |
boolean | Optional, default false. |
link_to |
uuid[] | Optional: your client identities to apply the list to right away, up to 100. |
curl -X POST -H "Authorization: Bearer $DINIO_API_KEY" -H "Content-Type: application/json" \ -d '{"type":"Blacklist","name":"ads","link_to":["'$IDENTITY_ID'"]}' \ https://api-dinio.bitelex.com/api/v1/sinkhole-listsReturns 201 with the list and the links it made, as links: SinkholeLink[] (empty without
link_to). The list and its links are created together: if an identity in link_to does not
exist (404) or is not yours (403), nothing is created.
Needs a filter package and counts against its filter.sinkhole_lists.max (403 NO_SUBSCRIPTION / 403 PLAN_LIMIT; see Packages and usage).
Change a list
Section titled “Change a list”/api/v1/sinkhole-lists/:uuidOwner only. Any of:
| Field | Type | |
|---|---|---|
name |
string | null | New label; null removes it. |
is_shared |
boolean | Share or unshare. Other users can link their identities to a shared list and read its rules, but not change or export them. Unsharing keeps the links they already have. |
Delete a list
Section titled “Delete a list”/api/v1/sinkhole-lists/:uuidOwner only. Deletes the list with its rules, its source and every link to it — for a shared list, other users’ links too.
Import and export
Section titled “Import and export”A list can be filled from a blocklist file instead of rule by rule, and written out again as one. Both are owner only.
Import a file
Section titled “Import a file”/api/v1/sinkhole-lists/:uuid/importThe body is the file itself, as text, up to 10 MiB; Content-Type is not inspected.
| Query | |
|---|---|
mode |
merge (default): add the rules the list doesn’t have, remove nothing. replace: make the list exactly the file — rules not in it are removed. |
dry_run |
true: read the file and report what would change, but write nothing. |
Each line is recognised on its own, so formats can be mixed in one file:
| In the file | Becomes |
|---|---|
0.0.0.0 ads.example.com (hosts file; several names per line work) |
Exact |
ads.example.com |
Exact — the name only, not its subdomains |
*.ads.example.com |
Wildcard |
||ads.example.com^ (Adblock, AdGuard) |
Exact and Wildcard *.ads.example.com |
|ads.example.com^, ||*.ads.example.com^, /regex/ |
Exact, Wildcard, Regex |
address=/ads.example.com/0.0.0.0 (dnsmasq) |
Exact and Wildcard |
CSV with the header type,pattern (the csv export) |
as written |
Comments (#, !), empty lines and names like localhost are skipped. Hosts lines must
point at a blocking address (0.0.0.0, 127.0.0.1, ::, ::1); Adblock rules with other
modifiers, paths or cosmetic filters are not supported. Adblock exceptions (@@||name^) are
the entries of a whitelist: import them into a Whitelist list. International names are
stored in their xn-- form.
curl -X POST -H "Authorization: Bearer $DINIO_API_KEY" --data-binary @hosts.txt \ "https://api-dinio.bitelex.com/api/v1/sinkhole-lists/$LIST_ID/import?mode=replace&dry_run=true"{ "success": true, "data": { "mode": "replace", "dry_run": true, "parse": { "rules": 81234, "duplicates": 12, "invalid": 3, "unsupported": 40, "ignored": 5, "samples": { "invalid": [{ "line": 17, "text": "bad..name", "reason": "name contains an empty label (consecutive dots)" }], "unsupported": [], "ignored": [] } }, "added": 81000, "removed": 120, "unchanged": 234, "total": 81234 }}| Field | |
|---|---|
parse.rules |
Distinct rules found in the file. |
parse.duplicates |
Lines that gave a rule an earlier line already gave. |
parse.invalid, unsupported, ignored |
Lines left out: malformed, in a form Dinio doesn’t support, or skipped on purpose. samples shows up to 10 of each, with the line number. |
added, removed, unchanged |
What the import changed (or, on a dry run, would change). |
total |
Rules in the list afterwards. |
Run a dry run first and show the report: a large import can take up to about 20 seconds. Importing the same file again changes nothing, so a failed import can simply be repeated.
| Error | |
|---|---|
403 PLAN_LIMIT |
The rules would go over filter.sinkhole_rules.max or filter.sinkhole_rules_per_list.max of your package. |
409 IMPORT_RUNNING |
An import into this list is already running. |
409 SOURCE_MANAGED |
The list is filled from a URL. |
413 PAYLOAD_TOO_LARGE |
The file is over 10 MiB. |
422 LIST_LIMIT |
The list would have more than 500 000 rules, or more than 1 000 Regex rules. |
422 VALIDATION |
No usable rule in the file, or an unknown mode. |
503 IMPORT_INTERRUPTED |
Only part was written. Repeat the import. |
Export a list
Section titled “Export a list”/api/v1/sinkhole-lists/:uuid/exportReturns the list as a file to download (Content-Disposition: attachment), not as JSON. Every
format can be imported again.
format |
|
|---|---|
csv (default) |
type,pattern lines: every rule, imports back exactly. |
adblock |
||name^ and /regex/ (AdGuard Home, uBlock Origin): every rule; with @@ for a whitelist. |
hosts |
0.0.0.0 name (Pi-hole, /etc/hosts): Exact rules only. |
domains |
One name per line: Exact rules only. |
For hosts and domains, a comment at the top says how many Wildcard and Regex rules were
left out.
curl -H "Authorization: Bearer $DINIO_API_KEY" -o ads.csv \ "https://api-dinio.bitelex.com/api/v1/sinkhole-lists/$LIST_ID/export?format=csv"Fill a list from a URL
Section titled “Fill a list from a URL”A list can follow a public blocklist on the web — a raw GitHub link, for example. Dinio downloads the file, replaces the list’s rules with it, and keeps it up to date on a schedule or on request. Owner only.
A list with a source takes no manual rules and no file imports (409 SOURCE_MANAGED) —
the next refresh would undo them. For your own rules next to a public list, create a second
list and link it to the same clients.
Set the source
Section titled “Set the source”/api/v1/sinkhole-lists/:uuid/source| Field | Type | |
|---|---|---|
url |
string | https:// on port 443, a public host; up to 2 048 characters. |
refresh_interval_s |
integer | null | Optional: refresh automatically this often, 86 400 (1 day) to 2 592 000 (30 days). Left out or null: only on request. |
Query dry_run=true downloads the file and reports what would change, without storing
anything.
curl -X PUT -H "Authorization: Bearer $DINIO_API_KEY" -H "Content-Type: application/json" \ -d '{"url":"https://blocklists.example.com/hosts.txt","refresh_interval_s":86400}' \ https://api-dinio.bitelex.com/api/v1/sinkhole-lists/$LIST_ID/sourceDinio downloads the file right away and replaces the list’s rules with it (the formats of
Import a file); only then is the source stored. Returns { source, import }:
the SinkholeListSource and the import report. A URL that can’t be fetched is
422 FETCH_FAILED with the reason in message, and nothing changes. The download must finish
within 30 seconds, be at most 10 MiB, and follow at most 3 redirects; addresses inside private
networks are refused.
Setting the source of a list that already has one replaces it.
Change the refresh interval
Section titled “Change the refresh interval”/api/v1/sinkhole-lists/:uuid/sourceBody { "refresh_interval_s": 86400 }, or null for only on request. Returns the source;
404 if the list has none.
Refresh now
Section titled “Refresh now”/api/v1/sinkhole-lists/:uuid/refreshDownloads the source now and replaces the rules with it. Returns { source, import };
import is null when the file hasn’t changed since the last download. With force=true
the file is read and applied even if it seems unchanged.
curl -X POST -H "Authorization: Bearer $DINIO_API_KEY" \ https://api-dinio.bitelex.com/api/v1/sinkhole-lists/$LIST_ID/refreshEvery attempt is recorded on the source — a failed one too, with last_status: "Failed" and
last_error; the list keeps its rules then. Show last_status, last_error and
last_fetched_at from Get a list.
Remove the source
Section titled “Remove the source”/api/v1/sinkhole-lists/:uuid/sourceThe list keeps its rules and takes manual rules again. 404 if it has no source.
Changing a list’s rules counts as changing the list: only its owner can. Anyone can read the
rules of a shared list. A list filled from a URL takes no manual
rules: adding, changing or removing one is 409 SOURCE_MANAGED.
Add a rule
Section titled “Add a rule”/api/v1/sinkhole-roles| Field | Type | |
|---|---|---|
list_id |
uuid | |
sinkhole_type |
string | See below. |
pattern |
string | Up to 1 024 characters. |
sinkhole_type |
pattern |
Matches |
|---|---|---|
Exact |
ads.example.com |
That name only. |
Wildcard |
*.example.com |
Every name strictly below example.com — not example.com itself. |
Regex |
^ads[0-9]+\.example\.com$ |
Names matching the regular expression, written without the trailing dot. |
A regular expression can’t use lookaround or backreferences ((?=, (?!, (?<=, (?<!,
\1): 422.
curl -X POST -H "Authorization: Bearer $DINIO_API_KEY" -H "Content-Type: application/json" \ -d '{"list_id":"'$LIST_ID'","sinkhole_type":"Wildcard","pattern":"*.tracker.example"}' \ https://api-dinio.bitelex.com/api/v1/sinkhole-roles409 if the list already has the same rule. A rule counts against your package’s
filter.sinkhole_rules.max and filter.sinkhole_rules_per_list.max (403 PLAN_LIMIT), and a
list holds at most 500 000 rules, 1 000 of them Regex (422 LIST_LIMIT).
Get, change and remove a rule
Section titled “Get, change and remove a rule”/api/v1/sinkhole-roles/:uuid/api/v1/sinkhole-roles/:uuid/api/v1/sinkhole-roles/:uuidPATCH takes any of sinkhole_type, pattern. There is no route that lists rules on their
own: read them with Get a list and include=roles, or
export the list. To replace all rules at once, import a
file with mode=replace; to drop them all, delete the list.
A link applies a list to a client identity. Links cannot be changed — delete and create instead.
List links
Section titled “List links”/api/v1/sinkhole-linksLinks of your identities. Optional filters client_identify_id, sinkholelist_id.
Create a link
Section titled “Create a link”/api/v1/sinkhole-linksYou must own the client identity, and either own the list or the list must be shared.
| Field | Type | |
|---|---|---|
client_identify_id |
uuid | Your client identity. |
sinkholelist_id |
uuid | Your list, or a shared one. |
curl -X POST -H "Authorization: Bearer $DINIO_API_KEY" -H "Content-Type: application/json" \ -d '{"client_identify_id":"'$IDENTITY_ID'","sinkholelist_id":"'$LIST_ID'"}' \ https://api-dinio.bitelex.com/api/v1/sinkhole-links403 if the list is neither yours nor shared, 403 SUSPENDED if the identity is
suspended; 409 if the link already exists.
Delete a link
Section titled “Delete a link”/api/v1/sinkhole-links/:client_identify_id/:sinkholelist_idThe identity’s owner decides; the list owner’s permission is not needed.