# Create an application (client_secret returned once)

> Create an application (client_secret returned once): CustomDomain™ API reference in the applications group. Covers parameters, request body and responses.

Source: https://docs.customdomain.ai/docs/api-reference/applications/createApplication

## `POST https://api.customdomain.ai/v1/applications`

Accepts the management key (the body `tenant_id` selects the tenant) or a tenant-scoped API key (the app is always created in the key's own tenant; any body `tenant_id` is ignored).

Send `"sandbox": true` to make a **test mode** application: a place to run the widget, the API and webhooks end to end before you have a real customer domain. It connects only reserved `.test` names (the first label picks the outcome: `conflict.acme.test` stays pending with `domain_already_connected`, `timeout.acme.test` fails after about a minute with `propagation_timeout`, any other name goes live after about 15 seconds), uses the manual rail only, and counts for nothing: no free domain, no quota, no billing, no metering and no reporting. Webhook events from it carry `"sandbox": true`. A workspace can hold up to 3 test mode applications, each up to 100 connections (`sandbox_limit`). A test mode application is always a development application, so leave `environment` out. A live application never connects a `.test` name (`sandbox_domain_reserved`), and a test mode one never connects a real name (`sandbox_domain_required`).

### Authentication

`managementKey`, `apiKey`

### Request body (JSON)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `tenant_id` | string | no | Required with the management key, where it selects the tenant. Ignored with a tenant API key, which always creates the application in its own tenant. |
| `name` | string | yes |  |
| `environment` | `production`, `staging`, `development` | no |  |
| `sandbox` | boolean | no | Create a test mode application (see above). Fixed for the life of the application. |

### Responses

| Status | Description |
| --- | --- |
| `201` | Created (client_secret present only here) |
| `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. |

### `201` response body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `tenant_id` | string | no |  |
| `name` | string | yes |  |
| `environment` | `production`, `staging`, `development` | no |  |
| `default_origin` | string | no |  |
| `connect_target` | object | no |  |
| `sandbox` | boolean | no | `true` for a **test mode** application, made with `"sandbox": true` on `POST /applications`. It runs the whole connect flow end to end (the widget, the API, webhooks) against simulated DNS, on reserved `.test` names only, and never touches a real domain, the free domain, the quota or billing. Fixed when the application is created. Omitted (treated as `false`) on a live application. |
| `nameserver_migration` | object | no | Whether and how the application offers to move an end user's nameservers. It is the one switch for every DNS hosting offer the widget makes. `off` (the default): never offered, and the setting is omitted; a widget token can't start managed DNS either, nor while the target is `dns_host` or `custom`. `offer`: the widget shows the move as a quiet second choice beside adding records. `prefer`: the move is the first choice. Send `{"mode": "off"}` to clear it. |
| `end_user_saved_access` | boolean | no | A returning end user may reuse the access their own secure sign-in kept, for their next domain in this application, after one tap of consent. On by default; set it with `PATCH /applications/{id}`. The access is that end user's own: bound to them and to this application, never usable by your team or by another end user. Always `false` for a test mode application, whose sign-ins are simulated and keep nothing. |
| `secure_sign_in` | object | no | Whether the secure sign-in (the widget's cloud browser way to connect) can run for this application's workspace at all, for any provider. Read only: it follows how this deployment is set up, not a setting of the application. Whether it runs for one domain is the `cloud_browser` entry of that domain's `rails`. |
| `created_at` | string | no |  |
| `client_secret` | string | no | mints widget JWTs; shown once |

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