# Reference

> Reference for the eight window.customdomain methods, window events and callbacks, the open() config, widget statuses, white-label options and theming tokens.

Source: https://docs.customdomain.ai/docs/widget-sdk/reference

## Methods

`window.customdomain` exposes eight methods. The hosted script
(`https://app.customdomain.ai/widget-assets/customdomain-sdk.js`) and the npm
packages (`customdomain-js` and `@customdomain/react`, both 0.5.5) have all
eight; `removeDomain()` reached npm in 0.5.2.

| Method                                                  | Purpose                                                                                                                                                                                                                                                                                                   |
| ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `open(config)`                                          | Open the connect modal (or embedded panel). Returns `{ close }`.                                                                                                                                                                                                                                          |
| `purchaseDomain(config)`                                | Open the in-widget domain **purchase** flow (Sell). Returns `{ close }`.                                                                                                                                                                                                                                  |
| `removeDomain(config)`                                  | Open the **removal** flow for a connected domain: the DNS records that were added, the choice to remove or keep them, one more sign-in when needed, and a summary. Pass `connectionId`, or the domain as `prefilledDomain`. Returns `{ close }`. See [Removing a domain](/docs/connect-flow/offboarding). |
| `loadSharedFlow(url, config?)`                          | Open a shared/delegated setup link in the modal. Returns `{ close }`.                                                                                                                                                                                                                                     |
| `checkDomain(domain, { token, apiBase?, dnsRecords? })` | Pre-flight feature detection. Resolves a `CheckDomainResult`. Pass `dnsRecords` (the set you intend to write) to have conflicts and `integrationWarnings` computed for those records.                                                                                                                     |
| `checkRecords(domain, { token, dnsRecords, apiBase? })` | Verify a desired record set against live public DNS. Resolves a `CheckRecordsResult`.                                                                                                                                                                                                                     |
| `load()`                                                | Resolve when the SDK is ready (CDN/npm loader parity).                                                                                                                                                                                                                                                    |
| `close()`                                               | Close and tear down the current modal.                                                                                                                                                                                                                                                                    |

