Skip to content

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:

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:

RecordTypeNameValue
Points the domain at OwnrecordCNAMEpaycname.vercel-dns.com
Proves the domain is yoursTXT_ownrecord-challenge.payownrecord-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. 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, 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 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.