Exchange an enrolment code for a node token (node side).
const url = 'https://api.opdns.io/v1/nodes/enrol';const options = { method: 'POST', headers: {'Content-Type': 'application/json'}, body: '{"enrol_token":"example","name":"example","versions":{"edge":"example","unbound":"example","lists":1,"profile":1,"schema":1},"profile":{"id":"example","version":1,"name":"example","deleted":true,"lists":[1],"deny":[{"pattern":"example"}],"allow":[{"pattern":"example"}],"rewrites":[{"name":"example","type":"example","value":"example"}],"security":{"rebinding":true,"idn_homograph":true,"typosquat":true,"nrd":true,"dga":true,"cryptojacking":true,"threat_intel":true,"csam":true},"parental":{"categories":["example"],"services":["example"],"safe_search":true,"youtube_restricted":true},"settings":{"block_mode":"nxdomain","logs_enabled":true,"log_client_ip":true,"log_domains":true,"log_destination":"cloud","retention_days":1,"cname_uncloak":true,"cache_boost_ttl":1,"ecs":"off","node_queue_hours":0,"log_region":"ca"},"linked_ips":["example"]}}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://api.opdns.io/v1/nodes/enrol \ --header 'Content-Type: application/json' \ --data '{ "enrol_token": "example", "name": "example", "versions": { "edge": "example", "unbound": "example", "lists": 1, "profile": 1, "schema": 1 }, "profile": { "id": "example", "version": 1, "name": "example", "deleted": true, "lists": [ 1 ], "deny": [ { "pattern": "example" } ], "allow": [ { "pattern": "example" } ], "rewrites": [ { "name": "example", "type": "example", "value": "example" } ], "security": { "rebinding": true, "idn_homograph": true, "typosquat": true, "nrd": true, "dga": true, "cryptojacking": true, "threat_intel": true, "csam": true }, "parental": { "categories": [ "example" ], "services": [ "example" ], "safe_search": true, "youtube_restricted": true }, "settings": { "block_mode": "nxdomain", "logs_enabled": true, "log_client_ip": true, "log_domains": true, "log_destination": "cloud", "retention_days": 1, "cname_uncloak": true, "cache_boost_ttl": 1, "ecs": "off", "node_queue_hours": 0, "log_region": "ca" }, "linked_ips": [ "example" ] } }'Called by the self-hosted node. The code works once and expires; any failure is
400 invalid_token. The node token (opdnsnode_...) is returned once; the node
presents it as Authorization: Bearer to the node endpoints below and to the
link (link_url). Rate limited per client IP.
A node enrolling out of standalone mode sends its local profile: it replaces
the cloud profile (through the profile write path, so the version is bumped and
published) only when that profile is untouched (version 1, no rules); otherwise
the enrolment fails with 409 profile_not_empty. Invalid profile content is
422. In both cases nothing changes and the code stays usable.
Request Bodyrequired
Section titled “Request Bodyrequired”object
Replaces the name given at creation when set.
object
The node’s local (standalone) profile; its id, version and linked IPs are ignored.
object
Public 6-character id; never 000000.
The profile’s display name (renames republish the document).
object
object
object
object
object
object
EDNS Client Subnet (RFC 7871) forwarded to authoritative servers. off sends none (an explicit /0 opt-out). anonymised sends the client’s network truncated to /24 (IPv4) or /56 (IPv6). full sends the client’s address itself (/32 and /128 by default; the operator may cap it lower, commonly /64 for IPv6), so the client’s IP address reaches every authoritative server queried on its behalf: better CDN steering, less privacy. A client’s own ECS option is forwarded no wider than the mode allows, and its /0 opt-out is always honoured.
How long opdns holds this profile’s logs for its self-hosted node while the node is offline (log destination self-hosted or both), in hours; 24 at most. 0 holds nothing: records made while the node is offline are dropped (with both the cloud copy is still kept). Records older than the window are never delivered. retention_days is the cloud log retention, a different setting. Profiles saved before 0.9.0 read as 24.
Where opdns stores this profile’s query logs in the cloud (log destination cloud or both). ca: Canada (the control plane at OVH Beauharnois, Québec), the only region available today and the default. eu is announced but not yet accepted: the API refuses it with a 422 problem (settings.log_region) until the region exists. A self-hosted node’s own logs stay on the node. Profiles saved before 0.10.0 read as ca.
Responses
Section titled “Responses”Enrolled.
object
Public 6-character id; never 000000.
object
Public 6-character id; never 000000.
pending until the enrolment code is exchanged.
object
object
Round trip of the link as the node measured it at its last health report (handshake, then keepalive pings). Absent when unknown (older nodes).
Ids of the profile-signing keys the node trusts (configured and learnt), as it reported them in its last health report; the node refuses profiles signed by any other key. Empty when it checks no signature. Absent from nodes that predate the field.
health.link_rtt_ms while the node is online; null when offline or unknown.
health.profile_key_ids, the key ids from the node’s last health report (kept while it is offline); empty when unknown.
Last committed rotation of the node token (node.token_rotated); absent before the first.
Approximate location of a self-hosted node, from GeoIP.
object
Latitude; the country’s representative point when the database has no coordinates.
Longitude, as lat.
ISO 3166-1 alpha-2 country code.
The city, when the database has one (a country database has none).
Where the location comes from.
Only in GET /v1/profiles/{id}/nodes: log batches waiting in the node’s
queue (pending plus delivered but unacknowledged on its consumer, or every
queued batch of the profile before the node first connected). Null when
unknown (revoked node, or the queue could not be read).
The request’s profile replaced the cloud profile.
The profile’s version after the replacement (only with profile_replaced).
Hex Ed25519 keys the cloud signs node profile responses with, the signing key first (threat model G-1). The node trusts them from enrolment on and refuses unsigned or badly signed profiles afterwards. Absent when the cloud does not sign.
Example
{ "link_url": "wss://link.opdns.net/link", "profile_url": "/v1/nodes/self/profile", "node": { "mode": "enrolled", "status": "pending", "location": { "source": "geoip" } }}Malformed request.
object
Stable machine code.
object
Seconds, repeating the Retry-After header (rate limits, offline nodes).
Example
{ "type": "https://opdns.io/problems/bad_request", "title": "Bad Request", "status": 400, "code": "bad_request", "detail": "invalid JSON body", "request_id": "5f2c9a0e7b1d4c38"}profile_not_empty: the cloud profile already has settings or rules.
object
Stable machine code.
object
Seconds, repeating the Retry-After header (rate limits, offline nodes).
Example
{ "type": "https://opdns.io/problems/profile_not_empty", "title": "Conflict", "status": 409, "code": "profile_not_empty", "detail": "the cloud profile already has settings or rules", "request_id": "5f2c9a0e7b1d4c38"}Field validation failed (validation_failed, unknown_list).
object
Stable machine code.
object
Seconds, repeating the Retry-After header (rate limits, offline nodes).
Example
{ "type": "https://opdns.io/problems/validation_failed", "title": "Unprocessable Content", "status": 422, "code": "validation_failed", "detail": "the request has invalid fields", "errors": [ { "field": "settings.block_mode", "message": "one of nxdomain, refused, null, block-page" } ], "request_id": "5f2c9a0e7b1d4c38"}Rate limited (rate_limited).
object
Stable machine code.
object
Seconds, repeating the Retry-After header (rate limits, offline nodes).
Example
{ "type": "https://opdns.io/problems/rate_limited", "title": "Too Many Requests", "status": 429, "code": "rate_limited", "detail": "too many requests", "request_id": "5f2c9a0e7b1d4c38", "retry_after": 6}