---
title: "Add a custom domain"
method: POST
path: "/api/domains/add"
tags: ["Domains"]
---

# Add a custom domain

`POST /api/domains/add`

## Request

- Base URL: `https://www.flyn.to`
- URL: `https://www.flyn.to/api/domains/add`
- Auth: HTTP bearer

Creates a pending custom domain and returns the DNS records to publish. The domain is not attached for serving yet; POST /api/domains/{id}/verify does that once the TXT record is in place. Also reachable as POST /api/domains, with the same body, responses and rate-limit bucket.

**Auth:** API key (`Bearer flyn_sk_live_...`), or the signed-in dashboard session cookie when no API key header is sent. MCP OAuth access tokens are not accepted here and get 401.

**Plan:** Pro, Lifetime or Team (a member of a paying owner's workspace has the effective plan `team`) only; Free gets 403 `UPGRADE_REQUIRED` before the body is read. At most 3 domains per workspace (400 `LIMIT_REACHED`, checked before body validation).

**Rate limit:** 10 requests per 60 s per client IP, fixed window, in memory per server instance, checked before authentication.

## Request body

- DomainCreateRequest
  - `domain` string, required: Domain or subdomain you own. Lower-cased; a leading http(s):// and www. and trailing slashes are stripped. Must contain a dot and match `^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$`. Flyn domains and well-known public platforms are refused. A non-string value returns 500.

## Response `200`

Created. Publish the TXT record, add the routing record, then call verify.

- DomainCreateResponse
  - `domain` Domain, required: A custom domain row, returned with its database column names.
    - `id` string, uuid, required
    - `user_id` string, uuid, required: Who added it.
    - `team_id` string, uuid, nullable, required: Workspace it belongs to, or null for a personal domain.
    - `domain` string, required: Lower-case host, for example go.brand.com.
    - `slug` string, required: The domain with dots replaced by hyphens. Unique, so a-b.com and a.b.com collide.
    - `verified` boolean, required
    - `status` 'pending' | 'active' | 'invalid' | 'expired', required
    - `txt_record` string, nullable, required: Ownership token to publish as a TXT record, `flyn-verify=<32 hex>`.
    - `cname_target` string, nullable, required: Routing target: an A record value for an apex domain, otherwise a CNAME target.
    - `last_checked_at` string, date-time, nullable
    - `verified_at` string, date-time, nullable
    - `placeholder` string, nullable: Stored placeholder text. Nothing at runtime reads it yet.
    - `not_found_url` string, nullable: Stored not-found URL. Nothing at runtime reads it yet: an unknown slug always gets the standard dead-link response.
    - `created_at` string, date-time, required
    - `updated_at` string, date-time, required
    - `pause_warned_at` string, date-time, nullable
    - `pause_scheduled_at` string, date-time, nullable
    - `paused_at` string, date-time, nullable: Set when the domain is paused; links on a paused domain stop redirecting.
  - `vercel` object, required
    - `verified` false, required: Always false here: the domain is attached for serving only by the verify step, after the TXT record matches.
  - `dnsInstructions` object, required
    - `txt` DnsRecord, required
      - `type` 'TXT' | 'CNAME' | 'A', required
      - `name` string, required
      - `value` string, required
    - `routing` DnsRecord, required
      - `type` 'TXT' | 'CNAME' | 'A', required
      - `name` string, required
      - `value` string, required

## Other responses

- `400`: At the 3-domain cap (code `LIMIT_REACHED`, `limit` 3), or the domain is invalid or refused. Messages: "Please enter a valid domain (e.g. links.yourbrand.com)", "Invalid domain format", "You cannot add a Flyn domain as a custom domain", "This domain cannot be added. Use a domain or subdomain you own, for example links.yourbrand.com.".
- `401`: No valid API key and no dashboard session. Messages: "Invalid or revoked API key", "API key has expired", "Unauthorized. Sign in to the dashboard, or pass a Bearer API key in the Authorization header.", or "Unauthorized" when the session lookup itself fails. An MCP OAuth token lands here too.
- `403`: Free plan (code `UPGRADE_REQUIRED`) or a suspended account.
- `409`: The domain (or its hyphenated slug) is already registered.
- `429`: More than 10 requests in the window. Counted per client IP. Retry-After gives the seconds until the window resets.
- `500`: Invalid JSON, a non-string domain, or a database error.

---

[API](https://skmtc.dev/flyn/apis/flyn-api.md) · [All operations](https://skmtc.dev/flyn/apis/flyn-api/llms.txt) · [OpenAPI document](https://skmtc.dev/flyn/apis/flyn-api/revisions/977962a98ccb?raw)
