# Start an embedded payment to buy a domain at retail

> Start an embedded payment to buy a domain at retail: CustomDomain™ API reference in the registrar group. Covers parameters, request body and responses.

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

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

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.

### Authentication

`widgetJWT`

### Request body (JSON)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `domain` | string | yes |  |
| `buyer` | object | yes |  |
| `return_url` | string | yes |  |
| `ui_mode` | `embedded`, `elements` | no | 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

| Status | Description |
| --- | --- |
| `200` | Stripe checkout session (embedded or elements) |
| `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}. |

### `200` response body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `url` | string | no | hosted checkout URL (unused for embedded and elements sessions). |
| `client_secret` | string | no | mount Stripe Embedded Checkout or the elements checkout with this. |
| `ui_mode` | `embedded`, `elements` | no | 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`. |

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