Create a connection
POST /v1/connections — the request, the idempotent replay, and the record set it returns.
A connection is created against an application and authorized by a
widget JWT (or an sk_ API key for
server-side use). The only required field is the domain:
curl -X POST http://localhost:8080/v1/connections \
-H "Authorization: Bearer <WIDGET_JWT>" \
-H "Content-Type: application/json" \
-d '{"domain":"app.customer.com"}'Response
The response is the connection plus its authoritative desired record set:
{
"id": "con_…",
"application_id": "app_…",
"domain": "app.customer.com",
"provider_id": "cloudflare",
"setup_type": "automatic",
"status": "pending",
"created_at": "2026-07-07T12:00:00Z",
"records": [
{ "type": "CNAME", "host": "app.customer.com", "value": "edge.customdomain.ai", "ttl": 3600 }
]
}iddoubles as thejobIdused by the widget and webhooks.setup_type(automatic/manual/async/mcp) is chosen from provider detection — it steers which rail the widget offers.recordsis the same set you can re-fetch any time fromGET /v1/connections/{id}/records. Before a rail writes records, the default edgeCNAMEis synthesized server-side.
Supplying your own records
Some record sets cannot be derived from the domain. The motivating case is email
verification: Amazon SES mints three Easy-DKIM CNAME selectors per domain,
so no template can know them. Replace the connection's desired set with your own
before the widget opens:
curl -X PUT http://localhost:8080/v1/connections/con_…/records \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"records":[
{"type":"MX","host":"@","value":"inbound-smtp.us-east-2.amazonaws.com","priority":10},
{"type":"TXT","host":"@","value":"v=spf1 include:amazonses.com ~all"},
{"type":"CNAME","host":"sel1._domainkey","value":"sel1.dkim.amazonses.com"}
]}'The supplied set then behaves exactly like ours everywhere: the OAuth rail writes
it with the customer's provider credential, the widget renders it for manual
setup, and the propagation poller verifies it before the connection goes live.
Omit dnsRecords in the SDK when you use this — the connection's set is
authoritative, so the browser never has to be trusted with the record list.
Three constraints follow from that trust boundary:
sk_API key only. A widget JWT lives in a browser; it may not choose the records written into a customer's zone.- Hosts must resolve inside the connection's zone.
@,_dmarc, andsel1._domainkeyare anchored to the connection's domain; a foreign FQDN is rejected. - The Domain Connect rail refuses a supplied set (
400). That rail applies our published templates at the provider, so it cannot write records it did not author — use the OAuth rail or manual setup for these connections.
The replacement is atomic and rejected with 409 once records have been applied.
GET /v1/connections/{id}/records returns the resulting authoritative set, and
the connection carries records_source: "integrator".
Idempotency
POST /v1/connections is idempotent per application + domain. If the calling app
already has a pending, propagating or live connection for the domain, that
connection is returned with 200 (instead of 201) — safe to call on every
widget open. If the only existing connection is failed, it is revived
(status back to pending, diagnosis cleared, a fresh verification window) and
returned with 200 rather than a duplicate being created. The explicit retry is
POST /v1/connections/{id}:recheck (409 if a different active connection
exists for the domain). This is also what makes an abandoned setup resumable:
the console's Finish setup / Resume reopens the guided flow against the
existing connection at the right step. A domain-bound widget JWT may only create
a connection for the hostname it was minted for. Hosting-platform subdomains
(*.vercel.app and the rest of the platform list) are refused with 422 platform_subdomain.