Check a custom domain again
The brands.domains.verify operation: POST /api/v1/brands/{id}/domains/{host}/verify in the REST API and the brands_domains_verify tool in the MCP server.
/api/v1/brands/{id}/domains/{host}/verifyAsks the domain provider to verify one of the brand's custom domains again, looks up the domain's
_ownrecord-challenge TXT record, and records the answer: verified once the domain is attached, its DNS points
at Ownrecord and the challenge record carries the domain's token (ownrecord-verify={token}); pending
otherwise, with the records still needed in dnsRecords and what is missing in check.problems.
A verified domain that no longer passes, for example because the challenge record was deleted, goes back to
pending: its pages stop being served and links stop using it until it is verified again. DNS changes can take a
few hours to be seen; checking again is safe at any time.
At a glance
- REST:
POST /api/v1/brands/{id}/domains/{host}/verify - MCP tool:
brands_domains_verify - Operation ID:
brands.domains.verify - 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-Keyrequired - Modes: Test and live
- Authentication: API key with the
brands:writescope - Since: M9
Request
Path parameters
| Field | Type | Description |
|---|---|---|
id | brd_… ID | |
host | string | The domain's host name, e.g. pay.acme.com. |
Body
This operation takes no body.
Response
200 OK with a JSON object:
| Field | Type | Description |
|---|---|---|
object | "brand_domain" | |
host | string | The host name, lower case (internationalised names in their xn-- form). |
purpose | one of checkout, portal | checkout serves hosted checkout, portal the customer portal. |
brandId | string | |
status | one of pending, verified | verified once the host points at Ownrecord and the challenge TXT record proves it's yours; pending until then. |
verifiedAt | date-time or null | |
url | string | The domain's base URL; hosted pages are served under it once it's verified. |
dnsRecords | array of object | The 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[].type | one of A, AAAA, CNAME, TXT | |
dnsRecords[].name | string | |
dnsRecords[].label | string | |
dnsRecords[].value | string | |
dnsRecords[].purpose | one of routing, ownership | |
check | object or null |
Examples
DNS points at Ownrecord and the challenge record is in place
POST /api/v1/brands/brd_01m4tx5nj8emwbyf7y9vcf47x4/domains/pay.acme-app.com/verify
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Response (200):
{
"object": "brand_domain",
"host": "pay.acme-app.com",
"purpose": "checkout",
"brandId": "brd_01m4tx5nj8emwbyf7y9vcf47x4",
"status": "verified",
"verifiedAt": "2026-10-20T09:40:00.000Z",
"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:40:00.000Z",
"ownershipVerified": true,
"dnsConfigured": true,
"challengeVerified": true,
"problems": [],
"simulated": false
}
}
Still waiting for the DNS records
POST /api/v1/brands/brd_01m4tx5nj8emwbyf7y9vcf47x4/domains/pay.acme-app.com/verify
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Response (200):
{
"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
}
}
Errors
| Code | HTTP status | When | What to do |
|---|---|---|---|
validation_failed | 400 | A field is missing or malformed. | Fix the fields listed in details.issues and send the request again. |
resource_not_found | 404 | The brand isn't visible to the request, or has no domain with this host. | Check the ID and that your credentials are scoped to the right organization, entity, brand and mode. |
domain_provider_error | 502 | The domain provider failed; nothing changed. | Retry after a short wait. If it keeps failing, contact support with the request ID. |
domain_dns_unavailable | 502 | DNS didn't answer for the challenge record; nothing changed. | Retry after a short wait. Nothing was changed. |
domain_provider_not_configured | 500 | Custom domains aren't set up on this deployment. | Contact support: the deployment's domain provider credentials are missing. |
upstream_timeout | 502 | The domain provider or DNS didn't answer in time; nothing changed. | Retry the request with the same Idempotency-Key after a short wait. |
version_conflict | 409 | The brand changed at the same time; retry. | Fetch the object again and retry the change. |
permission_denied | 403 | You 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.