Domain verification: publish another service's TXT record
When your app asks customers to add a TXT record for another service (single sign-on, Search Console, a Bluesky handle), hand that record to CustomDomain™: written or shown exactly, checked, then a webhook.
Many services ask a customer to prove they control a domain by adding a DNS
record: an identity provider before it turns on single sign-on for the domain,
Google Search Console for a Domain property, Bluesky for a handle. If your app
passes one of those records on to your customers, CustomDomain™ can carry it the
rest of the way. Your server supplies the record. It is written at the
customer's provider where their sign-in or API key allows it, or into the zone
CustomDomain™ hosts when the domain uses DNS hosting, and shown as the exact
row everywhere else. CustomDomain™ checks that it resolves and tells your
server with connection.live.
Does CustomDomain™ verify the domain for my app? No. It writes or shows
your record and checks that a TXT at that name carries your value. The
service that issued the token runs its own check. Your customer adds no record
for CustomDomain™ itself, and the token always comes from the service that asks
for it.
How it works
- Get the value from the issuing service. For example, WorkOS returns a
verification_tokenfor the domain. - Create a connection for the customer's domain from your server with an
API key:
POST /v1/connectionswith{"domain": "acme.com"}. See Create a connection. - Supply the record with
PUT /v1/connections/{id}/records. It replaces the connection's default set, so the connection asks for your record and nothing else. See Bring your own records. - Hand it to the customer. Open the widget for the same domain:
POST /v1/connectionsis idempotent per application and domain, so the widget's own create returns this connection and shows your record. If someone else runs the customer's DNS, email them the steps instead with a share link (POST /v1/sharing/connectwithrecipient_emailandnote). - Wait for
connection.live. It fires when every record in the set resolves. Then run your check, or ask the issuing service to run its own.
# 2. Create the connection (your server, API key)
curl -X POST https://api.customdomain.ai/v1/connections \
-H "Authorization: Bearer cd_live_..." \
-H "Content-Type: application/json" \
-d '{"domain": "acme.com"}'
# 3. Supply the record before the widget opens
curl -X PUT https://api.customdomain.ai/v1/connections/<ID>/records \
-H "Authorization: Bearer cd_live_..." \
-H "Content-Type: application/json" \
-d '{"records": [
{ "type": "TXT", "host": "@", "value": "verification_token=<TOKEN>" }
]}'The calls are the real endpoints. The token is a placeholder for the one the issuing service gives you.
A connection whose set has no record pointing at the CustomDomain™ edge is a
record-delivery connection: it goes live, fires the same webhooks and is
listed like any other, but it is not served and gets no certificate.
What your customer sees
| Way to connect | Your record |
|---|---|
| Sign in with the provider | Written as supplied |
| A provider API key | Written as supplied |
| One-click setup | Refused. The customer adds the record by hand |
| Secure sign-in | Refused before the customer signs in. The widget says "This setup includes records this sign in cannot add yet: TXT." and moves to the manual steps |
| DNS hosting (when your application offers it) | Written into the zone CustomDomain™ hosts for the domain, and served once the nameservers point there. See Managed DNS |
| Add the records | The manual screen shows the exact row ("Add 1 record at" the provider, Name, Value and Copy), checks it with Check now, and offers Email these instructions to someone else |
Where the record is written, the values already at that name stay: another service's token or the domain's SPF policy is not removed. The widget can still offer one-click setup or secure sign-in for such a domain. Starting either is refused, and the customer goes on with the manual steps.
Recipes
The record formats below are each service's own. The tokens are placeholders.
WorkOS (single sign-on and domain claims)
WorkOS documents the record as Name domain-to-verify.com, Value
verification_token= followed by the token it returns, and checks for it
periodically.
{ "records": [ { "type": "TXT", "host": "@", "value": "verification_token=<TOKEN>" } ] }When connection.live arrives, read the domain's state from WorkOS, which says
the domain endpoint can be polled once verification has started. WorkOS marks a
domain failed if it has not verified within thirty days.
Google Search Console (Domain property)
A Domain property is verified by DNS only. Search Console gives a TXT value
for the root of the domain.
{ "records": [ { "type": "TXT", "host": "@", "value": "google-site-verification=<TOKEN>" } ] }Search Console checks periodically that the token is still there, and access to the property expires after a grace period if it is not. Leave the record in place.
Bluesky handle
A handle is proved with a TXT at _atproto under the handle's own name, with
a value that starts did=.
{ "records": [ { "type": "TXT", "host": "_atproto", "value": "did=did:plc:<ID>" } ] }A relative host is anchored to the connection's own name, so on a connection for
acme.com this is _atproto.acme.com, the record for the handle acme.com. For
the handle alice.acme.com, use the host _atproto.alice.acme.com (any host at
or under the registrable domain is accepted).
Change the wording
The widget's words are for connecting a domain: its last screen says "Live".
For this job you can change them with whiteLabel.customCopy on a
direct open(), keyed by locale ("*" for all) and then by copy key:
window.customdomain.open({
applicationId, token,
domain: "acme.com",
whiteLabel: {
customCopy: {
"*": {
"done.title": "Record found",
"done.manual.title": "Record found",
},
},
},
});A teammate who opens a share link gets your branding only on the Enterprise plan. On other plans the resumed flow uses the default look and words. See Plans and quotas.
Limits
- Not in test mode. A test mode key gets
403 sandbox_not_allowedfromPUT /v1/connections/{id}/recordsandPOST /v1/sharing/connect. Try the flow with a live key on a domain you control. - Fixed once written. After the record is written, or the connection is
live, a different set is refused with
409. To change the token, disconnect and connect the domain again. A token that rotates on every check, such as_acme-challenge, does not fit. - One account per name. A domain another CustomDomain™ account already
serves
liveis refused at create (409 domain_already_connected). - Checked hourly, loosely. Every live domain is checked once an hour. A
TXTcounts when one of its values contains yours, compared in lowercase, so a value the issuing service would reject can still read as found here. See Monitoring. When you remove the domain, choose to keep its records if the other service still needs the token (Removing a domain). - A domain on your plan. Each connection counts like any other domain (Plans and quotas).
- No MCP tool takes the record. No MCP tool accepts DNS records as input. Supply them from your server.
Managed DNS
Point a domain's nameservers at CustomDomain™ once and every later record is written for you. How DNS hosting works, the API and the limits.
Domain maintenance (nameserver migration)
Keep your users' domains working for them: with their consent, one setting where they bought the domain hands its DNS to you. Setup, the registrar, the copy, steps, DNSSEC, moving back, the API.