Skip to content

Exchange an enrolment code for a node token (node side).

POST
/v1/nodes/enrol
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.

Media typeapplication/json
object
enrol_token
required
string
name

Replaces the name given at creation when set.

string
<= 100 characters
versions
object
edge
string
unbound
string
lists
integer
profile
integer
schema
integer
profile

The node’s local (standalone) profile; its id, version and linked IPs are ignored.

object
id
required

Public 6-character id; never 000000.

string
/^[a-z0-9]{6}$/
version
required
integer
name

The profile’s display name (renames republish the document).

string
deleted
boolean
lists
required
Array<integer> | null
deny
required
Array<object> | null
object
pattern
string
allow
required
Array<object> | null
object
pattern
string
rewrites
required
Array<object> | null
object
name
string
type
string
value
string
security
required
object
rebinding
required
boolean
idn_homograph
required
boolean
typosquat
required
boolean
nrd
required
boolean
dga
required
boolean
cryptojacking
required
boolean
threat_intel
required
boolean
csam
required
boolean
parental
required
object
categories
required
Array<string>
services
required
Array<string>
safe_search
required
boolean
youtube_restricted
required
boolean
settings
required
object
block_mode
required
string
Allowed values: nxdomain null refused block-page
logs_enabled
required
boolean
log_client_ip
required
boolean
log_domains
required
boolean
log_destination
required
string
Allowed values: cloud self-hosted both none
retention_days
required
integer
>= 1 <= 90
cname_uncloak
required
boolean
cache_boost_ttl
required
integer
<= 86400
ecs
required

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.

string
Allowed values: off anonymised full
node_queue_hours
required

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.

integer
default: 24
Allowed values: 0 1 12 24
log_region
required

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.

string
default: ca
Allowed values: ca eu
linked_ips
required
Array<string> | null

Enrolled.

Media typeapplication/json
object
node_id
required
string format: uuid
node_token
required
string
profile_id
required

Public 6-character id; never 000000.

string
/^[a-z0-9]{6}$/
link_url
required
string format: uri
profile_url
required
string
Allowed value: /v1/nodes/self/profile
node
required
object
id
required
string format: uuid
profile_id
required

Public 6-character id; never 000000.

string
/^[a-z0-9]{6}$/
name
required
string
mode
required
string
Allowed values: enrolled standalone
status
required

pending until the enrolment code is exchanged.

string
Allowed values: pending enrolled revoked
online
required
boolean
versions
required
object
edge
string
unbound
string
lists
integer
profile
integer
schema
integer
health
required
object
link_rtt_ms

Round trip of the link as the node measured it at its last health report (handshake, then keepalive pings). Absent when unknown (older nodes).

integer
profile_key_ids

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.

Array<string>
uptime_s
integer
last_sync_unix_ms
integer
sqlite_bytes
integer
queue_depth
integer
oldest_record_unix_ms
integer
clock_offset_ms
integer
dropped_records
integer
unbound_healthy
boolean
reported_at_unix_ms
integer
link_rtt_ms

health.link_rtt_ms while the node is online; null when offline or unknown.

integer | null
profile_key_ids

health.profile_key_ids, the key ids from the node’s last health report (kept while it is offline); empty when unknown.

Array<string>
created_at
required
string format: date-time
enrol_expires_at
string format: date-time
enrolled_at
string format: date-time
connected_at
string format: date-time
last_seen
required
string | null format: date-time
revoked_at
string format: date-time
token_rotated_at

Last committed rotation of the node token (node.token_rotated); absent before the first.

string format: date-time
location
required
One of:

Approximate location of a self-hosted node, from GeoIP.

object
lat
required

Latitude; the country’s representative point when the database has no coordinates.

number
>= -90 <= 90
lon
required

Longitude, as lat.

number
>= -180 <= 180
country
required

ISO 3166-1 alpha-2 country code.

string
/^[A-Z]{2}$/
city

The city, when the database has one (a country database has none).

string
source
required

Where the location comes from.

string
Allowed values: geoip
queue_depth

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

integer | null format: int64
profile_replaced

The request’s profile replaced the cloud profile.

boolean
profile_version

The profile’s version after the replacement (only with profile_replaced).

integer
profile_pubkey

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.

Array<string>
Example
{
"link_url": "wss://link.opdns.net/link",
"profile_url": "/v1/nodes/self/profile",
"node": {
"mode": "enrolled",
"status": "pending",
"location": {
"source": "geoip"
}
}
}

Malformed request.

Media typeapplication/problem+json
object
type
required
string format: uri
title
required
string
status
required
integer
code
required

Stable machine code.

