CustomDomain Registrar API

Domain search, quote, and purchase (Sell).

GitHub OpenAPI

Operations 7

GET /registrar/search Search domain availability and pricing #
POST /registrar/quote Cheapest real-time price for a domain #
POST /registrar/purchase Register a domain (fail-closed; requires Idempotency-Key) #
POST /registrar/checkout Start an embedded payment to buy a domain at retail #
POST /registrar/fulfill Finalize a domain purchase after the embedded payment completes #
GET /sell/orders List the calling application's domain-purchase orders, newest first #
GET /sell/orders/{id} Read a persisted domain-purchase order's status by id #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • 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.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/customdomain-ai:customdomain-ai-registrar-api"
All apis
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.

Get an API 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 Specification

customdomain-ai-registrar-api-openapi.yml Raw ↑
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