CustomDomain™ docs
Getting started

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 providerThe modal offersThey type
Cloudflare, DigitalOcean, DNSimple, Linode (Akamai Cloud), Netlify, WordPress.comAuthorize with the provider (OAuth). On the hosted service the permission is kept, encrypted, so records can be repaired, and the modal says sonothing
Azure DNS, Dynadot, Unstoppable DomainsAuthorize 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 oncenothing
Google Cloud DNSThe project ID and an access token. The Google Cloud sign-in is not offered on the hosted service until Google verifies the appnothing
VercelInstall CustomDomain™ on Vercel, on the team that owns the domainnothing
PorkbunApprove access at Porkbun (API access must be on for the domain)nothing
NameSilo, Glauca Digital, Domain ChiefOne-click setup at the provider, which sends them back when they approvenothing
GoDaddy, Namecheap, Name.com, IONOS, OVHcloud, Route 53, a site builder or shop that sold the domain (Shopify, Wix, Squarespace, Canva and more), everything elseAdd 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 DNSthe records (about two minutes), or nothing
Unknown providera provider picker, then the recordsthe 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"; read rails.
  • 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 with platform_subdomain.

On this page