Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Customdomain Ai Registrar API
version: '1.0'
license:
name: Apache-2.0
identifier: Apache-2.0
description: 'Operations tagged Registrar 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: Registrar
description: Domain search, quote, and purchase (Sell).
paths:
/registrar/search:
get:
operationId: searchDomains
summary: Search domain availability and pricing
tags:
- Registrar
description: Availability and retail prices for the typed name. With `suggest=1` it also returns the same name on other TLDs and close variants, each result carrying a `kind` (`exact`, `tld` or `variant`). A widget token of either scope, a tenant API key of any role, or a delegated agent token with `domains:read` may search.
security:
- widgetJWT: []
- apiKey: []
- agentToken:
- domains:read
parameters:
- name: q
in: query
required: true
schema:
type: string
- name: suggest
in: query
required: false
description: 'Send `1` for the shop style search: the typed name, the same name on other TLDs and close variants, each with a `kind`.'
schema:
type: string
enum:
- '1'
responses:
'200':
description: Results
'401':
$ref: '#/components/responses/Error'
'403':
$ref: '#/components/responses/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
/registrar/quote:
post:
operationId: quoteDomain
summary: Cheapest real-time price for a domain
tags:
- Registrar
description: The registrar's cheapest real time price for a domain, with the retail markup applied. A widget token of either scope, a tenant API key of any role, or a delegated agent token with `domains:read` may ask.
security:
- widgetJWT: []
- apiKey: []
- agentToken:
- domains:read
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- domain
properties:
domain:
type: string
responses:
'200':
description: Price
'400':
$ref: '#/components/responses/Error'
'401':
$ref: '#/components/responses/Error'
'403':
$ref: '#/components/responses/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
/registrar/purchase:
post:
operationId: purchaseDomain
summary: Register a domain (fail-closed; requires Idempotency-Key)
tags:
- Registrar
description: 'Off on the hosted service. This route registers a domain without taking a payment, so it needs both `SELL_PURCHASE_ENABLED` and its own switch `SELL_DIRECT_PURCHASE_ENABLED`; buy through checkout instead. A required `Idempotency-Key` header dedupes retries so a registration is never charged twice.
A delegated agent token needs `domains:purchase`; the grant''s monthly spend cap is charged first, and a purchase that would pass it answers `409`. A tenant API key is refused with `401`. A workspace that billing refuses answers `402`.'
security:
- widgetJWT: []
- agentToken:
- domains:purchase
parameters:
- name: Idempotency-Key
in: header
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- domain
- buyer
properties:
domain:
type: string
buyer:
type: object
required:
- email
properties:
name:
type: string
email:
type: string
country:
type: string
responses:
'200':
description: Order (idempotent replay)
'201':
description: Order
'400':
$ref: '#/components/responses/Error'
'401':
$ref: '#/components/responses/Error'
'402':
$ref: '#/components/responses/BillingRequired'
'403':
$ref: '#/components/responses/ForbiddenOrSuspended'
'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
/registrar/checkout:
post:
operationId: startDomainCheckout
summary: Start an embedded payment to buy a domain at retail
tags:
- Registrar
description: 'Authorizes payment (manual capture) for a domain at the retail price. Returns a Stripe checkout session whose `client_secret` drives the embedded payment element. The domain is registered only after payment completes via `POST /v1/registrar/fulfill`; the card is captured strictly after the registrar settles, so a failed registration never charges the buyer. `503` when purchasing is disabled on the deployment.
`ui_mode` is optional and picks how the payment is shown. Leave it out (a `null` or an empty string counts as leaving it out), or send `embedded`, for Stripe Embedded Checkout. `elements` is Elements with the Checkout Sessions API: the caller renders the page and mounts only Stripe''s Payment Element with the returned `client_secret`, so the page can wear its own theme; the session is created on a Stripe API version of the dahlia release train (the billing guide names the exact version), so mount it with a Stripe.js of that train. Authorizing now and capturing after registration works the same in both. The response''s `ui_mode` says what was really created (`embedded` or `elements`; a domain payment has no redirect flow, so it is never `hosted`): an `elements` request answers `embedded` when Stripe could not create an Elements session, so read it before mounting anything. Any other `ui_mode`, `hosted` included, is a 400.'
security:
- widgetJWT: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- domain
- buyer
- return_url
properties:
domain:
type: string
buyer:
type: object
required:
- email
properties:
name:
type: string
email:
type: string
format: email
country:
type: string
description: ISO 3166-1 alpha-2.
return_url:
type: string
format: uri
ui_mode:
type: string
enum:
- embedded
- elements
default: embedded
description: 'How the payment is shown. Leave it out (or send null or an empty string) for embedded: a domain payment has no redirect flow. Never send `hosted`.'
responses:
'200':
description: Stripe checkout session (embedded or elements)
content:
application/json:
schema:
$ref: '#/components/schemas/CheckoutSession'
'400':
$ref: '#/components/responses/Error'
'401':
$ref: '#/components/responses/Error'
'402':
$ref: '#/components/responses/BillingRequired'
'403':
$ref: '#/components/responses/ForbiddenOrSuspended'
'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
/registrar/fulfill:
post:
operationId: fulfillDomainPurchase
summary: Finalize a domain purchase after the embedded payment completes
tags:
- Registrar
description: 'Verifies the session, registers the domain, captures the charge, and auto-connects the domain. Idempotent per session id (the Stripe webhook drives the same path server-side, so a closed tab never strands a paid session). `201` on first fulfillment, `200` on an idempotent replay. Exactly one caller fulfills a session: the first to claim its order registers the domain and captures the charge, and a caller that arrives while it is still working waits briefly, then gets that result or a `409` with code `fulfillment_in_progress`. A `409` never releases the authorization; retry the call, or read `GET /v1/sell/orders/{id}`, to see the outcome.'
security:
- widgetJWT: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- session_id
properties:
session_id:
type: string
responses:
'200':
description: Already fulfilled (idempotent replay)
content:
application/json:
schema:
$ref: '#/components/schemas/SellOrder'
'201':
description: Purchased
content:
application/json:
schema:
$ref: '#/components/schemas/SellOrder'
'400':
$ref: '#/components/responses/Error'
'401':
$ref: '#/components/responses/Error'
'409':
description: 'Another call is still completing this purchase (code: fulfillment_in_progress). The payment stays authorized and `order` is the order as it stands (status `purchasing`). Also `conflict` when the order can no longer be fulfilled (for example `expired`); the card was not charged.'
content:
application/json:
schema:
$ref: '#/components/schemas/FulfillmentInProgressError'
'502':
description: 'Registrar registration failed after payment authorization; the card authorization was released (code: registration_failed).'
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
/sell/orders:
get:
operationId: listSellOrders
summary: List the calling application's domain-purchase orders, newest first
description: 'Every order the application recorded (quoted, purchasing, purchased, transferred, failed, or expired). `purchasing` is an order being registered right now, after its payment was authorized. A quote-only order that was never paid for is expired by the hourly reaper after 24 hours (status `expired`, with a `purchase.confirmation.expired` webhook), so "quoted" always means recent intent.
With a tenant API key it lists the orders of every application of the tenant, or of one with `app_id` (another tenant''s id answers `404`). A widget token minted with scope `app`, or a delegated agent token with `domains:read`, lists its own application''s orders.'
tags:
- Registrar
security:
- widgetJWT: []
- apiKey: []
- agentToken:
- domains:read
parameters:
- name: app_id
in: query
required: false
description: With a tenant API key, list the orders of this one application of the tenant.
schema:
type: string
responses:
'200':
description: Orders
content:
application/json:
schema:
type: object
required:
- orders
- count
properties:
orders:
type: array
items:
$ref: '#/components/schemas/SellOrderView'
count:
type: integer
'401':
$ref: '#/components/responses/Error'
'403':
$ref: '#/components/responses/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
/sell/orders/{id}:
get:
operationId: getSellOrder
summary: Read a persisted domain-purchase order's status by id
tags:
- Registrar
description: A widget token or a delegated agent token (`domains:read`) reads an order of its own application; a tenant API key reads an order of any of its applications. An order of another application answers `404`.
security:
- widgetJWT: []
- apiKey: []
- agentToken:
- domains:read
parameters:
- $ref: '#/components/parameters/Id'
responses:
'200':
description: Order
content:
application/json:
schema:
$ref: '#/components/schemas/SellOrderView'
'401':
$ref: '#/components/responses/Error'
'403':
$ref: '#/components/responses/Error'
'404':
$ref: '#/components/responses/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:
FulfillmentInProgressError:
description: The error envelope of `POST /v1/registrar/fulfill`'s `409`, plus the order as it stands.
allOf:
- $ref: '#/components/schemas/Error'
- type: object
properties:
order:
$ref: '#/components/schemas/SellOrderView'
SellOrderView:
type: object
description: The stable, documented projection of a domain-purchase order.
required:
- orderId
- domain
- status
properties:
orderId:
type: string
examples:
- ord_1a2b3c
domain:
type: string
registrar:
type: string
status:
type: string
enum:
- quoted
- purchasing
- purchased
- transferred
- failed
- expired
createdAt:
type: string
format: date-time
regOrderId:
type: string
description: the registrar's order reference, once purchased.
jobId:
type: string
link:
type: string
description: sharing-rail checkout URL, if any.
freeDomain:
type: boolean
description: promo (first-domain-free) registration.
SellOrder:
type: object
description: The finalized order record returned by fulfillment. Its stable, documented projection is available via `GET /v1/sell/orders/{id}` (`SellOrderView`); additional internal fields may be present here.
additionalProperties: true
properties:
Domain:
type: string
Registrar:
type: string
RegOrderID:
type: string
CheckoutSession:
type: object
description: A Stripe Checkout session for the embedded or elements payment form.
required:
- id
properties:
id:
type: string
examples:
- cs_test_123
url:
type: string
format: uri
description: hosted checkout URL (unused for embedded and elements sessions).
client_secret:
type: string
description: mount Stripe Embedded Checkout or the elements checkout with this.
ui_mode:
type: string
enum:
- embedded
- elements
description: How the session was really created. An `elements` request answers `embedded` when Stripe could not create an Elements session. A domain payment has no redirect flow, so it is never `hosted`.
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:
BillingRequired:
description: 'Refused because of the workspace''s billing. `billing_required`: the workspace has no active or trialing subscription (a new workspace that has not started its trial, a Free workspace past its grace period, a comp that ended, or a subscription Stripe marked unpaid or canceled). It stops a new connection, the start of a domain purchase and the first live credential (an application, an API key or a rotated client secret). Domains already connected keep working, and an owner or admin sets up billing in the console; `details` says where. A new workspace''s first domain is the exception: while it has fewer pending, propagating or live connections than its free domains (ten, unless an operator set another number: `free_domains`), a connect and the start of a purchase go through (`first_domain_free`), and the next one is refused with a `title` saying a card is needed to connect more domains. Credentials still need the trial. `quota_exceeded` (connections only): a plan that is refused past its quota (Starter, and the retired Free) is at it and the deployment enforces quotas. `connections_unavailable`: what a connect-scoped widget token, the tenant''s own end user, gets instead of either (and instead of `403 workspace_suspended`), with neutral wording, so the tenant''s customers are never told about the tenant''s billing or account. A delegated agent token gets the full wording, and `GET /billing/standing` tells it beforehand whether a new connection would be refused.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: billing_required
title: An active subscription is needed to connect new domains.
details: This workspace has not started its subscription yet. Domains that are already connected keep working. An owner or admin can set up billing at https://app.customdomain.ai/app/billing.
Error:
description: 'Error envelope: {code, title, details}.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
ForbiddenOrSuspended:
description: '`forbidden`: the credential may not do this. `workspace_suspended`: CustomDomain™ support suspended the workspace. While it is suspended, new connections (one by one or in bulk, where each domain fails on its own), every connect step on a domain it already has (the credentialed apply, a provider sign in to connect, the cloud browser, Domain Connect, the Route 53 access role, the hosted zone and `:reapply`) and the start of a domain purchase are refused; reads, rechecks, removals and the sign in to remove a domain keep working, and domains already connected keep their records. A connect-scoped widget token, the tenant''s own end user, gets `402 connections_unavailable` instead, the neutral answer it gets for a billing refusal. Support resumes the workspace; nothing in the API does.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
code: workspace_suspended
title: This workspace is suspended.
details: New domains, connect steps and domain purchases are refused while CustomDomain™ support has this workspace suspended. Contact support to have it resumed.
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