CustomDomain Managed DNS API
CustomDomain™ hosts a domain's whole zone after the user points its nameservers at us once, and writes every record from then on.
CustomDomain™ hosts a domain's whole zone after the user points its nameservers at us once, and writes every record from then on.
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.curl "https://apis.io/api/v1/apis/customdomain-ai:customdomain-ai-managed-dns-api"
curl "https://apis.io/api/v1/apis?limit=25"
Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: Customdomain Ai Managed DNS API
version: '1.0'
license:
name: Apache-2.0
identifier: Apache-2.0
description: 'Operations tagged Managed DNS across 2 of this provider''s published API definitions: customdomain-ai-openapi-original.json, customdomain-ai-openapi.yml. Each path carries the servers of the definition it was published in.'
servers:
- url: https://api.customdomain.ai/v1
description: Hosted control-plane
- url: http://localhost:8080/v1
description: Local control-plane
security:
- apiKey: []
tags:
- name: Managed DNS
description: CustomDomain™ hosts a domain's whole zone after the user points its nameservers at us once, and writes every record from then on.
paths:
/connections/{id}/managed-dns:
post:
operationId: startManagedDNS
summary: Start managed DNS for a connection's domain
tags:
- Managed DNS
description: 'Offers the connection''s end user a one-time change instead of typing records: point the domain''s nameservers at CustomDomain™, and we host the zone and write every record from then on.
The first call creates a hosted zone for the connection''s REGISTRABLE domain (acme.com, also for shop.acme.com), copies the records the domain already serves at common names (apex A/AAAA/MX/TXT/CAA, www, mail hosts, DMARC, common DKIM selectors, common SRV names, and every name the connection needs) so nothing breaks at the switch, adds the records CustomDomain™ needs for the connection, looks up the registrar, and answers 201 with the zone. Later calls answer 200 with the same zone; nothing is created twice. A second connection under the same registrable domain joins the existing zone. The zone becomes `active` once every nameserver the registry lists for the domain is ours; the connection then verifies and goes live on its own.
Available only when `managed_dns.available` is true on `POST /v1/domains:check` (the deployment flag is on, and the domain''s nameservers can be changed at a registrar). Authorized like the other connection routes: an API key within the tenant, or a widget token of the connection''s own application (and domain, when the token is bound to one).
The end user''s widget token (scope `connect`) can start it only when the application offers DNS hosting on CustomDomain™: its `nameserver_migration` setting has mode `offer` or `prefer` and target `managed`. With the setting off the call answers 403 `forbidden` with the details "this application does not offer nameserver migration"; with target `dns_host` or `custom` it answers 403 `forbidden` with "this application hosts its users'' DNS elsewhere; the nameserver move is its offer". Either way no zone is created. A connection that already has a zone can still call it, to resume. API keys and app-scoped tokens are not limited by the setting, and reading or editing an existing zone''s records is unchanged. `managed_dns` on `POST /v1/domains:check` stays the deployment''s and the domain''s verdict; it does not say whether the application offers hosting.
The end user''s widget token acts only on what is its own. A connection that names an end user (`end_user_ref`) is the token''s when that is the token''s own `end_user_ref` (mint the token with it); a connection that names none (an API key made it) is the token''s only when its domain is one the token is bound to. A token with an `end_user_ref` and no bound domain owns none of those, and a token with no `end_user_ref` and no bound domain counts only this connection''s domain. The call''s connection must be the token''s: a connection of another end user answers 403 `token_wrong_end_user` before anything is created or linked (the create call refuses a token for that end user''s exact name too, so it is not handed that connection''s id). One zone serves the whole registrable domain, so a zone that already exists is started, joined or resumed only when every connection linked to it is the token''s too. Another end user''s zone, one that also holds another name''s connection of the platform (made with an API key) or a connection of another application, and one nobody is linked to (the end user removed its last connection; the platform''s API key starts it for the new one) answer 409 `conflict`, "the DNS of acme.com is already hosted by CustomDomain™ for someone else"; nothing is linked or written and the answer shows none of the zone. A zone this call creates is the caller''s. Two starts at once by one end user on a new zone can answer one of them 409; the same call again succeeds. API keys and app-scoped tokens are not limited by it.'
security:
- apiKey: []
- widgetJWT: []
parameters:
- $ref: '#/components/parameters/Id'
requestBody:
required: false
description: No parameters. Send no body, or `{}`.
content:
application/json:
schema:
type: object
additionalProperties: false
responses:
'200':
description: Managed DNS was already started; the zone as it stands.
content:
application/json:
schema:
$ref: '#/components/schemas/ManagedDNSView'
'201':
description: The zone was created and filled.
content:
application/json:
schema:
$ref: '#/components/schemas/ManagedDNSView'
'400':
$ref: '#/components/responses/Error'
'401':
$ref: '#/components/responses/Error'
'403':
description: '`forbidden` (the application does not offer DNS hosting on CustomDomain™, or a role that cannot write connections), `token_wrong_end_user` (a widget token: the connection belongs to another end user; mint a token with this domain''s `end_user_ref`), or `workspace_suspended` (see the `ForbiddenOrSuspended` response).'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
$ref: '#/components/responses/Error'
'409':
description: 'The domain''s zone is already hosted for another account, or (for a widget token) for someone else: not every connection linked to it is the token''s (another end user''s, the platform''s for another name, another application''s), or none is linked. Or another change to it is in progress.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'422':
description: '`managed_dns_unavailable` (this domain can''t be hosted; `details` says why) or `managed_dns_limit` (the account''s zone limit).'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'502':
description: '`managed_dns_backend_error`: the DNS hosting service didn''t respond. Nothing is lost; call again to finish.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'503':
$ref: '#/components/responses/Error'
get:
operationId: getManagedDNS
summary: The connection's managed DNS zone
tags:
- Managed DNS
description: The hosted zone's status, the nameservers to set at the registrar, what the registry currently lists, the registrar, every record, and notes to show the user. Poll it while `status` is `awaiting_delegation`. 404 when managed DNS was never started for this connection. A widget token reads it only for its own end user's connection and only when the zone is that end user's (every connection linked to it is the token's, as for the start above); otherwise it gets 403 or 409 and none of the zone.
security:
- apiKey: []
- widgetJWT: []
parameters:
- $ref: '#/components/parameters/Id'
responses:
'200':
description: The zone
content:
application/json:
schema:
$ref: '#/components/schemas/ManagedDNSView'
'401':
$ref: '#/components/responses/Error'
'403':
description: '`token_wrong_end_user`: a widget token whose end user is not the connection''s (mint a token with this domain''s `end_user_ref`).'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
$ref: '#/components/responses/Error'
'409':
description: 'A widget token only: the zone is hosted for someone else, because not every connection linked to it is the token''s.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'503':
$ref: '#/components/responses/Error'
servers:
- url: https://api.customdomain.ai/v1
description: Hosted control-plane
- url: http://localhost:8080/v1
description: Local control-plane
/connections/{id}/managed-dns/records:
post:
operationId: addManagedDNSRecords
summary: Import a zone file or add records to the managed zone
tags:
- Managed DNS
description: 'Send exactly one of `zone_file` (a BIND zone file, at most 256 KB) or `records` (at most 500). Records from a zone file are `imported`: for every name and type the file defines, they replace what discovery found. Listed records are `manual` and are added alongside what is there. Records CustomDomain™ needs for the connection always win: an imported record that collides with one is skipped with a note, and a listed one is refused with 409. Zone file lines that can''t be hosted (SOA, the zone''s own NS set, unsupported types, names outside the zone) are skipped, each with a warning naming its line. A zone holds at most 500 records.
Once the zone is `active`, only an API key or an app-scoped widget token may change it; an end user''s connect-scoped token may change the zone only during setup, and only a zone that is its own: its connection''s end user and every connection linked to the zone are the token''s (as for the start above).'
security:
- apiKey: []
- widgetJWT: []
parameters:
- $ref: '#/components/parameters/Id'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ManagedDNSRecordsRequest'
responses:
'200':
description: The zone after the change.
content:
application/json:
schema:
$ref: '#/components/schemas/ManagedDNSView'
'400':
$ref: '#/components/responses/Error'
'401':
$ref: '#/components/responses/Error'
'403':
description: '`token_wrong_end_user` (a widget token whose end user is not the connection''s), or `forbidden` (the zone is live and the token is an end user''s: use an API key or an app-scoped token).'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
$ref: '#/components/responses/Error'
'409':
description: A listed record would override one CustomDomain™ needs for the connection, or (a widget token) the zone is hosted for someone else because not every connection linked to it is the token's.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'413':
$ref: '#/components/responses/Error'
'422':
description: '`invalid_zone_file` (nothing in the file can be hosted) or `too_many_records` (the zone would pass 500 records).'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'503':
$ref: '#/components/responses/Error'
servers:
- url: https://api.customdomain.ai/v1
description: Hosted control-plane
- url: http://localhost:8080/v1
description: Local control-plane
/connections/{id}/managed-dns/records/{record_id}:
delete:
operationId: deleteManagedDNSRecord
summary: Remove one record from the managed zone
tags:
- Managed DNS
description: Removes a `discovered`, `imported` or `manual` record. A `customdomain` record is how CustomDomain™ serves the domain and is refused with 409; disconnecting the domain removes it. Once the zone is `active`, only an API key or an app-scoped widget token may remove records. A widget token removes records only from a zone that is its own, as for adding them.
security:
- apiKey: []
- widgetJWT: []
parameters:
- $ref: '#/components/parameters/Id'
- name: record_id
in: path
required: true
schema:
type: string
responses:
'200':
description: The zone after the change.
content:
application/json:
schema:
$ref: '#/components/schemas/ManagedDNSView'
'401':
$ref: '#/components/responses/Error'
'403':
description: '`token_wrong_end_user` (a widget token whose end user is not the connection''s), or `forbidden` (the zone is live and the token is an end user''s).'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
$ref: '#/components/responses/Error'
'409':
description: The record is one CustomDomain™ needs for the connection, or (a widget token) the zone is hosted for someone else because not every connection linked to it is the token's.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'503':
$ref: '#/components/responses/Error'
servers:
- url: https://api.customdomain.ai/v1
description: Hosted control-plane
- url: http://localhost:8080/v1
description: Local control-plane
components:
parameters:
Id:
name: id
in: path
required: true
schema:
type: string
schemas:
ManagedDNSRecordInput:
type: object
required:
- type
- name
- value
additionalProperties: false
properties:
type:
type: string
enum:
- A
- AAAA
- CNAME
- MX
- TXT
- SRV
- CAA
- NS
name:
type: string
description: Relative to the zone ("@", "www", "_dmarc") or fully qualified inside it.
value:
type: string
description: Same forms as ManagedDNSRecord.value.
ttl:
type: integer
description: Seconds; default 3600, clamped to 60 to 604800.
priority:
type:
- integer
- 'null'
description: MX preference (default 10) or SRV priority (default 0).
ManagedDNSView:
type: object
required:
- zone
- status
- nameservers
- observed_nameservers
- registrar
- records
- warnings
- checked_at
- activated_at
properties:
zone:
type: string
description: 'The hosted zone: the registrable domain.'
examples:
- acme.com
status:
type: string
enum:
- awaiting_delegation
- active
- failed
description: '`awaiting_delegation`: the zone holds the records; set the nameservers below at the registrar. `active`: every nameserver the registry lists is ours. `failed`: the switch never happened within 14 days, or the nameservers moved away again (the first warning says which); a failed zone keeps being checked and turns active once the delegation appears.'
nameservers:
type: array
items:
type: string
description: The nameservers to set at the registrar.
observed_nameservers:
type: array
items:
type: string
description: The nameservers the registry listed at the last check.
registrar:
description: The domain's registrar from RDAP, or null when unknown.
oneOf:
- type: object
required:
- name
- url
properties:
name:
type: string
url:
type: string
description: The registrar's website, or an empty string.
- type: 'null'
records:
type: array
items:
$ref: '#/components/schemas/ManagedDNSRecord'
warnings:
type: array
items:
type: string
description: Plain-language notes for the user (a partial delegation, records a zone file import skipped, records replaced).
checked_at:
type: string
format: date-time
description: When the delegation was last checked.
activated_at:
type:
- string
- 'null'
format: date-time
ManagedDNSRecord:
type: object
required:
- id
- type
- name
- value
- ttl
- priority
- source
properties:
id:
type: string
description: Stable id derived from the record content.
examples:
- mdr_1f3a9c0b7d2e5a4c6b8d
type:
type: string
enum:
- A
- AAAA
- CNAME
- MX
- TXT
- SRV
- CAA
- NS
name:
type: string
description: Fully qualified owner name, lower case ("acme.com" is the apex, "*.acme.com" a wildcard).
value:
type: string
description: 'A/AAAA: the address. CNAME/NS/MX: the target host ("." for a null MX). TXT: the text. SRV: "<weight> <port> <target>". CAA: `<flags> <tag> "<value>"`.'
ttl:
type: integer
description: Seconds.
priority:
type:
- integer
- 'null'
description: MX preference or SRV priority; null for other types.
source:
type: string
enum:
- discovered
- imported
- customdomain
- manual
description: Where the record came from. `customdomain` records are the ones CustomDomain™ needs for connected domains; they win every conflict and can't be removed here.
ManagedDNSRecordsRequest:
type: object
additionalProperties: false
description: Exactly one of zone_file or records.
properties:
zone_file:
type: string
maxLength: 262144
description: A BIND zone file (RFC 1035 master format).
records:
type: array
maxItems: 500
items:
$ref: '#/components/schemas/ManagedDNSRecordInput'
Error:
type: object
required:
- code
- title
properties:
code:
type: string
description: stable machine code, e.g. invalid_request, unauthorized, not_found, service_unavailable, internal_error
title:
type: string
description: human-readable summary
details:
type: string
description: optional extra context
responses:
Error:
description: 'Error envelope: {code, title, details}.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
securitySchemes:
managementKey:
type: http
scheme: bearer
description: Operator management key. Gates provisioning + reads across tenants.
provisionSecret:
type: apiKey
in: header
name: X-Provision-Secret
description: Narrow secret held by the customer console's server, for signup provisioning and its other server to server calls (also accepted as a Bearer token, except on POST /audit, where a Bearer is always a tenant credential).
apiKey:
type: http
scheme: bearer
description: Long-lived tenant API key, cd_live_… (production) or cd_test_… (staging, development), stored hashed. Keys created before the switch to cd_ start with sk_live_… / sk_test_… and are still accepted. Scoped to the owning application's tenant.
widgetJWT:
type: http
scheme: bearer
bearerFormat: JWT
description: Short-lived widget JWT minted server-side via POST /v1/tokens. Scoped to one application (and optionally one domain).
browserSession:
type: http
scheme: bearer
description: The session token of one cloud browser sign in, returned by `POST /browser-sessions:redeem`. It binds one session, one connection and one workspace, lives at most as long as the session, and stops working the moment the session ends. It lives in the popup's memory only.
agentToken:
type: oauth2
description: 'A delegated agent access token: an ES256 JWT that lives 15 minutes, sent as a bearer token. A person approves an agent in the console and the agent exchanges its authorization code (with PKCE) at the token endpoint. The token carries the workspace, the one application the grant is bound to, the grant, the approved scopes and its audience, and is verified against `/.well-known/jwks.json`. A route that accepts it lists the scope it needs; a token without the scope is refused with `403`, and a connection or order of another application answers `404`.'
flows:
authorizationCode:
authorizationUrl: https://api.customdomain.ai/oauth/agent/authorize
tokenUrl: https://api.customdomain.ai/oauth/agent/token
refreshUrl: https://api.customdomain.ai/oauth/agent/token
scopes:
domains:read: Look at availability, connections, records, certificates, diagnoses, orders and the billing standing.
domains:connect: Connect a domain, recheck it, reapply its records and start a one click setup.
domains:disconnect: Remove a connected domain. The call must name the domain.
domains:purchase: Buy a domain, up to the grant's monthly spend cap.
x-refined-from:
- customdomain-ai-openapi-original.json
- customdomain-ai-openapi.yml