# 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](/docs/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. `theme` picks 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).

```bash
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}'
```

```json
{
  "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.

```ts
// 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

```html
<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

```bash
npm install @apexly/react
```

```tsx
'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](/docs/webhooks) or `GET /v1/domains/{id}`.

## One-click Cloudflare

The embedded flow has the same **Add it for me** as the [hosted page](/docs/connect-page#one-click-cloudflare) 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_…` or `sk_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.dev` and 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.dev` and `frame-src https://connect.apexly.dev`.

- [Connect page](/docs/connect-page)
- [Domains API](/docs/api/domains)
- [Webhooks](/docs/webhooks)
