Skip to content

Sinkhole lists

Part of DNS filtering. Sinkhole lists decide what your clients may resolve. Three resources work together:

  • a list — a Blacklist or a Whitelist, 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;
}
GET /api/v1/sinkhole-lists

Your lists. With shared=true: every shared list on the platform, from any owner.

GET /api/v1/sinkhole-lists/:uuid

The 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.

Terminal window
curl -H "Authorization: Bearer $DINIO_API_KEY" \
"https://api-dinio.bitelex.com/api/v1/sinkhole-lists/$LIST_ID?include=roles&limit=1000"
POST /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.
Terminal window
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-lists

Returns 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).

PATCH /api/v1/sinkhole-lists/:uuid

Owner 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 /api/v1/sinkhole-lists/:uuid

Owner only. Deletes the list with its rules, its source and every link to it — for a shared list, other users’ links too.

A list can be filled from a blocklist file instead of rule by rule, and written out again as one. Both are owner only.

POST /api/v1/sinkhole-lists/:uuid/import

The 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.

Terminal window
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.
GET /api/v1/sinkhole-lists/:uuid/export

Returns 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.

Terminal window
curl -H "Authorization: Bearer $DINIO_API_KEY" -o ads.csv \
"https://api-dinio.bitelex.com/api/v1/sinkhole-lists/$LIST_ID/export?format=csv"

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.

PUT /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.

Terminal window
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/source

Dinio 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.

PATCH /api/v1/sinkhole-lists/:uuid/source

Body { "refresh_interval_s": 86400 }, or null for only on request. Returns the source; 404 if the list has none.

POST /api/v1/sinkhole-lists/:uuid/refresh

Downloads 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.

Terminal window
curl -X POST -H "Authorization: Bearer $DINIO_API_KEY" \
https://api-dinio.bitelex.com/api/v1/sinkhole-lists/$LIST_ID/refresh

Every 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.

DELETE /api/v1/sinkhole-lists/:uuid/source

The 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.

POST /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.

Terminal window
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-roles

409 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 /api/v1/sinkhole-roles/:uuid
PATCH /api/v1/sinkhole-roles/:uuid
DELETE /api/v1/sinkhole-roles/:uuid

PATCH 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.

GET /api/v1/sinkhole-links

Links of your identities. Optional filters client_identify_id, sinkholelist_id.

POST /api/v1/sinkhole-links

You 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.
Terminal window
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-links

403 if the list is neither yours nor shared, 403 SUSPENDED if the identity is suspended; 409 if the link already exists.

DELETE /api/v1/sinkhole-links/:client_identify_id/:sinkholelist_id

The identity’s owner decides; the list owner’s permission is not needed.