---
title: "Create an application (client_secret returned once)"
method: POST
path: "/applications"
tags: ["Applications"]
---

# Create an application (client_secret returned once)

`POST /applications`

## Request

- Base URL: `https://api.customdomain.ai/v1`
- URL: `https://api.customdomain.ai/v1/applications`
- Auth: one of:
  - HTTP bearer
  - HTTP bearer

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

## Request body

- object
  - `tenant_id` string, required
  - `name` string, required
  - `environment` 'production' | 'staging' | 'development'
  - `sandbox` boolean: Create a test mode application (see above). Fixed for the life of the application.

## Response `201`

Created (client_secret present only here)

- object
  - `id` string, required
  - `tenant_id` string
  - `name` string, required
  - `environment` 'production' | 'staging' | 'development'
  - `default_origin` string
  - `connect_target` ConnectTarget
    - `cname_target` string, required
    - `apex_ipv4` string[]
  - `sandbox` boolean: `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.
  - `created_at` string, date-time
  - `client_secret` string: mints widget JWTs; shown once

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

---

[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)
