---
title: "Create domain"
method: POST
path: "/v2/domains.createDomain"
tags: ["domains"]
---

# Create domain

`POST /v2/domains.createDomain`

Attach a custom domain to an environment and start verifying it.

The domain is created in the `pending` state and does not serve traffic until verification succeeds. Verification runs in the background and polls DNS, so it is eventually consistent.

The response returns `dnsRecords`: every record needed to finish setup, already resolved for whether this domain is an apex or a subdomain. Create every entry exactly as given. One record establishes routing and one proves ownership, and both are needed: whether ownership can be inferred from the routing record depends on how your provider publishes it, and a name another workspace has already verified can only be claimed through the ownership record. Neither is knowable before the records exist.

When your DNS provider supports Domain Connect, the response also carries a `domainConnect` object; opening its `url` applies the same records at the provider in one step. The object is absent when the shortcut is unavailable.

Domains are unique per workspace, so the same name cannot be attached to two environments. Attaching a domain that already exists in your workspace returns a 409 conflict.

How many domains you may attach is set by your plan. Attaching one beyond that allowance returns a 403; upgrade the plan or remove a domain you no longer need.

**Important**: verification stops after 24 hours without the required DNS records, and the domain moves to `failed`.

**Required Permissions**

Your root key must have one of the following permissions:
- `environment.*.create_domain` (to attach domains to any environment)
- `environment.<environment_id>.create_domain` (to attach domains to a specific environment)

## Request body

- V2DomainsCreateDomainRequestBody
  - `project` string, required — Identifies a resource by either its unique ID or its slug. Accepts a prefixed ID (such as 'proj_' or 'app_') or a slug.
  - `app` string, required — Identifies a resource by either its unique ID or its slug. Accepts a prefixed ID (such as 'proj_' or 'app_') or a slug.
  - `environment` string, required — Identifies a resource by either its unique ID or its slug. Accepts a prefixed ID (such as 'proj_' or 'app_') or a slug.
  - `domain` string, required — Fully qualified domain name to attach to the environment, without a scheme, port, or path. Must be unique across your entire workspace: the same name cannot be attached to two environments. The name must sit under a registrable domain: 'api.acme.co.uk' is accepted, the public suffix 'co.uk' itself is not. Internationalized names may be sent in Unicode or Punycode form; either way the domain is stored and returned in its canonical form, lowercase ASCII with Unicode labels Punycode encoded, and the DNS records in the response use that form.

## Response `200`

Domain created and verification started. The domain is `pending` until the DNS records resolve.

- V2DomainsCreateDomainResponseBody
  - `meta` Meta, required — Metadata object included in every API response. This provides context about the request and is essential for debugging, audit trails, and support inquiries. The `requestId` is particularly important when troubleshooting issues with the Unkey support team.
    - `requestId` string, required — A unique id for this request. Always include this ID when contacting support about a specific API request. This identifier allows Unkey's support team to trace the exact request through logs and diagnostic systems to provide faster assistance.
  - `data` V2DomainsCreateDomainResponseData, required
    - `domainId` string, required — Identifies a resource by either its unique ID or its slug. Accepts a prefixed ID (such as 'proj_' or 'app_') or a slug.
    - `dnsRecords` DnsRecord[], required — Every DNS record needed to finish setting up this domain, ready to create at your provider. The list already accounts for whether the domain is an apex or a subdomain, so no further branching is needed: create each entry as given. One record establishes routing and one proves ownership. Create all of them: whether ownership can be inferred from the routing record depends on how your provider publishes it, and a name another workspace has already verified can only be claimed through the ownership record. Neither is knowable before the records exist.
      - `type` 'CNAME' | 'ALIAS' | 'TXT', required — Record type to create. `ALIAS` is not a real DNS record type: it means an apex-compatible alias, which providers expose as ALIAS, ANAME, or a flattened CNAME. Apex domains cannot hold a plain CNAME, so they receive `ALIAS` where a subdomain receives `CNAME`.
      - `name` string, required — Fully qualified name of the record, ready to use as-is. Some providers want a name relative to the zone instead. Drop the zone and its trailing dot: in zone `acme.com`, `api.acme.com` becomes `api` and `_unkey.api.acme.com` becomes `_unkey.api`. A name equal to the zone itself is usually entered as `@`.
      - `value` string, required — The value to set on the record, exactly as given, including any prefix. Do not trim or reformat it: verification compares the published record against this string. Use the lowest TTL your provider allows until the domain is verified. Verification polls DNS, so a long TTL keeps a stale value cached and can burn the verification window on a value you have already corrected. Raise it afterwards if you want.
      - `ttl` integer, required — Seconds a resolver may cache this record. Set it in your provider alongside the record's name and value.
      - `verified` boolean, required — Whether Unkey has read this record back with the expected value. Use it to see which records are still outstanding. False does not always mean the record is missing. A provider that does not expose the published value to a DNS lookup, such as a proxied or flattened routing record, leaves this false for as long as it serves traffic; such a domain verifies through its TXT record instead. Always false on a domain no check has run against yet.
      - `note` string — What this record is for and any provider-specific caveat that applies to it. Worth surfacing to whoever edits the DNS zone. Treat it as optional: it carries no data the record itself needs, so a future record type may omit it.
    - `domainConnect` DomainConnect — One-click setup at the domain's DNS provider. Omitted entirely when the provider does not support Domain Connect or discovery failed, so the object's presence is the signal that the shortcut is available and both of its fields are filled.
      - `provider` string, required — Display name of the DNS provider the domain is delegated to, such as 'Cloudflare'. Discovered from the domain's nameservers, so it reflects where DNS is actually hosted rather than where the domain was registered.
      - `url` string, required — Signed Domain Connect URL that pre-fills the records in `dnsRecords` at the provider. Open it in a browser and the domain owner approves them in one step instead of entering them by hand. The URL is signed with an Unkey key, so it cannot be constructed or altered by the caller. Intended for a browser, not a script: after approval the provider sends the browser to this workspace's app settings page in the Unkey dashboard, so it suits a caller who administers this workspace. Anyone who does not have access to it approves the records successfully but lands on a page they cannot open. Approving is what writes the records; verification then proceeds on its own, so nothing depends on completing that return trip.

## Other responses

- `400` — Bad request
- `401` — Unauthorized
- `403` — Forbidden - The root key or its workspace is disabled, or the workspace has already attached as many custom domains as its plan allows. A root key that simply lacks `environment.*.create_domain` receives a 404 instead, so permissions cannot be used to discover which environments exist.
- `404` — Not Found - The environment does not exist in your workspace, or your root key may not read it.
- `409` — Conflict - The domain is already attached to an environment in this workspace.
- `429` — Too Many Requests
- `500` — Internal server error

---

[API](https://skmtc.net/unkeyed/apis/unkey-api.md) · [All operations](https://skmtc.net/unkeyed/apis/unkey-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/unkeyed/unkey-api/revisions/4bd11a7252bc/schema)