`checkDomain` calls `POST /v1/domains:check` and returns camelCase fields:
`domain`, `provider`, `setupType`, `supportsAutomatic`, `oauthAvailable`, `domainConnect`,
`registered`, `NSSupport`, `wildcardSupport`, `cnameFlattening`,
`spfOverrideSupport`, `caaSupport`, `supportsSocialLogin`, `recordConflicts`
(a `caa-blocks-letsencrypt` entry is a certificate advisory, not a record to replace;
the REST answer also lists it in `advisories`, which the packages map since 0.5.5; advisories never count toward `willFallbackToManual`),
`conflictTolerance`, `willFallbackToManual`, `apexSupported`, `apexMessage`, the
Public Suffix List split of the domain (`subdomain`, `registrableDomain`,
`publicSuffix`), `authoritativeDnsProvider`, plus the rail truth: `rails` (`{ oauth, domainConnect, apiKey, manual, cloudBrowser? }`, each
`{ available, reason?, caveat? }`; `cloudBrowser` also carries `key` when the secure sign-in makes an API key), `recommendedRail`, `endUserAutomatic`,
`blockedReason`, `dashboardUrl`, `nameservers`. Branch on `rails` and
`recommendedRail`, not on `supportsAutomatic`, which only means an adapter
exists. See [Rail truth](/docs/concepts/ownership-and-setup-types#rail-truth-what-to-actually-tell-the-user).
Since 0.5.4 the packages also map `detectionError` (`nxdomain` or `resolver`;
`resolver` is transient, so check again), `targetMode` (`edge` or `custom`),
`integrationWarnings` (only when you passed `dnsRecords`: records the provider
cannot write as given, each `{ code, message }`), `managedDns` (whether the
deployment's managed DNS can take the domain, with a `reason` when it cannot;
whether your users are offered it is your application's
[Domain maintenance](/docs/guides/nameserver-migration) setting) and
`sandbox` (the token belongs to a test mode application). For a root domain,
`apexStrategy` (`alias`, `a_records` or `unsupported`), `apexRecordType` and
`apexTargets` say how the root would be pointed; see
[Root domains](/docs/dns/root-domains).
`checkRecords` rides `POST /v1/monitor:check` and returns
`{ domain, inSync, records[] }`, where each record is
`{ host, type, ok, observed[] }`.

## Events & callbacks

Every meaningful transition fires **both** a `window` `CustomEvent` (read
`event.detail`) and, where you provided one, the matching `open()` callback.

| Window event                 | Callback               | Fires                                                                                                                                                                                                                                        | `detail` / arg                                                                                                                                                                                                                                                                                                                                                                     |
| ---------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `customdomain:step`          | `onStepChange(detail)` | on every screen change                                                                                                                                                                                                                       | `{ step, domain?, provider?, user?, error? }`. `user` is your `userId`. `error` is the last error shown (`{ code, title, details }`), present only after an error screen. With several domains queued it also carries `pendingDomains` and `processedDomains`                                                                                                                      |
| `customdomain:success`       | `onSuccess(result)`    | when the domain reaches `live`, or a reconnect renewed its key                                                                                                                                                                               | `{ jobId, domain, setupType, provider, success: true }`. `setupType` is `automatic` or `manual`, or `reconnect` with `reconnected: true` after a [reconnect](/docs/guides/secure-sign-in#reconnect). `alreadyConnected: true` is added when the domain was already live and this was a resume. With several domains queued it also carries `processedDomains` and `pendingDomains` |
| `customdomain:close`         | `onClose(detail)`      | when the modal closes                                                                                                                                                                                                                        | `{ lastStatus, error?, shared?, manualScreenDisabled? }`. `shared` is set when the user forwarded the setup to a colleague before closing                                                                                                                                                                                                                                          |
| `customdomain:error`         | `onError(err)`         | when the widget shows an error                                                                                                                                                                                                               | `{ code, message, title?, details? }`                                                                                                                                                                                                                                                                                                                                              |
| `customdomain:fallback`      | `onFallback(detail)`   | once per domain, when the user goes to the records: routed there because no way connects automatically, or by choosing **Add the records** over an open card. A sign-in the user cancels, or that fails, keeps the card and does not fire it | `{ reason, provider?, screen, message? }`. `reason` is one of `no_end_user_rail` (no sign-in or one-click at this provider), `conflicts_exceed_tolerance`, `provider_unsupported`, `unknown_provider`, or `chose_records` (the user picked **Add the records** from the card); `message` is the human explanation shown, when there is one                                         |
| `customdomain:purchase`      | none                   | the buy flow created a checkout session                                                                                                                                                                                                      | `{ domain, sessionId?, clientSecret?, url? }`: mount Stripe Embedded Checkout with `clientSecret`                                                                                                                                                                                                                                                                                  |
| `customdomain:removed`       | `onRemoved(result)`    | a removal finished (npm packages 0.5.2 or newer)                                                                                                                                                                                             | `{ domain, connectionId, records, removed, restored, leftInPlace, user? }`. `records` is `removed`, `partly_removed`, `kept`, `manual` (the user deletes them; the widget listed how), `in_use` or `not_needed`                                                                                                                                                                    |
| `customdomain:request-close` | none                   | host-delegated dismissal (`whiteLabel.delegateClose`)                                                                                                                                                                                        | provider payload; you call `close()`                                                                                                                                                                                                                                                                                                                                               |
| `customdomain:doc-click`     | none                   | user clicked a documentation link                                                                                                                                                                                                            | link payload                                                                                                                                                                                                                                                                                                                                                                       |
| `customdomain:shared`        | none                   | a setup link was handed over: the instructions were emailed, the user chose *Copy link instead*, or your `forwardLink` page took it                                                                                                          | `{ domain, link?, jobId? }`                                                                                                                                                                                                                                                                                                                                                        |

`onError(err)` receives a `CustomDomainError` (`{ code, message, details?, status? }`).
`status` (0.5.4 or newer) is the HTTP status of the control plane response the
error came from; it is unset when the widget raised the error itself. A `status`
of 429 or any 5xx is safe to retry after a moment; any other 4xx gives the same
answer until the token, domain or records change. `checkDomain` and
`checkRecords` throw the same class.
The hosted script also fires a `customdomain:ready` window event once
`window.customdomain` is attached.
Error codes are an open union, so branch on the ones you handle and treat the
rest as a generic failure. The ones the widget raises include `SessionError`
(the token expired or was refused, including a domain-bound token used for
another host), `InvalidDomainError`, `DomainAlreadyConnectedError`,
`UnregisteredDomainError`, `PlatformSubdomainError`, `InvalidNameservers`,
`ProviderAuthenticationError`, `AccessDeniedError`, `PopupBlockedError`,
`DomainConnectError`, `ProviderError`, `TimeoutError`, `RateLimitError`,
`NetworkError`, `PlanLimitError` (the account's plan or allowance does not
allow another connection; the end user only sees a neutral message) and
`GenericError`.

> A **failed** connection is not a dead end in the widget: it shows the manual
> screen with the records and the control plane's reason, and *Check now*
> calls `POST /v1/connections/{id}:recheck` (which revives the connection)
> before polling again.

> `customdomain:success` is an event, not a dismissal. The widget stays on the
> success screen and closes itself when the user is done (or when you call
> `close()`).

## `open()` / `OpenConfig`

| Field                                                                             | Type                                         | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| --------------------------------------------------------------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `applicationId&#x60; &#x2A;(required)*                                            | `string`                                     | Public application id.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `token&#x60; &#x2A;(required)*                                                    | `string`                                     | Widget JWT (scope `connect`), minted on your server: `POST /api/widget-token` with an API key for applications created in the console, or `POST /v1/tokens` with the client secret for applications created over the API. See [Installation & embed](/docs/widget-sdk/installation-and-embed#1-mint-a-token-on-your-server).                                                                                                                                                                                          |
| `getToken`                                                                        | `() => string \| Promise<string>`            | Returns a fresh widget token; have it call your own server route, never the mint endpoint directly. Called when the current one expires mid-setup, so long manual setups never stop. See [Keeping a session alive](/docs/authentication/widget-tokens#keeping-a-session-alive).                                                                                                                                                                                                                                       |
| `domain`                                                                          | `string`                                     | Pre-filled domain.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `prefilledDomain`                                                                 | `string \| string[]`                         | Alias accepted for the pre-fill.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `wwwRedirect`                                                                     | `boolean`                                    | Root domains only. `true` connects the root and `www` and redirects the root to `www`; `false` connects the root alone; unset lets the user choose (root and www is the recommended default).                                                                                                                                                                                                                                                                                                                         |
| `dnsRecords`                                                                      | `DNSRecord[] \| Record<string, DNSRecord[]>` | DKIM records for the widget's DKIM screen (a `purpose: "dkim"` record, or a host under `_domainkey`). Array, or per-domain object mode `{ "acme.com": [...] }`. Every other record is ignored, and the SDK says so once in the console: customers are shown the records of your application's [routing](/docs/connect-flow/routing), or the set your backend supplies with `PUT /v1/connections/{id}/records`.                                                                                                        |
| `applicationName` / `applicationUrl`                                              | `string`                                     | Branding shown in the flow.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `locale`                                                                          | `string`                                     | UI locale.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `whiteLabel`                                                                      | `WhiteLabel`                                 | Theming + behavior (below).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `forceManualSetup`                                                                | `boolean`                                    | Skip automatic rails; always show records.                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `forceSubdomain`                                                                  | `boolean`                                    | Only accept a subdomain; the root option is hidden.                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `supportForSubdomains` / `defaultSubdomain` / `hostRequired`                      |                                              | Subdomain-entry behavior.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `manualSetupDocumentation`                                                        | `string`                                     | Deep-link to your own manual-setup guide.                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `enableDkim`                                                                      | `boolean`                                    | Offer the DKIM screen.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `enableEmail`                                                                     | `boolean`                                    | Show the email setup screen (the email record template) before the domain records.                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `managed`                                                                         | `boolean`                                    | Opt the one-click (Domain Connect) flow into the durable managed rail. The control plane falls back to the standard redirect when the provider or deployment cannot do it. Default `false`. See [Managed connections](/docs/connect-flow/managed-connections).                                                                                                                                                                                                                                                        |
| `endUserRef`                                                                      | `string`                                     | Your id for this customer. Stored on the connection as `end_user_ref`, shown in the console, and sent with every webhook as `user_id`.                                                                                                                                                                                                                                                                                                                                                                                |
| `userId`                                                                          | `string`                                     | An id echoed back on the widget's step events.                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `container`                                                                       | `string`                                     | CSS selector for embedded mode (implies `whiteLabel.embedded`).                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `apiBase` / `widgetBase`                                                          | `string`                                     | Override the default control-plane (`https://api.customdomain.ai`) / widget (`https://app.customdomain.ai`) hosts (for example a staging environment).                                                                                                                                                                                                                                                                                                                                                                |
| `iframeSandbox`                                                                   | `string`                                     | Advanced. Replaces the iframe's `sandbox` attribute. The default allows scripts, forms and popups that escape the sandbox so provider sign-in windows can open.                                                                                                                                                                                                                                                                                                                                                       |
| `searchType`                                                                      | `string`                                     | Search mode for the buy-a-domain screen, such as `standard` or `ai`.                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `mode`                                                                            | `"connect" \| "remove" \| "reconnect"`       | `"remove"` opens the removal flow (what `removeDomain` sets; npm packages 0.5.2 or newer). `"reconnect"` opens straight on the sign-in that renews the domain's [lasting API key](/docs/guides/secure-sign-in#reconnect): one sign-in, a new key, no DNS change. A domain with no such key, or a provider whose key is turned off, gets a short notice instead. The hosted script and the npm packages pass `mode` through; the packages' types name `"reconnect"` since 0.5.5 (before, a TypeScript caller cast it). |
| `connectionId`                                                                    | `string`                                     | The connection to remove or reconnect, when you kept its id. Otherwise the prefilled domain is looked up. Either way, mint the token for that domain and end user (`POST /v1/tokens` with `domain` and `end_user_ref`).                                                                                                                                                                                                                                                                                               |
| `onSuccess` / `onClose` / `onStepChange` / `onError` / `onFallback` / `onRemoved` | fn                                           | Callbacks (mirror the events above).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |

A `DNSRecord` is `{ type, host, value, ttl?, priority?, purpose? }`. Supported
types include `A`, `AAAA`, `CNAME`, `CAA`, `MX`, `NS`, `TXT`, plus the meta-types
`SPFM` (SPF-merge) and `REDIR301`.

## What the end user can type

The domain field accepts what people actually paste: a full URL
(`https://www.acme.com/pricing`), an email address (`me@acme.com`), a host with
a port, uppercase, internationalized names and full-width dots. The widget
reduces each to the hostname, offers a correction for a mistyped ending
("Did you mean acme.com?"), and otherwise says exactly what is wrong.

When the result is a root domain (`acme.com`), the widget asks before creating
anything: root and `www` (recommended), the root alone, or a subdomain instead.
Where the user's DNS provider cannot point a root at a hostname, the widget
uses the provider's IP address records if your deployment publishes them, and
otherwise steers the user to `www` or a subdomain. See
[Root domains](/docs/dns/root-domains).

## Manual setup and live DNS checks

On the manual screen the widget shows each record with the Name to type into
the provider's panel (`@`, `www`, `shop`) and the provider's own label for the
record type, then reads public DNS for every record through
`GET /v1/connections/{id}/diagnose`. Each record shows one of: found, not found
yet, saved at the provider and waiting for caches, or still pointing somewhere
else (named when it is a known service, such as "Still points to Squarespace").
It also flags the mistakes that block a connection even when a record looks
right: the full hostname typed into the Name field, Cloudflare's proxy turned
on, a leftover IPv6 record, extra A records, a conflicting record, or a CAA
record that blocks certificates. The connection goes live the moment everything
is in place.

Each record card says exactly what to type at *that* provider: the Name for a
root domain is `@`, an empty field, or the full domain depending on the panel;
a hostname value keeps its final dot where the panel would otherwise append the
zone; on Cloudflare the card adds *Proxy: DNS only*. The provider's step-by-step
guide and known pitfalls sit behind *Show me how*. Warnings appear only for
problems the live check actually found.

### Other ways to finish

Below the records, the screen offers three ways out for people who can't, or
would rather not, edit DNS themselves.

**Let an AI assistant do it.** *Copy prompt* copies one self-contained prompt,
and a row of icons opens it in ChatGPT (covers Codex), Claude, Gemini, Grok,
Perplexity or Microsoft Copilot. ChatGPT, Claude, Perplexity and Grok open with
the prompt already filled in; Gemini and Copilot have no prefill, so the widget
copies the prompt, opens the app and says &#x2A;Prompt copied. Paste it in.* The
prompt (English, so any assistant can act on it) names the domain, the DNS
provider and its DNS panel link, and each record with its type, the Name to
type at that provider, the value and the TTL. It turns the live check into
exact instructions (delete the old A or AAAA record, turn Cloudflare's proxy
off, remove a doubled name), adds the provider's pitfalls, works for assistants
that drive a browser (the user signs in and handles two factor codes) and for
ones that run code (the provider's DNS API with a token the user supplies),
forbids touching MX or email TXT records, and ends with `dig` commands and the
answers to expect. It also carries a status page link, minted with
`POST /v1/sharing/connect` the first time the user reaches for the section;
that link works once. The prompt shares the domain and its records, nothing
about your application beyond `applicationName`.

**Email these instructions to someone else.** An inline form takes the
recipient's address and an optional note (up to 280 characters) and calls
`POST /v1/sharing/connect` with `recipient_email` and `note`; the control plane
emails the records and a link to finish. On success the widget shows &#x2A;Sent to
[name@company.com](mailto:name@company.com)* and reports `FINISHED_SUCCESSFULLY_LINK_SHARED` with
`customdomain:shared`. A rejected address (`400`), too many sends (`429`), or an
email that could not be sent (`email_error`) each get a plain explanation, and
the last still hands over the link. *Copy link instead* mints a link and copies
it. If you configured `customProperties.forwardLink.url`, the row opens your
page with the link instead, as before.

The person who opens the link acts as the end user who shared it: a widget token
shares as its own `end_user_ref`, and your API key (or an `app` scope token) shares
as the `end_user_ref` of your connection for the one domain the link names, so the
link keeps working on a connection that names a customer. The link is a credential
for finishing that one setup, not the customer: it cannot remove the domain, cannot
set the customer's contact address, and is never offered the customer's kept keys
(saved access); a key made at a sign-in it starts is not the customer's own and
never replaces theirs. One rule governs a link: its token owns exactly what the token
that made it owned, and may touch only the shared domains. A widget token can share
only a domain whose connection it owns. A link made before this release stops working:
ask for a new one. See [Widget tokens](/docs/authentication/widget-tokens).

**Let your platform keep the domain working.** The row reads "Let Acme Sites
keep your domain working", with your `applicationName`. Offered only when your application
turns on [Domain maintenance](/docs/guides/nameserver-migration) (its
`nameserver_migration` setting), the one switch for every DNS hosting offer the
widget makes. The user agrees once, and the widget moves their nameservers to
CustomDomain™ or to your own DNS host. With the setting off (the default), the
widget offers no DNS hosting, even where `managed_dns.available` is `true`, and
when the offer comes back `available: false` for a domain it shows no hosting
row at all.

**Finishing a zone CustomDomain™ already hosts.** A connection whose end user
started [managed DNS](/docs/dns/managed-dns) in an earlier session (before the
setting decided every offer) gets a row titled *Let CustomDomain™ host your DNS*
to pick up where they left off. The widget no longer offers it to anyone new.
The row leads to the copied records (where a zone file exported from the
current provider can be pasted), then the nameservers to set with the
registrar's name and link and short steps, including turning DNSSEC off first.
The widget watches `GET /v1/connections/{id}/managed-dns` until the delegation
is `active`, then continues to the normal success path (`IN_PROGRESS`, then
`FINISHED_SUCCESSFULLY` with `setupType: "automatic"`). These screens report
`MANUAL_CONFIGURATION`.

To turn one off, set `customProperties.aiHandoff.hide` or
`customProperties.forwardLink.hide` (the email row). DNS hosting is turned on
or off by the application's setting, not here.
`customProperties.managedDns.hide` is **deprecated**: it is still accepted and
still hides the row for finishing a zone already started, which is all it can
hide now.

## White-label configuration

`whiteLabel` accepts quick keys, a full raw-token override, per-screen disables,
and two layout behaviors:

| Key                              | Effect                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `colors`                         | `primary`, `primaryHover`, `background`, `text`, `success`, `error`, … mapped onto tokens. A `primary` brings readable text on it, a hover shade, a link color that reads on the page and a focus ring; in dark mode a dark brand color is lifted until it stands out from the surface. A `background` or `text` brings the surfaces, borders, inputs and quiet text with it, so a custom page color never leaves white inputs on it. A value that is not a hex color is used as given, with none of this.                                                                                                                     |
| `font` / `borderRadius` / `logo` | Font family, corner radius, header logo (an `https:` URL or a base64 image). Every corner of the widget follows `borderRadius` in proportion, so `0px` is square throughout. With a `logo` set, the header shows it centered on its own (the stock globe and DNS marks under the header are dropped, though an icon you supply for that slot still shows), at the `logo-height` token (32px by default); a logo that fails to load gives way and the header closes up. A logo uploaded in the console's widget designer is hosted for you at a permanent `https:` URL, and the install code the console shows already uses it. |
| `logoDark` / `logoPlate`         | In dark mode `logoDark` replaces the logo. With no `logoDark`, `logoPlate: true` puts `logo` on a small light plate, for a logo drawn for light pages.                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `fontUrl` / `googleFont`         | `fontUrl` is an `https:` stylesheet URL (your own `@font-face`) loaded into the widget; `googleFont` is `{ family, weights?, display? }` and loads that Google Font. Both still use `font` or the family you name, and your page's content security policy must allow the font host.                                                                                                                                                                                                                                                                                                                                           |
| `customCopy` / `icons`           | `customCopy` overrides screen text, keyed by locale (or `"*"` for all) and then by copy key. `icons` replaces icon slots with an `https:` or `data:image` URL or inline SVG.                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `hideLogo`                       | Hide the powered-by footer.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `hideConfetti`                   | Disable the success confetti.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `tokens`                         | Override any of the \~90 raw design tokens (`modal-width`, `color-primary`, `radius-md`, `shadow-modal`, …).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `screens.disable`                | Array of optional screens to skip: `existingRecords`, `dkim`, `email`, `manualConfiguration`, `unsupported`, `provider-list` and `congratulations`. A skipped screen moves on as its main button would. With `congratulations` skipped, `onSuccess` still fires for each domain, then the widget opens the next queued domain or closes, and that close is not counted as abandoned.                                                                                                                                                                                                                                           |
| `delegateClose`                  | The widget never tears itself down; it posts `customdomain:request-close` and you call `close()`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `embedded`                       | Render as a flat, full-bleed panel inside your `container` instead of a fullscreen modal.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `darkMode` / `darkTokens`        | `"disabled"` (default), `"enabled"` or `"auto"` (follows the OS); `darkTokens` fine-tunes the dark palette.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `customProperties`               | Granular toggles: `hideCloseButton`, `hideProgressIndicator`, `providerLogin.changeProvider.hide`, `recordsPreview.hide`, `forwardLink` (`hide`, `url`, `label`), `gotoManualLink.hide`, `useCustomExitConfirmation`, `aiHandoff.hide`, `managedDns.hide` (deprecated: see [Other ways to finish](#other-ways-to-finish)).                                                                                                                                                                                                                                                                                                     |

Tokens are emitted as CSS custom properties (`--je-<token>`) on the widget's
shadow host, so they cannot affect your page. Every value must be a plain CSS
value (a color, length, font stack or shadow); anything that could inject CSS
or script is dropped, and custom icons are rebuilt from an allowlist of SVG
elements and attributes.

The default look follows one system: a 12, 13, 14, 15, 17 and 21 px type scale
(`font-size-xs` through `font-size-h2`, plus `font-size-lg`), a 4 px spacing
rhythm (`space-2xs` through `space-xl`), and one motion curve. Screens slide in
the direction of travel (`motion-duration`, `motion-ease`,
`motion-slide-distance`), the modal glides between heights, and controls react
in `motion-duration-fast`. Under `prefers-reduced-motion` all of it is off and
spinners only slow down.

## Widget statuses

`onStepChange` / `customdomain:step` emit the widget's public status, one of a
fixed 15-value set of stable names: `INITIAL`, `ENTER_DOMAIN`,
`DOMAIN_ANALYSIS`, `DOMAIN_SETUP`, `IN_PROGRESS`, `EXISTING_RECORDS`, `LOGIN`,
`LOGIN_2FA`, `MANUAL_CONFIGURATION`, `PROVIDER_MANUAL_SELECTION`,
`EXIT_WITH_ERROR`, `DKIM_SETUP`, `FINISHED_SUCCESSFULLY`,
`FINISHED_SUCCESSFULLY_MANUAL`, `FINISHED_SUCCESSFULLY_LINK_SHARED`.

The removal flow uses the same set: reading what removal would do is
`DOMAIN_ANALYSIS`, the choice is `DOMAIN_SETUP`, one more sign-in is `LOGIN`,
removing is `IN_PROGRESS`, and the summary is `FINISHED_SUCCESSFULLY`.

## Token lifecycle & accessibility

Widget tokens expire (15 minutes by default). With `getToken` the widget
refreshes itself and carries on; without it, the user is told they can close
the window while the connection finishes on its own. A domain-bound token may
only act on the hostname it was minted for.

Focus trap, `aria-modal`, a live-region announcer, generous touch targets,
safe-area insets, `prefers-reduced-motion`, and `prefers-color-scheme` dark mode
are built in. Enter submits every form; each new step moves focus to its
heading (or its one input) so screen readers announce it; Escape asks before
leaving; live checks announce only real changes. On phones the widget takes
the full screen. The iframe is sandboxed with `allow-popups`/
`allow-popups-to-escape-sandbox` so provider OAuth windows and the AI
assistants can open in a new tab.

## Analytics

Every widget session reports its funnel (opened, domain entered, provider
detected, offered one-click or sign-in, shown records to copy, live, or closed
early) with the provider, the rail (`oauth`, `domain_connect`, `api_key`,
`managed_dns`, `cloud_browser` for [secure sign-in](/docs/guides/secure-sign-in),
or `manual`), and whether the domain was a root or a subdomain. Read it in the
console under **Analytics**, or from your server with
`GET /v1/analytics/connect` (an API key). The records screen's alternatives
report as their own steps: `handoff-ai` (copied or opened the AI prompt),
`share-email` (sent the instructions, or copied the link instead), and
`managed-dns` (opened or started DNS hosting).

### What a widget session records

Each time the widget opens, CustomDomain™ keeps one record of that session,
attributed to your workspace and application, for security, abuse prevention,
support and to improve the widget. It never holds the domain name, anything the
end user typed, a cookie or an account.

| From        | What is kept                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| The request | The end user's IP address. The country, region and city Cloudflare derives from it, and the network's AS number and name where the deployment passes them on. The device type (phone, tablet or computer), operating system and browser (its major version), read from the user agent, which itself is not kept.                                                                                                                                       |
| The widget  | The screen size and the widget's viewport size, the pixel ratio, whether the screen is a touch screen, the browser's language and the widget's language, the time zone, light or dark mode and reduced motion, the kind of network connection where the browser reports it, the widget's and the SDK's version, whether the widget was embedded or a modal, and the origin of the host page and of its referrer (scheme, host and port, never a path). |
| How it went | The first and last step, the rail, the provider, whether it worked, the error code, and the connection and any secure sign-in it started.                                                                                                                                                                                                                                                                                                              |

The IP address is kept for as long as your account exists, unless the deployment
sets a window (`IP_RETENTION_DAYS`, the number of days after which the address
is cleared and the rest of the record stays). The record is deleted with the
application it belongs to. You see counts only: the devices, countries,
browsers, systems and embeds in **Analytics**, **Devices and places**, and in
`audience` on `GET /v1/analytics/connect`. No address, city or single session
is ever shown.
