DNSCove control-plane API (1.0)
Download OpenAPI specification:
JSON API for the DNSCove control-plane: magic-link auth, zones and records, delegation/parity checks, snapshot publish, Route53 import, and scoped machine tokens for automation (ACME DNS-01, per-zone record management, and tenant-wide provisioning).
Two credential types:
Session — established by
POST /api/auth/exchange(magic-link) orPOST /api/webauthn/login/finish(passkey) and ended byPOST /api/auth/logout. Delivered as an HttpOnlySet-Cookie, never in a response body or anAuthorizationheader (see thesessionCookiesecurity scheme below). Full access to the tenant's zones for as long as the cookie is valid and not revoked — see SECURITY.md for the revocation model. Because a browser attaches this cookie to requests automatically, every mutating request that carries it must also pass anOrigin/Sec-Fetch-Sitecheck and echo a matchingX-CSRF-Tokenheader; see API.md for the cookie attributes and the CSRF rule. Used by the console.Scoped machine token (
dnsc_<id>_<secret>) — sent asAuthorization: Bearer <token>. Never a browser credential, so it is exempt from the CSRF/Originchecks above (see API.md). Carries one scope:acme-dns01— bound to one zone; may call onlyPOST /api/zones/{id}/acme-challengeon that zone.zone-admin— bound to one zone; full record CRUD + publish + delegation/parity + ACME on that zone. No zone lifecycle or token minting.tenant-admin— tenant-wide (no zone binding): create/list/delete zones, act on any zone, publish, and mint/revoke tokens. Root-equivalent — prefer a narrower scope when one zone is enough.
An endpoint that doesn't accept a token's scope answers
403.
Guides: the Quickstart walks a zone from create to live over the API; Migrating from Route 53 imports a hosted zone in one call; cert-manager issues Let's Encrypt certificates on Kubernetes; and Terraform / OpenTofu and CloudFormation manage DNS as code.
Request a magic-link sign-in token
Request Body schema: application/jsonrequired
| email required | string <email> |
Responses
Request samples
- Payload
{- "email": "user@example.com"
}Response samples
- 200
{- "sent": true,
- "dev_token": "string",
- "dev_link": "string"
}Exchange a magic-link token for a session
Consumes a single-use magic-link token and starts a console session. The session itself is never returned in the body — it is delivered as an HttpOnly Set-Cookie (see the Set-Cookie response header below and sessionCookie in securitySchemes), alongside a second, non-HttpOnly dnscove.csrf cookie the console echoes back as X-CSRF-Token on subsequent mutating requests. See API.md for the cookie attributes and the CSRF rule.
Request Body schema: application/jsonrequired
| token required | string |
Responses
Request samples
- Payload
{- "token": "string"
}Response samples
- 200
- 401
{- "email": "string",
- "tenant": "string"
}End the console session
Revokes the session's jti (see SECURITY.md) and clears both the session and dnscove.csrf cookies. Deliberately NOT gated behind an active session: a missing, malformed, expired, or already-revoked cookie all take the same path to the same 204 as a live session being logged out, so the endpoint can never be used to probe whether a session exists. When the request DOES carry a live session cookie it must still satisfy the same Origin/Sec-Fetch-Site and X-CSRF-Token double-submit checks as any other mutating route (see API.md) — those checks run ahead of this handler, not inside it.
header Parameters
| X-CSRF-Token | string Required on this mutating request only when it is authenticated by the |
Responses
Response samples
- 401
- 500
{- "error": {
- "code": "AccessDenied",
- "message": "string"
}
}Begin passkey registration (returns PublicKeyCredentialCreationOptions)
Requires a live console session (cookie) — never accepts a scoped machine token, so passkeys always bind to a signed-in human.
Authorizations:
header Parameters
| X-CSRF-Token | string Required on this mutating request only when it is authenticated by the |
Responses
Response samples
- 200
- 401
{ }Finish passkey registration
Authorizations:
header Parameters
| X-CSRF-Token | string Required on this mutating request only when it is authenticated by the |
Request Body schema: application/jsonrequired
the browser's attestation PublicKeyCredential
Responses
Request samples
- Payload
{ }Response samples
- 400
- 401
{- "error": {
- "code": "AccessDenied",
- "message": "string"
}
}Remove a passkey
Authorizations:
path Parameters
| id required | string |
header Parameters
| X-CSRF-Token | string Required on this mutating request only when it is authenticated by the |
Responses
Response samples
- 401
- 404
{- "error": {
- "code": "AccessDenied",
- "message": "string"
}
}Finish passkey sign-in — starts a console session
Same cookie-not-body session delivery as POST /api/auth/exchange (see its description) — the console has one session model, not two.
Request Body schema: application/jsonrequired
the browser's assertion PublicKeyCredential
Responses
Request samples
- Payload
{ }Response samples
- 200
- 401
{- "email": "string",
- "tenant": "string"
}Response samples
- 200
- 401
{- "zones": [
- {
- "id": "string",
- "tenant_id": "string",
- "origin": "string",
- "status": "PENDING_VERIFICATION",
- "ns": [
- "string"
], - "serial": 0,
- "published_serial": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]
}Create a zone
Authorizations:
header Parameters
| X-CSRF-Token | string Required on this mutating request only when it is authenticated by the |
Request Body schema: application/jsonrequired
| origin required | string |
Responses
Request samples
- Payload
{- "origin": "example.com"
}Response samples
- 201
- 400
- 401
{- "id": "string",
- "tenant_id": "string",
- "origin": "string",
- "status": "PENDING_VERIFICATION",
- "ns": [
- "string"
], - "serial": 0,
- "published_serial": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Get a zone
Authorizations:
path Parameters
| id required | string Example: Z1a2b3c4d5e6f70 |
Responses
Response samples
- 200
- 404
{- "id": "string",
- "tenant_id": "string",
- "origin": "string",
- "status": "PENDING_VERIFICATION",
- "ns": [
- "string"
], - "serial": 0,
- "published_serial": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Delete a zone
Authorizations:
path Parameters
| id required | string Example: Z1a2b3c4d5e6f70 |
header Parameters
| X-CSRF-Token | string Required on this mutating request only when it is authenticated by the |
Responses
Response samples
- 401
- 404
{- "error": {
- "code": "AccessDenied",
- "message": "string"
}
}Check parent NS delegation (activates the zone when delegated)
Authorizations:
path Parameters
| id required | string Example: Z1a2b3c4d5e6f70 |
Responses
Response samples
- 200
{- "delegation": {
- "origin": "string",
- "delegated": true,
- "expected": [
- "string"
], - "found": [
- "string"
], - "missing": [
- "string"
], - "checked_at": "2019-08-24T14:15:22Z"
}, - "status": "PENDING_VERIFICATION"
}Prove domain ownership via the `_dnscove-challenge` TXT and activate the zone
Checks for the _dnscove-challenge.<origin> TXT record whose value is dnscove-site-verification=<zone id> on the domain's current public DNS. On success the zone advances PENDING_VERIFICATION → ACTIVE and is published immediately — so the zone starts serving on ns1/ns2 before you move nameservers (a no-downtime cutover). The exact record to publish is returned by zone create, Route 53 import, and any unverified response (verification). Session, zone-admin (this zone), or tenant-admin.
Authorizations:
path Parameters
| id required | string Example: Z1a2b3c4d5e6f70 |
header Parameters
| X-CSRF-Token | string Required on this mutating request only when it is authenticated by the |
Responses
Response samples
- 200
- 401
{- "status": "PENDING_VERIFICATION",
- "verified": true,
- "served": true,
- "publish_error": "string",
- "verification": {
- "record": "_dnscove-challenge.example.com.",
- "type": "TXT",
- "value": "dnscove-site-verification=Z1a2b3c4d5e6f70"
}, - "challenge": { }
}Compare served records against public DNS ("safe to delete the old zone")
Authorizations:
path Parameters
| id required | string Example: Z1a2b3c4d5e6f70 |
Responses
Response samples
- 200
{- "parity": {
- "origin": "string",
- "matches": true,
- "checked": 0,
- "mismatched": 0,
- "truncated": true,
- "checked_at": "2019-08-24T14:15:22Z"
}, - "safe_to_delete": true,
- "status": "PENDING_VERIFICATION"
}Create, upsert, or delete a record set
Session, zone-admin (this zone), or tenant-admin. A record set is owned by (name, type): CREATE claims that pair and fails with 409 RRSetAlreadyExists if it is taken, while UPSERT replaces whatever is there — including every value it currently answers. Use CREATE when you mean "add a record" so a collision is reported instead of overwritten.
Authorizations:
path Parameters
| id required | string Example: Z1a2b3c4d5e6f70 |
header Parameters
| X-CSRF-Token | string Required on this mutating request only when it is authenticated by the |
Request Body schema: application/jsonrequired
| action | string Default: "UPSERT" Enum: "CREATE" "UPSERT" "DELETE" |
| name required | string |
| type required | string Enum: "A" "AAAA" "CNAME" "TXT" "MX" "SRV" "CAA" "NS" "PTR" "ALIAS" Standard RR types plus DNSCove's apex-safe ALIAS — the record other providers call ANAME and Route 53 calls an alias record. There is no separate |
| ttl | integer Default: 300 |
| records | Array of strings |
Responses
Request samples
- Payload
{- "action": "CREATE",
- "name": "www.example.com.",
- "type": "A",
- "ttl": 300,
- "records": [
- "192.0.2.1"
]
}Response samples
- 200
- 400
- 401
- 409
{- "status": "PENDING",
- "serial": 0
}List a zone's tokens (session or tenant-admin; never returns secrets)
Authorizations:
path Parameters
| id required | string Example: Z1a2b3c4d5e6f70 |
Responses
Response samples
- 200
- 404
{- "tokens": [
- {
- "id": "string",
- "zone_id": "string",
- "scope": "acme-dns01",
- "name": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "expires_at": "2019-08-24T14:15:22Z",
- "revoked": true
}
]
}Mint a per-zone token (session or tenant-admin)
Mints an acme-dns01 or zone-admin token bound to this zone. Returns the plaintext token exactly once — store it securely; it is unrecoverable afterward. tenant-admin is not a per-zone scope; mint it at POST /api/tokens.
Authorizations:
path Parameters
| id required | string Example: Z1a2b3c4d5e6f70 |
header Parameters
| X-CSRF-Token | string Required on this mutating request only when it is authenticated by the |
Request Body schema: application/json
| name | string |
| scope | string Default: "acme-dns01" Enum: "acme-dns01" "zone-admin" |
| expires_in_days | integer 0 = no expiry |
Responses
Request samples
- Payload
{- "name": "cert-manager webhook",
- "scope": "acme-dns01",
- "expires_in_days": 0
}Response samples
- 201
- 400
- 401
- 404
{- "id": "string",
- "zone_id": "string",
- "scope": "acme-dns01",
- "name": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "expires_at": "2019-08-24T14:15:22Z",
- "revoked": true,
- "token": "dnsc_1a2b3c4d5e6f7a8b_…"
}Revoke a per-zone token (session or tenant-admin)
Authorizations:
path Parameters
| id required | string Example: Z1a2b3c4d5e6f70 |
| tid required | string |
header Parameters
| X-CSRF-Token | string Required on this mutating request only when it is authenticated by the |
Responses
Response samples
- 401
- 404
{- "error": {
- "code": "AccessDenied",
- "message": "string"
}
}List every token in the tenant (session or tenant-admin; no secrets)
Authorizations:
Responses
Response samples
- 200
{- "tokens": [
- {
- "id": "string",
- "zone_id": "string",
- "scope": "acme-dns01",
- "name": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "expires_at": "2019-08-24T14:15:22Z",
- "revoked": true
}
]
}Mint a token of any scope (session or tenant-admin)
Mints tenant-admin (default, tenant-wide) or a zone-bound scope when a zone_id in the caller's tenant is supplied. Returns the plaintext token exactly once.
Authorizations:
header Parameters
| X-CSRF-Token | string Required on this mutating request only when it is authenticated by the |
Request Body schema: application/json
| name | string |
| scope | string Default: "tenant-admin" Enum: "tenant-admin" "zone-admin" "acme-dns01" |
| zone_id | string required for zone-bound scopes; ignored for tenant-admin |
| expires_in_days | integer 0 = no expiry |
Responses
Request samples
- Payload
{- "name": "terraform",
- "scope": "tenant-admin",
- "zone_id": "string",
- "expires_in_days": 0
}Response samples
- 201
- 400
- 401
{- "id": "string",
- "zone_id": "string",
- "scope": "acme-dns01",
- "name": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "expires_at": "2019-08-24T14:15:22Z",
- "revoked": true,
- "token": "dnsc_1a2b3c4d5e6f7a8b_…"
}Revoke any token in the tenant by id (session or tenant-admin)
Authorizations:
path Parameters
| tid required | string |
header Parameters
| X-CSRF-Token | string Required on this mutating request only when it is authenticated by the |
Responses
Response samples
- 401
- 404
{- "error": {
- "code": "AccessDenied",
- "message": "string"
}
}Present or clean up an ACME DNS-01 challenge TXT
Adds (PRESENT) or removes (CLEANUP) one value on the _acme-challenge TXT record and republishes to the edge. Accepts a session or an acme-dns01 / zone-admin token bound to this zone, or a tenant-admin token. Values merge, so multiple challenges at the same name (SAN/wildcard) coexist.
Authorizations:
path Parameters
| id required | string Example: Z1a2b3c4d5e6f70 |
header Parameters
| X-CSRF-Token | string Required on this mutating request only when it is authenticated by the |
Request Body schema: application/jsonrequired
| action required | string Enum: "PRESENT" "CLEANUP" |
| name required | string |
| value required | string base64url key authorization |
Responses
Request samples
- Payload
{- "action": "PRESENT",
- "name": "_acme-challenge.canary.lab.dnscove.com.",
- "value": "string"
}Response samples
- 200
- 400
- 401
{- "status": "ok",
- "action": "string",
- "changed": true,
- "published": true,
- "serial": 0
}Compile and ship a full snapshot to every edge node
Session, zone-admin, or tenant-admin. A zone-admin uses this to make its bound-zone record changes live; the publish operation compiles and ships the full snapshot. Responses to zone-admin tokens report only the bound zone's confirmation state and do not expose other zone origins. If another active zone is already unpublished when the request starts, it returns 409 PublishBlocked.
Authorizations:
header Parameters
| X-CSRF-Token | string Required on this mutating request only when it is authenticated by the |
Responses
Response samples
- 200
- 401
{- "published": true,
- "zones": 0,
- "nodes": [
- "string"
]
}Import a zone from `aws route53 list-resource-record-sets` JSON
Creates a zone from a pasted Route 53 export and imports every convertible record set (apex SOA/NS are dropped and managed by DNSCove; ALIAS records convert to DNSCove ALIAS; routing-policy record sets are skipped). AWS credentials are never sent — you run the CLI locally and post the JSON. The new zone is PENDING_VERIFICATION and not served yet; the response returns the _dnscove-challenge TXT to publish (verification), which you prove via POST /api/zones/{id}/verify to go live. Session or tenant-admin.
Authorizations:
header Parameters
| X-CSRF-Token | string Required on this mutating request only when it is authenticated by the |
Request Body schema: application/jsonrequired
| json required | string raw Route53 JSON |
Responses
Request samples
- Payload
{- "json": "string"
}Response samples
- 201
- 400
- 401
{- "zone": {
- "id": "string",
- "tenant_id": "string",
- "origin": "string",
- "status": "PENDING_VERIFICATION",
- "ns": [
- "string"
], - "serial": 0,
- "published_serial": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "imported": 0,
- "skipped": [
- {
- "name": "string",
- "type": "string",
- "reason": "string"
}
], - "ns": [
- "string"
], - "verification": {
- "record": "_dnscove-challenge.example.com.",
- "type": "TXT",
- "value": "dnscove-site-verification=Z1a2b3c4d5e6f70"
}
}