---
title: "Start an embedded payment to buy a domain at retail"
method: POST
path: "/registrar/checkout"
tags: ["Registrar"]
---

# Start an embedded payment to buy a domain at retail

`POST /registrar/checkout`

## Request

- Base URL: `https://api.customdomain.ai/v1`
- URL: `https://api.customdomain.ai/v1/registrar/checkout`
- Auth: HTTP bearer

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.

## Request body

- object
  - `domain` string, required
  - `buyer` object, required
    - `name` string
    - `email` string, email, required
    - `country` string: ISO 3166-1 alpha-2.
  - `return_url` string, uri, required
  - `ui_mode` 'embedded' | 'elements': 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`.

## Response `200`

Stripe checkout session (embedded or elements)

- CheckoutSession: A Stripe Checkout session for the embedded or elements payment form.
  - `id` string, required
  - `url` string, uri: hosted checkout URL (unused for embedded and elements sessions).
  - `client_secret` string: mount Stripe Embedded Checkout or the elements checkout with this.
  - `ui_mode` 'embedded' | 'elements': 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`.

## Other responses

- `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, with neutral wording, so the tenant's customers are never told about the tenant's billing. A delegated agent token gets the full wording, and `GET /billing/standing` tells it beforehand whether a new connection would be refused.
- `503`: Error envelope: {code, title, details}.

---

[API](https://skmtc.dev/customdomain/apis/customdomain-control-plane-api.md) · [All operations](https://skmtc.dev/customdomain/apis/customdomain-control-plane-api/llms.txt) · [OpenAPI document](https://skmtc.dev/customdomain/apis/customdomain-control-plane-api/revisions/34d89cb7a7cd?raw)
