Get connect instructions
GET /v1/domains/{id}/instructions — one-click Domain Connect where available, provider-specific manual steps everywhere else.
GET /v1/domains/{id}/instructions
Returns setup steps written for the DNS provider actually serving the customer's domain, instead of a generic "add a CNAME".
Most custom-domain connections die at the last mile: the end user opens a registrar panel they have never seen and has to guess which of three name fields to use, whether their provider can even hold a CNAME-like record at a root domain, and which provider-specific toggle silently breaks everything. This endpoint answers all three.
Domainee fingerprints the provider from the zone's public NS records, so
there is nothing to configure and no credentials involved.
Don't want to build this UI? The connect widget renders these instructions, the one-click button and the verification polling for you, from one
<script>tag. Overview: Domain Connect API.
Request
curl https://api.domainee.dev/v1/domains/$DOMAIN_ID/instructions \
-H "Authorization: Bearer $DOMAINEE_API_KEY"
Query parameters
| Param | Notes |
|---|---|
redirect_uri | Where the customer lands after approving a Domain Connect flow at their provider. Use it to bring them back into your app. See Returning to your app. |
state | Opaque value round-tripped through that redirect. Use it for CSRF protection and to identify the domain on return. |
Response — 200 OK
{
"instructions": {
"hostname": "acme.com",
"provider": { "id": "cloudflare", "name": "Cloudflare", "detected": true },
"apex": true,
"method": "a",
"methodReason": "At a root domain the A records route each visitor to the nearest region, which ALIAS or CNAME flattening cannot do.",
"records": [
{ "type": "A", "name": "@", "value": "75.2.52.197", "ttl": 300 },
{ "type": "A", "name": "@", "value": "3.33.255.144", "ttl": 300 }
],
"steps": [
"Sign in to Cloudflare at https://dash.cloudflare.com.",
"Open the DNS record editor: Select your domain > DNS > Records > Add record.",
"Add 2 A records with the name @, one for each address below. Add all 2: one alone works but leaves you with no redundancy.",
"Delete any existing A, AAAA or CNAME record with the name @, or it will answer alongside the new ones.",
"Save. We re-check every 5 minutes and issue the TLS certificate on the first request, so there is nothing else to do."
],
"quirks": [
"Set Proxy status to DNS only (the grey cloud). Leaving it proxied (orange) makes Cloudflare terminate TLS itself, and the certificate will never be issued."
],
"dashboardUrl": "https://dash.cloudflare.com"
}
}
Fields
| Field | Notes |
|---|---|
provider.id | Stable slug, e.g. cloudflare, route53, godaddy. unknown when nothing matched. |
provider.detected | false means the steps are generic. The record values are still correct. |
apex | true when the hostname is its own zone root. |
method | a or cname. Which one is right depends on both the position and the provider. |
methodReason | Why this method, in words you can show the end user. |
records | Exactly what to publish. name is already written the way this provider's form wants it. |
steps | Ordered, plain-language instructions. Safe to render as-is. |
quirks | Provider-specific gotchas. Show these; they are the difference between a connection that works and one that silently doesn't. |
The name field is the point
Providers disagree about how to write a record name, and this is the single
most common way a correct record ends up in the wrong place. A user who types
the full hostname into a form that already appends the zone gets
blog.acme.com.acme.com and no error message.
| Provider | Apex | Subdomain blog.acme.com |
|---|---|---|
| Cloudflare, GoDaddy, Namecheap | @ | blog |
| Route 53 | leave the record name blank | blog |
| Google Cloud DNS, Akamai | acme.com | blog.acme.com |
| Porkbun, Name.com, Dynadot | leave the host field blank | blog |
records[].name is already correct for the detected provider, so you can put
it straight into your UI without a lookup table of your own.
Why a separate endpoint
This does a live NS lookup, so it is not a field on the
Domain object. Listing 500 domains must not fan
out 500 DNS queries. Call it when you are about to show someone setup steps.
Errors
| Code | Status | When |
|---|---|---|
not_found | 404 | Domain doesn't exist in this workspace. |
Detection itself never fails. If the NS lookup returns nothing or the
nameservers match no known provider, provider.detected is false and you get
correct generic steps.
One-click setup with Domain Connect
Every response also carries a domainConnect object. When the customer's DNS
provider supports Domain Connect and has
adopted Domainee's template, they can approve a single screen at their own
provider instead of editing records by hand.
Providers only apply templates they have synced from the shared Domain-Connect/Templates repository and onboarded one by one, so availability grows provider by provider.
| Provider | One-click |
|---|---|
| Cloudflare | Live for all accounts. Records are created DNS only. |
| Glauca Digital | Live. |
| IONOS, Vercel, WordPress.com | Support the protocol, not yet onboarded. reason: "template_not_adopted". |
| GoDaddy | Not available. GoDaddy is not onboarding new templates directly. |
| Namecheap, Route 53, Porkbun, Squarespace, Wix and most others | Don't support the protocol. reason: "no_discovery_record". |
You don't need to track this list. supported turns true on its own as
providers adopt the template.
{
"domainConnect": {
"supported": true,
"providerId": "cloudflare.com",
"providerName": "Cloudflare",
"providerDisplayName": "Cloudflare",
"applyUrl": "https://dash.cloudflare.com/domainconnect/v2/domainTemplates/providers/domainee.dev/services/edge-apex/apply?domain=acme.com&ip1=...&redirect_uri=...",
"width": 750,
"height": 750
}
}
Send the customer to applyUrl, in a popup sized width × height or as a
full redirect. Their provider writes the records and returns them to your
redirect_uri with your state. No credentials reach Domainee and there is
nothing for the customer to revoke afterwards.
Returning to your app
At Cloudflare, redirect_uri can be any URL on your own domain. After the
customer approves, Cloudflare sends them there with your query string intact
and state appended, for example
https://app.example.com/domains?id=123&state=abc. Check state against the
value you passed, then call
POST /v1/domains/{id}/check so the domain
verifies right away instead of on the next 5-minute check.
Domainee's templates list domainee.dev as their redirect domain. Cloudflare
does not enforce that, but other providers may once they enable the templates,
and would then reject an outside redirect_uri. If you open applyUrl in a
popup, you can leave redirect_uri out and run the check when your page regains
focus, which works at every provider. Don't wait for the popup to close:
Cloudflare's approval page sets Cross-Origin-Opener-Policy, so popup.closed
reads true straight away. The connect widget already handles
this.
When it is unavailable you get a reason instead:
{ "domainConnect": { "supported": false, "reason": "no_discovery_record" } }
reason | Meaning |
|---|---|
no_discovery_record | The zone has no _domainconnect TXT record, so the provider doesn't support the protocol. |
settings_unavailable | Discovery record present but the provider's settings endpoint didn't answer. |
no_sync_support | The provider supports Domain Connect but not the synchronous flow. |
template_not_adopted | The provider supports the protocol but hasn't adopted Domainee's template yet. |
lookup_failed | The DNS lookup itself failed. Retry. |
Always render steps regardless. One-click depends on where each
customer's DNS is hosted, so for most integrations supported is false more
often than it is true, and the manual steps are the path many of your
customers will actually take. Treat applyUrl as a shortcut you offer when it exists,
never as the only route.
MCP
The same thing is available to AI agents as the get_connect_instructions
tool, so an agent that just created a domain can walk the customer through
pointing it. See the MCP guide.