API — provision domains from your server
The server-to-server path with an sk_ key — pre-flight honestly, create, apply, verify, react — and the mistakes integrators actually make.
Use the API when your server decides which domains get connected — a platform that provisions a custom domain per customer, a migration script, an internal tool. The browser-embedded path is the widget.
Base URL for the hosted service: https://api.customdomain.ai. Auth:
Authorization: Bearer sk_… from the console's Keys page.
1. Pre-flight — and read what it says
curl -X POST https://api.customdomain.ai/v1/domains:check \
-H "Authorization: Bearer sk_..." -H "Content-Type: application/json" \
-d '{"domain":"shop.acme.com"}'Branch on rails and recommended_rail, not on setup_type:
automatic only means an adapter exists (for GoDaddy that is an API key most
accounts cannot get). end_user_automatic: true is the only field that means
"nobody has to type anything". A blocked_reason (unregistered,
platform_subdomain) means stop and tell the user why. dashboard_url is
where to send them for manual records. If you supply your own records, pass
them here too so conflicts are computed against the real set.
2. Create
curl -X POST https://api.customdomain.ai/v1/connections \
-H "Authorization: Bearer sk_..." -H "Content-Type: application/json" \
-d '{"domain":"shop.acme.com","application_url":"https://tenant-42.yourapp.com"}'201 with the connection and its desired records. Re-posting the same
domain returns the existing connection with 200; if the only existing one is
failed, it is revived and returned. Idempotent by design — call it on every
"connect" click.
For records only you know (SES DKIM, verification TXT), follow with
PUT /v1/connections/{id}/records — Bring your own records.
3. Apply (optional)
If you hold a provider credential for the zone, write the records yourself:
curl -X POST https://api.customdomain.ai/v1/connections/<ID>/apply \
-H "Authorization: Bearer sk_..." -H "Content-Type: application/json" \
-d '{"provider":"cloudflare","credential":"<scoped token>"}'A rejected credential is 422 ProviderAuthenticationError; any other
provider refusal is 422 dns_write_failed with the provider's reason.
Route 53 temporary (ASIA…) keys also need the session token. Otherwise, show
the records from GET /v1/connections/{id}/records and let the user add them.
4. Verify and react
Subscribe to webhooks (connection.live, connection.failed — both carry
error_code / error_message when there is one) and poll
GET /v1/connections/{id} as the fallback. pending never fails on its own;
propagating fails after 24 hours; POST /v1/connections/{id}:recheck
revives a failed connection.
Mistakes we see
- Promising "automatic" from
setup_type. Userails. - Telling users "unregistered" on a resolver hiccup. Only act on
registered === falsewhen the field is present; a transient failure omits it and setsdetection_error: resolver. - Sending
Bearer sk_from a browser. Mint a widget JWT server-side (POST /v1/tokens) and use that in the page. - Treating
connection.appliedas verified. Applied means written; onlylivemeans resolving and served. - Inventing the edge CNAME for an email-only connection. A connection whose records have no edge record is never served and gets no certificate — by design.
Console — connect your own domain
The no-code path — from sign-up to a domain serving your app over HTTPS, and what every screen is actually doing.
Widget — let your customers connect their domains
The embedded path — mint a token on your server, open the modal in your UI, and know exactly what your customer will see for their DNS provider.