string
Allowed values: bad_request body_too_large bad_if_match unauthenticated invalid_credentials signup_closed invite_required waitlist_unavailable invalid_token token_expired token_used session_required insufficient_scope csrf_rejected not_found version_mismatch ip_conflict unknown_list too_many_rules validation_failed rate_limited internal node_revoked node_offline node_busy node_timeout node_error result_too_large shape_unsupported node_token_superseded relay_unavailable not_implemented query_timeout query_too_expensive range_too_large too_many_queries second_factor_required invalid_second_factor mfa_enrolment_required mfa_required reauth_required no_second_factor totp_already_enabled totp_not_enabled last_credential passkey_exists passkey_invalid passkeys_unavailable ceremony_invalid account_pending_deletion credential_required deletion_pending no_deletion_pending deletion_started organisation_has_members export_in_progress export_not_ready block_exists invalid_recovery_code profile_not_empty invalid_cursor bad_idempotency_key idempotency_key_reused idempotency_in_progress too_many_streams too_many_reports password_too_short password_too_long profile_suspended
detail
string
errors
Array<object>
object
field
required
string
message
required
string
request_id
string
retry_after

Seconds, repeating the Retry-After header (rate limits, offline nodes).

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

Media typeapplication/problem+json
object
type
required
string format: uri
title
required
string
status
required
integer
code
required

Stable machine code.

string
Allowed values: bad_request body_too_large bad_if_match unauthenticated invalid_credentials signup_closed invite_required waitlist_unavailable invalid_token token_expired token_used session_required insufficient_scope csrf_rejected not_found version_mismatch ip_conflict unknown_list too_many_rules validation_failed rate_limited internal node_revoked node_offline node_busy node_timeout node_error result_too_large shape_unsupported node_token_superseded relay_unavailable not_implemented query_timeout query_too_expensive range_too_large too_many_queries second_factor_required invalid_second_factor mfa_enrolment_required mfa_required reauth_required no_second_factor totp_already_enabled totp_not_enabled last_credential passkey_exists passkey_invalid passkeys_unavailable ceremony_invalid account_pending_deletion credential_required deletion_pending no_deletion_pending deletion_started organisation_has_members export_in_progress export_not_ready block_exists invalid_recovery_code profile_not_empty invalid_cursor bad_idempotency_key idempotency_key_reused idempotency_in_progress too_many_streams too_many_reports password_too_short password_too_long profile_suspended
detail
string
errors
Array<object>
object
field
required
string
message
required
string
request_id
string
retry_after

Seconds, repeating the Retry-After header (rate limits, offline nodes).

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

Media typeapplication/problem+json
object
type
required
string format: uri
title
required
string
status
required
integer
code
required

Stable machine code.

string
Allowed values: bad_request body_too_large bad_if_match unauthenticated invalid_credentials signup_closed invite_required waitlist_unavailable invalid_token token_expired token_used session_required insufficient_scope csrf_rejected not_found version_mismatch ip_conflict unknown_list too_many_rules validation_failed rate_limited internal node_revoked node_offline node_busy node_timeout node_error result_too_large shape_unsupported node_token_superseded relay_unavailable not_implemented query_timeout query_too_expensive range_too_large too_many_queries second_factor_required invalid_second_factor mfa_enrolment_required mfa_required reauth_required no_second_factor totp_already_enabled totp_not_enabled last_credential passkey_exists passkey_invalid passkeys_unavailable ceremony_invalid account_pending_deletion credential_required deletion_pending no_deletion_pending deletion_started organisation_has_members export_in_progress export_not_ready block_exists invalid_recovery_code profile_not_empty invalid_cursor bad_idempotency_key idempotency_key_reused idempotency_in_progress too_many_streams too_many_reports password_too_short password_too_long profile_suspended
detail
string
errors
Array<object>
object
field
required
string
message
required
string
request_id
string
retry_after

Seconds, repeating the Retry-After header (rate limits, offline nodes).

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

Media typeapplication/problem+json
object
type
required
string format: uri
title
required
string
status
required
integer
code
required

Stable machine code.

string
Allowed values: bad_request body_too_large bad_if_match unauthenticated invalid_credentials signup_closed invite_required waitlist_unavailable invalid_token token_expired token_used session_required insufficient_scope csrf_rejected not_found version_mismatch ip_conflict unknown_list too_many_rules validation_failed rate_limited internal node_revoked node_offline node_busy node_timeout node_error result_too_large shape_unsupported node_token_superseded relay_unavailable not_implemented query_timeout query_too_expensive range_too_large too_many_queries second_factor_required invalid_second_factor mfa_enrolment_required mfa_required reauth_required no_second_factor totp_already_enabled totp_not_enabled last_credential passkey_exists passkey_invalid passkeys_unavailable ceremony_invalid account_pending_deletion credential_required deletion_pending no_deletion_pending deletion_started organisation_has_members export_in_progress export_not_ready block_exists invalid_recovery_code profile_not_empty invalid_cursor bad_idempotency_key idempotency_key_reused idempotency_in_progress too_many_streams too_many_reports password_too_short password_too_long profile_suspended
detail
string
errors
Array<object>
object
field
required
string
message
required
string
request_id
string
retry_after

Seconds, repeating the Retry-After header (rate limits, offline nodes).

integer
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
}
Retry-After
integer