Embed the connect flow in your app
Put Apexly's connect flow inside your own settings page with a script tag or the React component, themed with your brand colour and logo, and hear when the domain goes live.
The connect page is a link you send. The widget is the same flow inside your app: your customer adds their DNS record without leaving your settings page, and your page is told the moment the domain is live.
- Same flow as the hosted page: detects their DNS provider, shows the exact record, checks from three resolvers, explains the one thing to change.
- Your brand: the logo and brand colour from your app's settings in the dashboard.
themepicks light, dark or auto. - Inline or modal: inline grows to fit its content; modal opens over the page and is full-screen on phones.
- Events:
onReady,onStatusChange,onVerified,onError,onClose.
1. Mint a connect session on your server
The browser never sees your API key. Your server asks for a connect session: a cs_… token that opens one domain's connect flow for a limited time (1 hour by default, ttl 60 to 86400 seconds).
curl -X POST https://api.apexly.dev/v1/domains/dom_01M5…/connect-sessions \
-H "Authorization: Bearer $APEXLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ttl": 3600}'{
"token": "cs_Q2xh…",
"expires_at": "2026-10-24T15:04:05.000Z",
"url": "https://connect.apexly.dev/embed/cs_Q2xh…"
}With the Node SDK, in a route your page calls. Check that the signed-in user owns the domain first: the token opens whichever domain you mint it for.
// app/api/connect-session/route.ts
import { Apexly } from '@apexly/node';
const apexly = new Apexly({ apiKey: process.env.APEXLY_API_KEY! });
export async function POST(req: Request) {
const { domainId } = await req.json();
// …look up the signed-in user and make sure domainId is theirs…
const { token } = await apexly.domains.createConnectSession(domainId, { ttl: 3600 });
return Response.json({ token });
}Mint a new session each time the page loads rather than storing one. Sessions are rate limited to 300 per minute per app.
2a. Script tag
<div id="connect-domain"></div>
<script src="https://connect.apexly.dev/widget.js"></script>
<script type="module">
const { token } = await (await fetch('/api/connect-session', { method: 'POST', body: JSON.stringify({ domainId }) })).json();
Apexly.mount('#connect-domain', {
token,
mode: 'inline', // or 'modal' (pass null as the element)
theme: 'light', // 'dark' | 'auto'
onStatusChange: ({ status, status_reason }) => console.log(status),
onVerified: ({ hostname }) => location.reload(),
onError: ({ code, message }) => console.warn(code, message),
});
</script>Apexly.mount returns { close(), destroy() }. /widget.js is always the current v1 loader (/widget/v1.js); it has no dependencies and adds only window.Apexly.
2b. React
npm install @apexly/react'use client';
import { ApexlyConnect } from '@apexly/react';
export function ConnectDomain({ token }: { token: string }) {
return <ApexlyConnect token={token} onVerified={() => location.reload()} />;
}<ApexlyConnect> takes the same options as props and loads the same widget.js once per page. With mode="modal", render it when the customer clicks your "Connect domain" button and unmount it in onClose.
Events
| Event | When | Payload |
|---|---|---|
onReady | The flow has loaded | { hostname, status } |
onStatusChange | Each status the flow shows: pending_dns → verifying → issuing → active, or dns_broken, cert_failed | { status, status_reason } |
onVerified | The domain is live with its certificate (once) | { hostname } |
onError | invalid_token (not a cs_ token: nothing loads), not_found (expired or the domain was deleted), load_failed | { code, message } |
onClose | Modal only: the close button, Escape, or a click on the backdrop | none |
Treat onVerified as a hint to refresh your UI. Your backend should still rely on the domain.active webhook or GET /v1/domains/{id}.
One-click Cloudflare
The embedded flow has the same Add it for me as the hosted page when the domain's DNS is at Cloudflare: your customer pastes a Cloudflare API token limited to their domain, and we add the record and check it. Nothing to configure, and nothing changes on your side: the token goes from the frame straight to Apexly, is used once and never stored, and onStatusChange / onVerified fire as usual. Records already on the name are listed and only replaced when your customer confirms.
Security
- No API key in the browser. The widget refuses anything that looks like
sk_live_…orsk_test_…and loads nothing. The session token can read one domain's records and status and ask for a re-check; it cannot list, change or delete domains, call the API, or collect email addresses. - Short-lived. A session stops working at
expires_at, or as soon as the domain is deleted. Apexly stores only a hash of it. - Messages stay between your page and the frame. The flow runs in a sandboxed iframe on
connect.apexly.devand posts only to your page's exact origin; the loader acts only on messages from its own iframe and Apexly's origin. Nothing your page sends is read. - Framing. The embed page is the one Apexly page any site may frame, because it has nothing worth clickjacking: copy buttons, "I've added it" and "Check again" (which only queue a DNS check), and links that open a new tab. The hosted connect page and the dashboard refuse to be framed.
- Your Content Security Policy: allow
script-src https://connect.apexly.devandframe-src https://connect.apexly.dev.