Widget, let your customers connect their domains
Let your customers connect domains inside your product: mint a widget token on your server, open the modal, and see what customers get per DNS provider.
Use the widget when your customers own the domains and should connect them inside your product. You never touch their DNS; the modal detects the provider, offers the best rail it can actually complete, and reports back.
No customer domain to try it with yet? Make a test mode
application and follow the steps below with its client_secret and a name such as
www.acme.test. The widget runs end to end on a simulated DNS, with no card, no free
domain used and nothing billed.
1. Mint a widget token on your server
Your server trades a credential for a short-lived widget token, and only that token reaches the browser. If you signed up in the console (most customers), create an API key on API & SDK with Create key and call the console's token endpoint. This is what the Install tab on the Widget page generates by default:
curl -X POST https://app.customdomain.ai/api/widget-token \
-H "Authorization: Bearer $CUSTOMDOMAIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"application_id":"app_...","domain":"shop.acme.com"}'
# → 200 { "application_id": "app_...", "token": "eyJ…", "expires_in": 900, "token_type": "Bearer" }If you created the application over the API, you have its client_secret
instead; mint with POST /v1/tokens. The console never shows a client secret, so
an API key is not a substitute for it. Both calls, the errors and the difference
between the two credentials are on Widget tokens.
domain binds the token to one hostname (recommended). Tokens last 15
minutes, so mint one per open and pass getToken (step 2) so a long manual
setup refreshes instead of stopping. The default scope, connect, runs the
connect flow and nothing else.
Mint it with the domain and your customer's end_user_ref too
("end_user_ref":"user_42" beside domain) where you can. Some providers can be
signed in through a secure sign in, a temporary browser where your customer signs
in to the provider (offered to selected workspaces for now), and it runs only
for a token bound to the domain and the end user. If your platform asks the
customer for the domain first, mint the token after that. A token minted before
the domain is known still runs the whole flow; the widget then shows the manual
setup for those providers instead.
2. Open the modal
<script src="https://app.customdomain.ai/widget-assets/customdomain-sdk.js"></script>
<script>
customdomain.open({
applicationId: "app_...",
token: "<widget JWT from step 1>",
getToken: () => fetch("/api/customdomain-token", { method: "POST" })
.then((r) => r.json()).then((d) => d.token), // your endpoint for step 1
domain: "shop.acme.com",
endUserRef: "user_42", // your id for this customer
onSuccess: ({ domain }) => markConnected(domain),
onClose: ({ lastStatus, error }) => log(lastStatus, error),
});
</script>endUserRef is your id for the customer: it is stored on the connection as
end_user_ref, shown in the console, and sent with every webhook as
user_id. Every
screen change fires customdomain:step; a failure fires customdomain:error
with a branchable code. Full options and events:
Widget reference.
When the customer types a root domain (acme.com), the widget asks whether to
connect it with www (recommended), on its own, or as a subdomain instead;
pass wwwRedirect to decide for them. See Root domains.
3. What your customer will see, by provider
| Their DNS provider | The modal offers | They type |
|---|---|---|
| Cloudflare, DigitalOcean, DNSimple, Linode (Akamai Cloud), Netlify, WordPress.com | Authorize with the provider (OAuth). On the hosted service the permission is kept, encrypted, so records can be repaired, and the modal says so | nothing |
| Azure DNS, Dynadot, Unstoppable Domains | Authorize with the provider (Dynadot and Unstoppable Domains through their MCP servers; Dynadot needs MCP switched on in its API settings first). The permission is used once | nothing |
| Google Cloud DNS | The project ID and an access token. The Google Cloud sign-in is not offered on the hosted service until Google verifies the app | nothing |
| Vercel | Install CustomDomain™ on Vercel, on the team that owns the domain | nothing |
| Porkbun | Approve access at Porkbun (API access must be on for the domain) | nothing |
| NameSilo, Glauca Digital, Domain Chief | One-click setup at the provider, which sends them back when they approve | nothing |
| GoDaddy, Namecheap, Name.com, IONOS, OVHcloud, Route 53, a site builder or shop that sold the domain (Shopify, Wix, Squarespace, Canva and more), everything else | Add these records, with why the automatic rails aren't available, the Name to type in their panel, step-by-step instructions for their provider (135 providers and platforms have their own), and a live check of each record; or hand the job to an AI assistant, email it to whoever manages DNS, or (where offered) let CustomDomain™ host the DNS | the records (about two minutes), or nothing |
| Unknown provider | a provider picker, then the records | the records |
This table is what POST /v1/domains:check returns as rails; the widget
reads it live, so you never have to maintain it.
4. Records you supply
For records only you know (SES DKIM, a verification TXT), create the
connection server-side first (POST /v1/connections, then
PUT /v1/connections/{id}/records with your API key), and open the widget
for that domain: it shows your records. An email-only set (no edge record) is
verified and reported but never served and gets no certificate; that is
by design. Bring your own records.
5. After the modal closes
After a sign-in or one-click setup writes the records, the modal waits for DNS to show them, which usually takes seconds. Past a minute it tells the customer the wait is unusual and that they can close the window, because the domain connects on its own as soon as DNS updates.
onSuccess fires when the domain is live. If the customer closes early,
the connection keeps being checked (pending never fails on its own); the
next open resumes it. A failed connection is recoverable in the modal
itself: the records stay on screen with the reason, and Check now
re-enters verification. If the customer emailed the instructions to someone
else, that person finishes from the link in the email (the modal reports
FINISHED_SUCCESSFULLY_LINK_SHARED). Subscribe to connection.live /
connection.failed webhooks (each with user_id and, when present,
error_code / error_message), and poll GET /v1/connections/{id} as the
fallback.
Don't
- Don't gate your UI on
setupType === "automatic"; readrails. - Don't send your API key or client secret from the page; the widget JWT is the browser credential.
- Don't call
customdomain.open()for*.vercel.app-style hosts; the pre-flight refuses them withplatform_subdomain.
API, provision domains from your server
Provision custom domains from your server with an API key: pre-flight check, create, apply records, verify and react, plus mistakes integrators actually make.
MCP: drive it from an agent
Let an AI agent discover providers, start domain connections and read their status through the hosted MCP server, with API key or scoped agent token access.