API versioning and deprecation
The API’s contract is its OpenAPI document,
/v1/openapi.yaml, which
the reference is generated from. The policy below
is the one written in that document’s info.description and enforced by
the control plane’s tests (internal/cp/api/versioning.go).
Within /v1, changes are additive
Section titled “Within /v1, changes are additive”The path carries the major version: every route is under /v1. Within
/v1 only these changes happen:
- new endpoints;
- new optional request fields;
- new response fields;
- new enum values in responses;
- new columns appended to query results (the
destinationsshape gainedcountryandownerthis way).
Nothing is renamed, removed, retyped or made required within /v1. Such a
change needs /v2, served alongside /v1 until /v1’s sunset.
So that additions never break you, a client must:
- ignore response fields it does not know;
- treat an enum value it does not know as “other”;
- read query results by column name, never by position.
Which version a server speaks
Section titled “Which version a server speaks”The document’s info.version is the contract’s version. It changes with
every change to the document. The rule written in the document is the
minor version for additions and the patch version for wording, but
several additions have shipped as patch versions (0.5.2, 0.6.1, 0.6.2).
Either way, a new version never removes or changes what /v1 already
has. The current version is 0.6.2.
Every API response carries it:
X-Opdns-Api-Version: 0.6.2It is also the api_version field of GET /v1/meta. The control plane
holds a hash of the document per version (api/openapi.lock): a change to
the document without a new info.version fails its tests
(TestSpecVersionBumped), so the header always names the document the
server was built with.
The spec lint
Section titled “The spec lint”Besides the version check, every change to the document must pass
mise run lint:spec (part of CI’s lint job): Redocly CLI 2.54.3 with
api/redocly.yaml, its recommended strict rules plus the house ones.
Every operation has an operationId and a description, every tag a
description, every error response is an application/problem+json
Problem with an example, and every example validates against its
schema. The few deliberate exceptions are listed in
api/.redocly.lint-ignore.yaml. This is why the
reference has a description for every operation and
an example for every error.
Deprecation
Section titled “Deprecation”A field or endpoint being retired is marked deprecated: true in the
document, with its replacement named in its description, and stays
working until its sunset. Every response that uses it carries two
headers:
| Header | Standard | Value |
|---|---|---|
Deprecation |
RFC 9745 | when it was deprecated, as @<unix seconds> |
Sunset |
RFC 8594 | the HTTP date after which it may be removed |
Deprecation: @1790726400Sunset: Wed, 31 Mar 2027 00:00:00 GMTThe sunset is at least 90 days after the deprecation; the tests refuse a
shorter one, and refuse a deprecated: true in the document without its
dates in the code (or the reverse). When a response involves several
deprecated items, the headers carry the earliest dates. Log or alert on
Deprecation in your client to find the calls that must change before
they break.
Deprecated now
Section titled “Deprecated now”| Deprecated | Use instead | Deprecated | Sunset |
|---|---|---|---|
lists_url, lists_sha256 in GET /v1/nodes/self/profile (node token) |
lists.url, lists.sha256 |
2026-09-30 | 2027-03-31 |
release in POST /v1/client-errors |
dashboard_version |
2026-09-30 | 2027-03-31 |
route in POST /v1/client-errors |
url |
2026-09-30 | 2027-03-31 |
at in POST /v1/client-errors |
nothing: it is ignored, the server records its own time | 2026-09-30 | 2027-03-31 |
The node profile’s two fields are always sent, so that response always carries the headers; a client error report gets them only when it uses one of the three old fields.
API changelog
Section titled “API changelog”Written by hand from the document’s changes, newest first. Every entry is
a new info.version. Several versions can come out on the same day.
0.6.2 (2026-09-30)
Section titled “0.6.2 (2026-09-30)”Added:
POST /v1/nodes/self/token(node token only): rotates the calling node’s token, which is whatopdns-node rotate-tokencalls. The answer holds the newnode_tokenandprevious_valid_s(600): after the node’s first request with the new token, the old one keeps working that many seconds. Only the current token can rotate; the previous one, in its grace, gets409 node_token_superseded. See Rotate the node token.token_rotated_aton nodes: the last committed rotation, absent before the first.- Problem code
node_token_superseded(409).
0.6.1 (2026-09-30)
Section titled “0.6.1 (2026-09-30)”Added:
- In the public catalogue,
GET /v1/lists, each list’stoggle(the profile setting it backs, such asthreat_intel; empty for a list enabled by id),attribution(the credit its sources require, one per line) andsources: every enabled source feeding it, withid,name,url,licence(SPDX),licence_url,attribution,entries(what it gave the last build) andupdated_at. triage_due_aton list reports: the triage target, two business days after the report.- For operators,
GET /v1/admin/lists/sourcesandGET /v1/admin/lists/versions: the list compiler’s sources and the history of its builds (List history).
0.6.0 (2026-09-30)
Section titled “0.6.0 (2026-09-30)”Added, for self-hosted nodes (node token only): profiles are signed.
profile_pubkeyin the answer ofPOST /v1/nodes/enrol: the keys the cloud signs profiles with, the signing key first.profile_signature,profile_key_idandprofile_pubkeyinGET /v1/nodes/self/profile: a base64 Ed25519 signature over"opdns-profile-sig/1\n" + id + "\n" + version + "\n"and the profile’s compact JSON. Absent when the cloud does not sign. See Profile signing keys.
0.5.2 (2026-09-30)
Section titled “0.5.2 (2026-09-30)”Added: limits on log and analytics queries
(Log query limits).
GET /v1/profiles/{id}/logs and /analytics/{shape} can answer
422 range_too_large, 422 query_too_expensive and
429 too_many_queries (with Retry-After), and a query without from
now starts at the shape’s longest range before to.
Changed, with no effect on requests or answers: the document passes the spec lint, so every operation and tag has a description, every error an example, and the document names its licence.
0.5.1 (2026-09-30)
Section titled “0.5.1 (2026-09-30)”Removed: the password_breached problem code. New passwords are no longer
checked against breached-password lists; the only rule is the length
(below). No request or response shape changed.
0.5.0 (2026-09-30)
Section titled “0.5.0 (2026-09-30)”Added:
GET /v1/pops: the PoP catalogue (id,name,city,country,lat,lon,status). No authentication; cacheable for five minutes.- Analytics: a new shape,
top_pops, which counts queries by the PoP that answered them (pop,count). See Log query results.
0.4.0 (2026-09-30)
Section titled “0.4.0 (2026-09-30)”Added:
suspendedon every profile (always present,falseunless an operator suspended it).- Problem code
profile_suspended(409) on the writes to a suspended profile:PATCHandDELETEof the profile,PUTof its rules and lists, adding and removing linked IPs,PATCHandDELETEof its devices, and creating a node. - For operators,
GETandPATCH /v1/admin/profiles/{id}to read and change a profile’s suspension. See Suspended profiles.
0.3.1 (2026-09-30)
Section titled “0.3.1 (2026-09-30)”Changed: new passwords (sign-up, password change, reset and account
recovery) must be 12 to 128 characters, counted in Unicode code points,
instead of 10 to 256. A password outside those bounds is
422 password_too_short or password_too_long, alone, or
validation_failed when other fields are invalid too. This version also
added password_breached (a password found in known data breaches),
which 0.5.1 removed again the same day. (0.3.0 existed only on a
development branch.)
0.2.1 (2026-09-30)
Section titled “0.2.1 (2026-09-30)”Changed: settings.retention_days accepts 1 to 90 days (was 1 to 730). A
larger value is 422 validation_failed.
0.2.0 (2026-09-30)
Section titled “0.2.0 (2026-09-30)”Added:
X-Opdns-Api-Versionon every response;api_versionandgeoip_attributioninGET /v1/meta.DeprecationandSunsetheaders on responses that use a deprecated field (above).GET /v1/capabilities: which settings and setting values this deployment supports, with the reason for any that it does not, and thedestinations_mapandlist_reportsfeatures. No authentication.- List reports:
POSTandGET /v1/profiles/{id}/list-reports(report a domain a list blocks wrongly or misses; at most 20 a day per account,429 too_many_reports), and for operatorsGET /v1/admin/list-reportsandPATCH /v1/admin/list-reports/{id}. - Analytics: the
destinationsshape has two new columns,countryandowner; a new shape,top_owners. See Log query results. - Problem codes
mfa_required(403: an operator action from a session signed in with the password alone, outside development) andtoo_many_reports(429).
Deprecated: lists_url and lists_sha256 of the node profile, and
release, route and at of client error reports.
The document before this versioning policy was adopted.