Reference
The seven methods, the eight window events and callbacks, full config, statuses, and theming tokens.
Methods
window.customdomain exposes seven methods:
| Method | Purpose |
|---|---|
open(config) | Open the connect modal (or embedded panel). Returns { close }. |
purchaseDomain(config) | Open the in-widget domain purchase flow (Sell): search, pick a domain, and start a real checkout (POST /v1/registrar/checkout), firing customdomain:purchase with the Stripe session for you to complete and finalize (POST /v1/registrar/fulfill). Returns { close }. |
loadSharedFlow(url, config?) | Resume a share link in the modal, rehydrated from the sharer's server-side prefill (including their white-label branding, entitlement permitting — see White-label configuration). Returns { close }. |
checkDomain(domain, { token, apiBase? }) | Pre-flight feature detection. Resolves a CheckDomainResult. |
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, authoritativeDnsProvider, NSSupport,
wildcardSupport, cnameFlattening, spfOverrideSupport, caaSupport,
supportsSocialLogin, recordConflicts. The raw API response additionally
carries an authoritative subdomain / registrable_domain / public_suffix
parse of the domain (the server's Public-Suffix-List split); the widget's own
screens use this server verdict to decide the subdomain/apex boundary, but the
SDK's checkDomain() wrapper does not yet surface those three fields to callers.
checkRecords rides POST /v1/monitor:check and returns { inSync, records[] }.
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(step) | on every screen change | { step, domain?, provider?, pendingDomains?, processedDomains? } — the last two appear only during a multi-domain flow |
customdomain:success | onSuccess(result) | when the domain reaches live | { jobId, domain, setupType, provider?, alreadyConnected?, pendingDomains?, processedDomains? } — alreadyConnected is true when the domain was already live and this call resumed it (200) rather than connecting it fresh |
customdomain:close | onClose(detail) | when the modal closes | { lastStatus, error?, shared?, manualScreenDisabled? } |
customdomain:purchase | — | the in-widget purchase flow started a real checkout | { domain, sessionId?, clientSecret?, url? } — mount Stripe Embedded Checkout with clientSecret (or redirect to url), then call POST /v1/registrar/fulfill to finalize |
customdomain:error | onError(err) | any hard failure | CustomDomainError ({ code, message, details? }), mirroring onError |
customdomain:request-close | — | host-delegated dismissal (whiteLabel.delegateClose) | provider payload; you call close() |
customdomain:doc-click | — | user clicked a documentation link | link payload |
customdomain:shared | — | a shared/delegated setup link was produced | share payload |
onError(err) / customdomain:error receive a CustomDomainError
({ code, message, details? }) — error codes are an open, stable union:
SessionError, AccessDeniedError, PopupBlockedError, InitTimeoutError,
InvalidDomainError, DomainConnectError, ProviderError,
InvalidNameservers, ProviderAuthenticationError, PlanLimitError,
TimeoutError, RateLimitError, NetworkError, GenericError.
InvalidNameservers and ProviderAuthenticationError mirror the control
plane's connect-time error codes (a domain with no usable nameserver
delegation, and a DNS provider that rejected the authorization, respectively);
PlanLimitError mirrors the quota_exceeded / 402 response when the plan's
quota is used up (enforcement of that response is gated on the deployment —
see Plans & quotas).
customdomain:successis an event, not a dismissal — the widget stays on the success screen and closes itself when the user is done (or when you callclose()).
open() / OpenConfig
| Field | Type | Notes |
|---|---|---|
applicationId (required) | string | Public application id. |
token (required) | string | Widget JWT from POST /v1/tokens. |
domain | string | Pre-filled domain. |
prefilledDomain | string | string[] | Alias accepted for the pre-fill. Normalized the same way a typed domain would be; an array pre-fills a multi-domain run. An invalid entry doesn't fail silently — the widget falls back to the plain entry screen with a notice instead of hanging. |
dnsRecords | DNSRecord[] | Record<string, DNSRecord[]> | Records to apply. Array, or per-domain object mode { "acme.com": [...] }. Omit to use the control-plane's authoritative per-connection set. |
applicationName | 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 | Force the flow onto a subdomain; the widget hides the apex option. |
supportForSubdomains / defaultSubdomain / hostRequired | Subdomain-entry behavior. | |
manualSetupDocumentation | string | Deep-link to your own manual-setup guide. |
enableDkim | boolean | Offer the DKIM screen. |
enableEmail | boolean | Offer the email-setup screen (MX/SPF/DKIM/DMARC template records) before the domain records. |
userId | string | End-user id, echoed back on customdomain:step / onStepChange only (the user field). Not sent to the control plane — see endUserRef for attribution. |
endUserRef | string | Your own id for the end user this connect belongs to. Sent to the control plane as the connection's end_user_ref, so the console renders "Connected by …" and you can search/filter by customer instead of seeing a bare "Direct" connection. Omit to leave the connection's attribution unchanged. |
managed | boolean | Opt this connect into the durable managed (async) rail when the provider/deployment supports it; falls back to the ordinary sync flow otherwise. Default false. |
container | string | CSS selector for embedded mode (implies whiteLabel.embedded). |
apiBase / widgetBase | string | Override the default control-plane / widget hosts (e.g. for a staging environment). |
onSuccess / onClose / onStepChange / onError | fn | Callbacks (mirror the events above). |
There is no applicationUrl field — only applicationName is read for
branding.
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.
Multi-domain prefill
Pass an array to prefilledDomain (or domain) to connect several domains in
one guided run instead of reopening the widget per domain. The widget completes
the first domain, then automatically advances to the next: pendingDomains (not
yet started) and processedDomains (already finished) are included on
customdomain:step / onStepChange and on the final customdomain:success /
onSuccess once more than one domain has been processed, so you can render
progress ("2 of 5 connected") without tracking it yourself.
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. |
font / borderRadius / logo | Font family, corner radius, header logo. |
fontUrl | Load an external stylesheet URL into the widget iframe and apply it as the base font. Requires your host CSP to allow the font origin. |
googleFont | { family, weights?, display? } — load a Google Font by family name instead of hosting a stylesheet yourself. |
hideLogo | Hide the powered-by footer. |
hideConfetti | Disable the success confetti. |
tokens | Override any of the 85 raw design tokens (modal-width, color-primary, radius-md, shadow-modal, …). |
darkMode | "disabled" (default) | "enabled" | "auto" (follows the visitor's OS prefers-color-scheme). |
darkTokens | Raw token overrides applied only while dark mode is active. |
customCopy | Per-locale copy overrides: { [locale | "*"]: { [key]: string } }. A lookup for the active locale falls back to its base language, then to "*". |
icons | Icon slot overrides, keyed by slot: success, error, close, back, domain, dns, manual, share, loading. Each value is an https:/data: image URL or an inline SVG. |
screens.disable | Array of optional screens to hide (e.g. "manualConfiguration", "existingRecords"). Unknown names are ignored; core-flow screens can't be disabled. |
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. |
customProperties | Granular, loosely-typed UI toggles for narrower behaviors (for example forwardLink.url to route the widget's "share by email" action through your own link, or gotoManualLink.hide to hide the manual-setup escape hatch). |
Tokens are emitted as CSS custom properties (--je-<token>) on the widget's
shadow host, so they cannot affect your page.
White-label is an Enterprise-plan entitlement on shared/resumed flows. A
direct open() call always honors whatever whiteLabel you pass — it's your own
client-side configuration. But when a connect is forwarded through
a share link and resumed by a teammate
via loadSharedFlow, the control plane strips any whiteLabel branding from the
resumed session unless your workspace is on the Enterprise plan; the flow still
works, just with default (unbranded) styling. See
Plans & quotas.
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.
Token lifecycle & accessibility
Widget tokens expire (default 60 minutes). On expiry the widget surfaces a session-expired state; mint a fresh token server-side and reopen. 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. The iframe is sandboxed with allow-popups/
allow-popups-to-escape-sandbox so provider OAuth windows can open.