---
title: "Add a custom domain (optionally redirecting to another), or a wildcard domain such as *.example.com (docs/PLATFORM_API.md \"Wildcard domains\"; not_configured while free certificates are off); returns the DNS records, including the ownership TXT record"
method: POST
path: "/projects/{projectId}/domains"
tags: ["Domains"]
---

# Add a custom domain (optionally redirecting to another), or a wildcard domain such as \*.example.com (docs/PLATFORM_API.md "Wildcard domains"; not_configured while free certificates are off); returns the DNS records, including the ownership TXT record

`POST /projects/{projectId}/domains`

## Request

- Base URL: `https://hostingsurge.com/api/v1`
- URL: `https://hostingsurge.com/api/v1/projects/{projectId}/domains`
- Auth: one of:
  - HTTP bearer
  - API key in cookie `__Host-hs_session`

## Path parameters

- `projectId` string, uuid, required

## Request body

- object
  - `hostname` string, required: e.g. www.example.com; *.example.com serves every name one label below example.com (not example.com itself)
  - `redirectTo` string: never for a wildcard domain
  - `redirectStatusCode` union
    - 301
    - 302
    - 307
    - 308

## Response `201`

Added (verification attempted)

- object
  - `id` string, uuid, required
  - `hostname` string, required: a hostname, or a wildcard domain such as *.example.com (see wildcard)
  - `kind` 'default' | 'custom', required
  - `redirectTo` string, nullable, required
  - `redirectStatusCode` union, required: status used when redirectTo is set
    - 301
    - 302
    - 307
    - 308
  - `status` 'valid' | 'pending' | 'invalid', required: valid: ownership verified and DNS points at the platform (the domain serves); pending: waiting for DNS or the ownership TXT record; invalid: DNS points elsewhere
  - `statusReason` string, nullable, required: the result of the last check in words, e.g. which record is missing
  - `verification` object, required: Ownership proof. When no other team uses the domain (or a name under its registrable domain), the A/CNAME record pointing here proves it, like on Vercel. Otherwise, and for a wildcard DNS record, a TXT record _hostingsurge.<hostname> with the value hostingsurge-verify=<token> (token per organization + hostname) is needed as well. Unverified claims never block other teams and are released after 7 days; only the TXT proof moves a domain that another team holds. Hostnames under the platform app domain need nothing.
    - `status` 'verified' | 'pending', required: verified: ownership proven (or platform-owned); pending: not proven yet
    - `method` 'routing' | 'txt' | 'delegation', required: what proves ownership while pending: routing = the A/CNAME record pointing here is enough (nobody else on the platform uses this domain or its registrable domain); txt = the TXT record `record` is needed as well (txtReason); delegation = a wildcard domain: its base verified by this team and the _acme-challenge delegation (see Domain.wildcard)
    - `txtReason` 'proof_required' | 'wildcard_dns' | 'null', nullable, required: why the TXT record is needed: wildcard_dns = a wildcard DNS record points the whole zone here (visible in public DNS); proof_required = the platform asks for the TXT proof for this name, without saying why (the reason would tell anyone whether the name is or was used on the platform). null when routing is enough or once verified
    - `record` object, nullable, required: the TXT record that proves ownership while pending (name fully qualified; needed when method is txt, accepted either way); null once verified
      - `type` 'A' | 'AAAA' | 'CNAME' | 'TXT', required
      - `name` string, required
      - `value` string, required
    - `verifiedAt` string, nullable, required: ISO-8601 timestamp
    - `expiresAt` string, nullable, required: an unverified claim is released at this time
  - `ssl` 'active' | 'pending' | 'error' | 'none', required
  - `requiredRecords` object[], required: exactly what the customer must configure at their DNS provider (names relative to the registrable domain, "@" = apex); includes the ownership TXT record only while it is needed (verification.method txt)
    - `type` 'A' | 'AAAA' | 'CNAME' | 'TXT', required
    - `name` string, required
    - `value` string, required
  - `observedRecords` object[], required: what public DNS answered at checkedAt (fully qualified names)
    - `type` 'A' | 'AAAA' | 'CNAME' | 'TXT', required
    - `name` string, required
    - `value` string, required
  - `phase` 'waiting_dns' | 'dns_points_here' | 'issuing_https' | 'live', required: waiting_dns: the records do not point here yet; dns_points_here: they do (ownership or the first deployment may still be missing); issuing_https: attached, the certificate is being issued; live: served over HTTPS
  - `issues` object[], required: what is in the way, from the last check (empty when nothing is)
    - `code` 'no_record' | 'points_elsewhere' | 'extra_records' | 'conflicting_aaaa' | 'cloudflare_proxy' | 'caa_blocks' | 'propagating' | 'lookup_failed' | 'nameservers_not_asked' | 'txt_needed' | 'not_deployed' | 'https_error', required: no_record: nothing answers for the name yet; points_elsewhere: the records point at another server (values: where, usually the old host); extra_records: other A records next to ours (values); conflicting_aaaa: AAAA records send IPv6 visitors and Let’s Encrypt elsewhere (values); cloudflare_proxy: the Cloudflare proxy (orange cloud) is on; caa_blocks: a CAA record forbids Let’s Encrypt (values: the records, name: where); propagating: the DNS provider already answers with our record, public resolvers may keep the old one for ttlSeconds; lookup_failed: DNS did not answer in time; nameservers_not_asked: the domain’s own nameservers resolve only to addresses the platform never queries (private, loopback, link-local…), so only the public resolvers’ answer is known (values: the nameservers); txt_needed: the TXT record is needed too (values: [txtReason]); not_deployed: goes live with the first deployment; https_error: HTTPS does not work yet
    - `values` string[], required
    - `name` string
    - `ttlSeconds` integer, nullable
  - `dnsProvider` object, nullable, required: who answers DNS for the domain; null when unknown or not checked yet
    - `id` string, required: e.g. cloudflare, loopia, one-com, binero, godaddy, namecheap, dynadot, websupport, squarespace, route53
    - `name` string, required
    - `nameservers` string[], required
    - `dnsUrl` string, nullable, required: the provider’s DNS settings page
  - `wildcard` object: present only for a wildcard domain (*.example.com)
    - `base` string, required: the domain the wildcard is under (*.example.com -> example.com); it is not covered itself
    - `available` boolean, required: free certificates (the platform's ACME client) are switched on: without them a wildcard cannot go live
    - `baseVerified` boolean, required: the base is a verified domain of this team (on any of its projects) or a name it bought here
    - `otherTeamBelow` boolean, required: another team serves a name directly under the base: the wildcard cannot be verified while it does
    - `delegation` object, required
      - `record` object, nullable, required: _acme-challenge.<base> CNAME <label>.<acme zone> (fully qualified name), once known
        - `type` 'A' | 'AAAA' | 'CNAME' | 'TXT', required
        - `name` string, required
        - `value` string, required
      - `status` 'verified' | 'missing' | 'mismatch' | 'unchecked', required
      - `observed` string[], required: CNAME targets public DNS answered at the last check
    - `certificate` object, required: the *.base certificate from the platform's ACME client (issued after ownership is verified, renewed by itself)
      - `status` 'active' | 'pending' | 'error' | 'unavailable', required
      - `reason` string, nullable, required
  - `checkedAt` string, nullable, required: ISO-8601 timestamp
  - `createdAt` string, required: ISO-8601 timestamp

## Other responses

- `400`: Invalid request (code invalid_request)
- `401`: Missing or invalid session/token (code unauthorized)
- `402`: limit_reached (too many domains on the project), or upgrade_required: a custom domain on a site made with the AI builder needs a paid plan (details.feature builder.custom_domain)
- `403`: Authenticated but not permitted (code forbidden)
- `404`: Not found or not visible to the caller (code not_found)
- `409`: Conflicting state (code conflict)
- `429`: Too many requests (code rate_limited); see Retry-After
- `503`: Hosting engine unavailable (code provider_unavailable)

---

[API](https://skmtc.dev/hostingsurge/apis/hostingsurge-platform-api.md) · [All operations](https://skmtc.dev/hostingsurge/apis/hostingsurge-platform-api/llms.txt) · [OpenAPI document](https://skmtc.dev/hostingsurge/apis/hostingsurge-platform-api/revisions/4d08337cdff1?raw)
