# Register a domain (fail-closed; requires Idempotency-Key)

> Register a domain (fail-closed; requires Idempotency-Key): CustomDomain™ API reference in the registrar group. Covers parameters, request body and responses.

Source: https://docs.customdomain.ai/docs/api-reference/registrar/purchaseDomain

## `POST https://api.customdomain.ai/v1/registrar/purchase`

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

### Authentication

`widgetJWT`, `agentToken`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | yes |  |

### Request body (JSON)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `domain` | string | yes |  |
| `buyer` | object | yes |  |

### Responses

| Status | Description |
| --- | --- |
| `200` | Order (idempotent replay) |
| `201` | Order |
| `400` | Error envelope: {code, title, details}. |
| `401` | Error envelope: {code, title, details}. |
| `402` | 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. |
| `403` | `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. |
| `503` | Error envelope: {code, title, details}. |

The full specification is served at `GET https://api.customdomain.ai/v1/openapi.json`.
