Skip to content

Add a custom domain to a brand

The brands.domains.add operation: POST /api/v1/brands/{id}/domains in the REST API and the brands_domains_add tool in the MCP server.

POST/api/v1/brands/{id}/domains

Adds a custom domain on which the brand's hosted checkout (purpose: checkout) or customer portal (purpose: portal) is served, and attaches it to Ownrecord's hosting. The answer has the DNS records to create at your DNS provider:

  • a CNAME for a subdomain such as pay.acme.com (recommended), or A records for a domain at the apex;
  • a TXT record _ownrecord-challenge.{host} with the value ownrecord-verify={token}, which proves the domain is yours. The token is new for every domain added;
  • a TXT record of the hosting provider, when it needs its own proof because another account uses the domain.

The domain is pending until its DNS points at Ownrecord and the challenge record is in place; then brands.domains.verify marks it verified. From then on, checkout links of the brand (the session url, waitlist and invoice emails) use the domain, without the brand's slug: https://pay.acme.com/cs_….

A brand has at most one checkout and one portal domain, and a host belongs to one brand only. The plan limits the custom domains of the whole organization (custom_domains.max). Needs brands:write.

At a glance

  • REST: POST /api/v1/brands/{id}/domains
  • MCP tool: brands_domains_add
  • Operation ID: brands.domains.add
  • Scope: Organization, or one of its legal entities or brands
  • Permission: brands:write
  • Risk tier: write
  • Dry run: Supported (send Ownrecord-Dry-Run: true)
  • Idempotency: Idempotency-Key required
  • Modes: Test and live
  • Authentication: API key with the brands:write scope
  • Since: M9

Request

Path parameters

FieldTypeDescription
idbrd_… ID

Body

A JSON object (Content-Type: application/json):

FieldTypeRequiredDescription
hoststringYesThe host name, e.g. pay.acme.com: one you registered, without https:// or a path.
purposeone of checkout, portalYescheckout serves the brand's hosted checkout, portal its customer portal.

Response

201 Created with a JSON object:

FieldTypeDescription
object"brand_domain"
hoststringThe host name, lower case (internationalised names in their xn-- form).
purposeone of checkout, portalcheckout serves hosted checkout, portal the customer portal.
brandIdstring
statusone of pending, verifiedverified once the host points at Ownrecord and the challenge TXT record proves it's yours; pending until then.
verifiedAtdate-time or null
urlstringThe domain's base URL; hosted pages are served under it once it's verified.
dnsRecordsarray of objectThe DNS records to create at the domain's DNS provider: routing, Ownrecord's challenge TXT record, and the hosting provider's TXT record when it asks for one.
dnsRecords[].typeone of A, AAAA, CNAME, TXT
dnsRecords[].namestring
dnsRecords[].labelstring
dnsRecords[].valuestring
dnsRecords[].purposeone of routing, ownership
checkobject or null

Examples

A checkout domain

HTTP
POST /api/v1/brands/brd_01m4tx5nj8emwbyf7y9vcf47x4/domains
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json

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

Response (201):

JSON
{
  "object": "brand_domain",
  "host": "pay.acme-app.com",
  "purpose": "checkout",
  "brandId": "brd_01m4tx5nj8emwbyf7y9vcf47x4",
  "status": "pending",
  "verifiedAt": null,
  "url": "https://pay.acme-app.com",
  "dnsRecords": [
    {
      "type": "CNAME",
      "name": "pay.acme-app.com",
      "label": "pay",
      "value": "cname.vercel-dns.com",
      "purpose": "routing"
    },
    {
      "type": "TXT",
      "name": "_ownrecord-challenge.pay.acme-app.com",
      "label": "_ownrecord-challenge.pay",
      "value": "ownrecord-verify=5e0c9a1b7d3f42e8a6b0c4d2e9f17a35",
      "purpose": "ownership"
    }
  ],
  "check": {
    "checkedAt": "2026-10-20T09:00:00.000Z",
    "ownershipVerified": true,
    "dnsConfigured": false,
    "challengeVerified": false,
    "problems": [
      {
        "code": "dns_not_configured",
        "message": "The host doesn't point at Ownrecord yet. Create the DNS records below; changes can take up to a few hours."
      },
      {
        "code": "challenge_missing",
        "message": "Ownrecord can't find its _ownrecord-challenge TXT record yet. Create it with the value below to prove the domain is yours."
      }
    ],
    "simulated": false
  }
}

