Which path are you on?
Custom Domain is one product with four front doors — the console, the API, the embeddable widget, and the MCP server. Pick yours, then read the truths every path shares.
Custom Domain is one product with four front doors. The model underneath is
the same everywhere: a workspace has applications; an application owns
connections (and its origin, keys and webhooks); a connection is one domain
moving pending → propagating → live; live means the edge terminates TLS
and proxies the domain to the application's origin.
| You are… | Use | Start |
|---|---|---|
| Pointing your own domain at your app | the console | Console — no code |
| A product that provisions domains for customers server-to-server | the API (/v1, sk_ key) | API |
| A product whose customers connect their domains inside your UI | the widget (customdomain-js) | Widget |
| An AI agent or MCP client | the MCP server (mcp.customdomain.ai) | MCP |
| Running the stack yourself | Docker Compose | Self-hosted quickstart |
Shared truths
These hold on every path. If a screen or a response seems to contradict one of them, that is a bug — tell us.
- Four statuses.
pending(records not yet seen),propagating(a rail wrote them, we are watching DNS),live,failed. pendinghas no deadline. Fast checks for 72 hours, then the connection stayspendingwitherror_code: setup_incompleteand is re-checked every six hours, forever. It goes live the moment the records appear.propagatingfails after 24 hours (error_code: propagation_timeout) — records were written but never resolved. Usually a wrong value or a Cloudflare proxy (orange cloud) in the way.failedis not terminal. Resume /:recheckrevives it; re-creating the same domain revives it (200); real traffic at the edge revives it.- The diagnosis is on the connection:
error_codeanderror_messageonGET /v1/connections/{id}, on every list, and on the webhook events. - HTTPS only after
live. The certificate issues on the first request once the connection is live; a domain pointed at us before that gets a clear "not active yet" page, never a raw error. - Root domains work everywhere, but the record differs by provider
(ALIAS/flattened CNAME vs
Arecords) — see Root domains. - "Automatic" means an adapter exists, not "the user types nothing". Read
rails/recommended_rail/end_user_automaticon the pre-flight — Rail truth. - Drift monitoring watches live connections hourly; alerts are not delivered on the hosted service yet.
Which credential do I have?
| You got it from | It is | Use it for |
|---|---|---|
| The console → Keys | an sk_… API key (tenant-scoped) | server-to-server API calls, the MCP server, minting widget tokens |
| The console → Embed | the application's client secret (shown once; rotate there) | minting widget tokens from your server (POST /v1/tokens) |
POST /v1/tokens | a short-lived widget JWT (one application, optionally one domain, optionally one end user) | the browser: customdomain-js open({ token }), or direct /v1 calls from the page |
| An agent grant | a delegated agent token | the agent's scoped API calls |
A widget JWT can never mint another token, never read another application's
data, and never choose the records we write. Anything that chooses records is
sk_-only.