# Custom domains

> Serve a brand's checkout and customer portal on your own domain, such as pay.acme.com. Two DNS records, a verification, and every link of the brand uses it.

By default a brand's hosted pages live on Ownrecord's own address: `https://ownrecord.io/c/{brand}/…` for checkout and `https://ownrecord.io/portal/{brand}` for the customer portal. With a **custom domain** they're served on a domain you own instead, so customers stay on your name from the cart to the receipt.

A brand can have one custom domain for **checkout** and one for the **customer portal**. A domain belongs to one brand only.

## Adding a domain

In the console, open the brand, then **Settings** and **Manage domains**. Or call [`brands.domains.add`](https://ownrecord.io/docs/reference/brands-domains-add.md):

```http
POST /api/v1/brands/{brand}/domains
Idempotency-Key: 3c9a…

{ "host": "pay.acme.com", "purpose": "checkout" }
```

- `host` is the host name alone: `pay.acme.com`, without `https://`, a path or a port, and at most 232 characters. Names are stored in lower case, and internationalised names in their `xn--` form.
- `purpose` is `checkout` or `portal`.

Some names can never be a brand's domain: IP addresses, wildcards, reserved names that can't be registered (`.local`, `.test`, `example.com` and the like), and Ownrecord's own domains. Those are refused with `domain_invalid`, and `details.reason` says why.

Ownrecord attaches the domain to its hosting and answers with the domain, `pending`, and the DNS records to create.

## DNS records

Create the records at the company that runs your domain's DNS (your registrar or DNS provider). For `pay.acme.com`:

| Record                         | Type  | Name                       | Value                           |
| ------------------------------ | ----- | -------------------------- | ------------------------------- |
| Points the domain at Ownrecord | CNAME | `pay`                      | `cname.vercel-dns.com`          |
| Proves the domain is yours     | TXT   | `_ownrecord-challenge.pay` | `ownrecord-verify={your token}` |

- **A subdomain is easiest**: one CNAME record, and the rest of your domain is untouched. A domain at its apex (`acme.com`) can't have a CNAME, so it gets an A record at `@` to `76.76.21.21` instead.
- **The challenge record** carries a token that's new for every domain you add (32 letters and digits, shown in the console and in `dnsRecords`). Only someone who controls the domain's DNS can create it, so nobody can take over a domain whose DNS happens to point at Ownrecord already, for example records you set up for another brand or forgot to delete. Leave it in place: Ownrecord checks it every time the domain is checked.
- **The hosting provider's TXT record** (`_vercel`, value shown) is asked for only when another account at the hosting provider already uses the domain.

Use the exact values the console or the API shows for your domain: when checking a domain, the hosting provider may recommend a CNAME value of its own. Some DNS editors want the record's name without your domain (`_ownrecord-challenge.pay`), others the full name (`_ownrecord-challenge.pay.acme.com`); the API gives both (`label` and `name`).

DNS changes can take from a few minutes to a few hours to be seen.

## Verification

Once the records are in place, choose **Check again** in the console, or call [`brands.domains.verify`](https://ownrecord.io/docs/reference/brands-domains-verify.md). The domain is `verified` when:

- it's attached to Ownrecord's hosting and its DNS points at Ownrecord,
- the hosting provider confirmed its owner (its own TXT record, when it asked for one), and
- the `_ownrecord-challenge` TXT record carries the domain's token.

Until then the answer lists what's missing in `check.problems` (`dns_not_configured`, `challenge_missing`, `challenge_mismatch`, `ownership_unverified`, `not_attached`) and the records still needed in `dnsRecords`. Checking again is safe at any time; if DNS doesn't answer, the check fails with `domain_dns_unavailable` and nothing changes.

Every check looks at both again. A verified domain that later stops pointing at Ownrecord, or whose challenge record is deleted or changed, goes back to `pending` the next time it's checked, and links stop using it until it's verified again. A domain that isn't verified serves nothing.

## What changes once it's verified

- **Checkout links** use the domain, without the brand in the path: a checkout session's `url` is `https://pay.acme.com/{session}?key=…`. The pages keep working at `https://pay.acme.com/c/{brand}/…` too.
- **Customer emails** of the brand link to the domain: waitlist confirmations, payment reminders and failed-payment emails (the hosted invoice page), and the invoice and credit note downloads.
- **The portal** is served on the portal domain at its root. Without a portal domain, portal links use the checkout domain at `/portal/{brand}`.

A checkout domain serves only that brand's hosted pages, and the public API calls those pages make. The console, sign-in, other brands' pages and the rest of the API aren't served on it, and cookies set there (the portal's session, for example) belong to that domain alone.

Responses on your domain carry `Strict-Transport-Security` for two years, without `includeSubDomains`, so your other subdomains aren't affected.

## Removing a domain

**Remove** in the console, or [`brands.domains.remove`](https://ownrecord.io/docs/reference/brands-domains-remove.md), takes the domain off the brand at once. Within a minute its pages stop being served there, and links sent earlier that use it stop working. New links use Ownrecord's own address (or the brand's other verified domain). Ownrecord then detaches the domain from its hosting in the background; delete the DNS records whenever you like. Adding the domain again later, to this brand or another, gives it a new token, so the old challenge record no longer proves anything.

## Plan limits

Your plan sets how many custom domains the organization can have, counted across all its brands (`custom_domains.max`: 2 on Growth, 10 on Scale). [`brands.domains.list`](https://ownrecord.io/docs/reference/brands-domains-list.md) shows the brand's domains and the organization's `usage` against the limit. Adding one more than the plan allows is refused with `entitlement_required`; domains you already have keep working after a downgrade.

## In development

Development and test deployments never call the hosting provider or real DNS. Fakes answer instead:

- names ending in `.localhost` or `.test` are verified at once, challenge record included. Browsers send every `*.localhost` name to your own machine, so `http://pay.acme.localhost:3000/…` reaches the development server and shows the brand's checkout;
- names under `unproven.test` never have a challenge record, so they stay `pending`;
- names under `pending.test` ask for the hosting provider's TXT record and never verify; names under `in-use.test` are refused as used by another account;
- any other name is attached and stays `pending`.