A portal domain whose hosting provider asks for its own TXT record too

The domain is used by another account at the hosting provider, which asks for proof of ownership as well.

HTTP
POST /api/v1/brands/brd_01m4tx5nj8emwbyf7y9vcf47x4/domains
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json

{
  "host": "billing.acme-app.com",
  "purpose": "portal"
}

Response (201):

JSON
{
  "object": "brand_domain",
  "host": "billing.acme-app.com",
  "purpose": "portal",
  "brandId": "brd_01m4tx5nj8emwbyf7y9vcf47x4",
  "status": "pending",
  "verifiedAt": null,
  "url": "https://billing.acme-app.com",
  "dnsRecords": [
    {
      "type": "CNAME",
      "name": "billing.acme-app.com",
      "label": "billing",
      "value": "cname.vercel-dns.com",
      "purpose": "routing"
    },
    {
      "type": "TXT",
      "name": "_ownrecord-challenge.billing.acme-app.com",
      "label": "_ownrecord-challenge.billing",
      "value": "ownrecord-verify=c18d7e2f90ab4c3d8e5f6a7b1c2d3e4f",
      "purpose": "ownership"
    },
    {
      "type": "TXT",
      "name": "_vercel.acme-app.com",
      "label": "_vercel",
      "value": "vc-domain-verify=billing.acme-app.com,9b1f6c2e8d3a4f5b6c7d",
      "purpose": "ownership"
    }
  ],
  "check": {
    "checkedAt": "2026-10-20T09:00:00.000Z",
    "ownershipVerified": false,
    "dnsConfigured": false,
    "challengeVerified": false,
    "problems": [
      {
        "code": "ownership_unverified",
        "message": "The domain is used elsewhere on the hosting platform. Add the TXT record below to prove it's yours."
      },
      {
        "code": "dns_not_configured",
        "message": "The host doesn't point at Ownrecord yet. Create the DNS records below; changes can take up to a few hours."
      },
      {
        "code": "challenge_missing",
        "message": "Ownrecord can't find its _ownrecord-challenge TXT record yet. Create it with the value below to prove the domain is yours."
      }
    ],
    "simulated": false
  }
}

Errors

CodeHTTP statusWhenWhat to do
validation_failed400A field is missing or malformed.Fix the fields listed in details.issues and send the request again.
domain_invalid400The host isn't one a brand can use (a URL, an IP address, a reserved name; details.reason), or the domain provider refused it.Enter one host name you registered, such as pay.acme.com: no https://, path, port or wildcard, and not an IP address.
domain_taken409The host is attached to a brand already (this one or another).Use another domain, or remove it from the brand that uses it first.
domain_purpose_taken409The brand already has a domain for this purpose.Remove the brand's current domain for this purpose first, then add the new one.
domain_unavailable409Another project or account at the hosting provider holds the domain.Remove the domain from the other project or account that uses it, or use another host name, then try again.
entitlement_required403The organization's plan allows no more custom domains.Upgrade the organization's plan, or remove something to get back under the limit.
domain_provider_error502The domain provider failed; nothing was added.Retry after a short wait. If it keeps failing, contact support with the request ID.
domain_dns_unavailable502DNS didn't answer for the challenge record; nothing was added.Retry after a short wait. Nothing was changed.
domain_provider_not_configured500Custom domains aren't set up on this deployment.Contact support: the deployment's domain provider credentials are missing.
upstream_timeout502The domain provider or DNS didn't answer in time; nothing was added.Retry the request with the same Idempotency-Key after a short wait.
resource_not_found404No brand with this ID is visible to the request.Check the ID and that your credentials are scoped to the right organization, entity, brand and mode.
version_conflict409The brand changed at the same time; retry.Fetch the object again and retry the change.
permission_denied403You can't change this brand's settings.Ask an organization owner or admin for a role or scope that allows this action.

It can also return these common errors: api_version_unsupported, invalid_json, unsupported_media_type, authentication_required, api_key_invalid, idempotency_key_required, idempotency_key_reused, idempotency_in_progress, approval_required, approval_rejected, approval_expired, stale_approval, approval_policy_denied, rate_limited, internal_error.