Skip to content

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

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 destinations shape gained country and owner this 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.

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

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

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.

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: @1790726400
Sunset: Wed, 31 Mar 2027 00:00:00 GMT

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

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.

Added:

  • POST /v1/nodes/self/token (node token only): rotates the calling node’s token, which is what opdns-node rotate-token calls. The answer holds the new node_token and previous_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, gets 409 node_token_superseded. See Rotate the node token.
  • token_rotated_at on nodes: the last committed rotation, absent before the first.
  • Problem code node_token_superseded (409).

Added:

  • In the public catalogue, GET /v1/lists, each list’s toggle (the profile setting it backs, such as threat_intel; empty for a list enabled by id), attribution (the credit its sources require, one per line) and sources: every enabled source feeding it, with id, name, url, licence (SPDX), licence_url, attribution, entries (what it gave the last build) and updated_at.
  • triage_due_at on list reports: the triage target, two business days after the report.
  • For operators, GET /v1/admin/lists/sources and GET /v1/admin/lists/versions: the list compiler’s sources and the history of its builds (List history).

Added, for self-hosted nodes (node token only): profiles are signed.

  • profile_pubkey in the answer of POST /v1/nodes/enrol: the keys the cloud signs profiles with, the signing key first.
  • profile_signature, profile_key_id and profile_pubkey in GET /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.

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.

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.

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.

Added:

  • suspended on every profile (always present, false unless an operator suspended it).
  • Problem code profile_suspended (409) on the writes to a suspended profile: PATCH and DELETE of the profile, PUT of its rules and lists, adding and removing linked IPs, PATCH and DELETE of its devices, and creating a node.
  • For operators, GET and PATCH /v1/admin/profiles/{id} to read and change a profile’s suspension. See Suspended profiles.

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

Changed: settings.retention_days accepts 1 to 90 days (was 1 to 730). A larger value is 422 validation_failed.

Added:

  • X-Opdns-Api-Version on every response; api_version and geoip_attribution in GET /v1/meta.
  • Deprecation and Sunset headers 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 the destinations_map and list_reports features. No authentication.
  • List reports: POST and GET /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 operators GET /v1/admin/list-reports and PATCH /v1/admin/list-reports/{id}.
  • Analytics: the destinations shape has two new columns, country and owner; 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) and too_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.