---
title: "Start a Stripe Checkout session to save a payment method"
method: POST
path: "/billing/setup"
tags: ["Billing"]
---

# Start a Stripe Checkout session to save a payment method

`POST /billing/setup`

## Request

- Base URL: `https://api.customdomain.ai/v1`
- URL: `https://api.customdomain.ai/v1/billing/setup`
- Auth: HTTP bearer

Opens a Stripe Checkout session in `setup` mode: it saves a card WITHOUT charging it. This is the card-on-file flow that agent purchases and domain buys draw against later; it is not a plan purchase (see `/billing/checkout` for that). Requires `billing:manage` (owners and admins).
Supply EITHER `return_url` for the embedded flow, OR both `success_url` and `cancel_url` for the hosted redirect flow. Supplying neither is a 400.

`ui_mode` is optional and picks how the session is shown. Leave it out (a `null` or an empty string counts as leaving it out), or send `embedded`, for the behavior from before it existed: Stripe Embedded Checkout when `return_url` is set, and the hosted redirect when only `success_url` and `cancel_url` are. `elements` is Elements with the Checkout Sessions API: the caller renders the whole page and mounts only Stripe's Payment Element with the returned `client_secret`, so the page can wear its own theme. It needs `return_url`, which is where redirect based payment methods and the finished setup land, and 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. The response's `ui_mode` says what was really created (`embedded`, `elements` or `hosted`): an `elements` request answers `embedded` when Stripe could not create an Elements session, so read it before mounting anything. `hosted` is only ever an answer, never a request value: a client that keeps the response's `ui_mode` must not send it back (the redirect flow is chosen by sending `success_url` and `cancel_url` without `return_url`). Any other value is a 400.

## Request body

- object
  - `return_url` string: Where Checkout returns the user (embedded and elements flows).
  - `success_url` string: Hosted redirect flow; requires cancel_url too.
  - `cancel_url` string: Hosted redirect flow; requires success_url too.
  - `ui_mode` 'embedded' | 'elements': How the session is shown. Leave it out (or send null or an empty string) for Stripe Embedded Checkout when `return_url` is set and the hosted redirect when only `success_url` and `cancel_url` are. `elements` requires `return_url`. Never send `hosted`: it is only an answer.

## Response `200`

The Checkout session. Embedded and elements flows read `client_secret`; redirect flows read `url`.

- object
  - `id` string
  - `url` string: Hosted Checkout URL (redirect flow).
  - `client_secret` string: Client secret to mount the embedded or elements checkout with.
  - `ui_mode` 'embedded' | 'elements' | 'hosted': How the session was really created. An `elements` request answers `embedded` when Stripe could not create an Elements session.

## Other responses

- `400`: Error envelope: {code, title, details}.
- `403`: Error envelope: {code, title, details}.
- `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)
