Custom Domain docs
Widget SDK

Reference

The seven methods, the eight window events and callbacks, full config, statuses, and theming tokens.

Methods

window.customdomain exposes seven methods:

MethodPurpose
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 eventCallbackFiresdetail / arg
customdomain:steponStepChange(step)on every screen change{ step, domain?, provider?, pendingDomains?, processedDomains? } — the last two appear only during a multi-domain flow
customdomain:successonSuccess(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:closeonClose(detail)when the modal closes{ lastStatus, error?, shared?, manualScreenDisabled? }
customdomain:purchasethe 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:erroronError(err)any hard failureCustomDomainError ({ code, message, details? }), mirroring onError
customdomain:request-closehost-delegated dismissal (whiteLabel.delegateClose)provider payload; you call close()
customdomain:doc-clickuser clicked a documentation linklink payload
customdomain:shareda shared/delegated setup link was producedshare 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: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

FieldTypeNotes
applicationId (required)stringPublic application id.
token (required)stringWidget JWT from POST /v1/tokens.
domainstringPre-filled domain.
prefilledDomainstring | 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.
dnsRecordsDNSRecord[] | 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.
applicationNamestringBranding shown in the flow.
localestringUI locale.
whiteLabelWhiteLabelTheming + behavior (below).
forceManualSetupbooleanSkip automatic rails; always show records.
forceSubdomainbooleanForce the flow onto a subdomain; the widget hides the apex option.
supportForSubdomains / defaultSubdomain / hostRequiredSubdomain-entry behavior.
manualSetupDocumentationstringDeep-link to your own manual-setup guide.
enableDkimbooleanOffer the DKIM screen.
enableEmailbooleanOffer the email-setup screen (MX/SPF/DKIM/DMARC template records) before the domain records.
userIdstringEnd-user id, echoed back on customdomain:step / onStepChange only (the user field). Not sent to the control plane — see endUserRef for attribution.
endUserRefstringYour 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.
managedbooleanOpt this connect into the durable managed (async) rail when the provider/deployment supports it; falls back to the ordinary sync flow otherwise. Default false.
containerstringCSS selector for embedded mode (implies whiteLabel.embedded).
apiBase / widgetBasestringOverride the default control-plane / widget hosts (e.g. for a staging environment).
onSuccess / onClose / onStepChange / onErrorfnCallbacks (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:

KeyEffect
colorsprimary, primaryHover, background, text, success, error, … mapped onto tokens.
font / borderRadius / logoFont family, corner radius, header logo.
fontUrlLoad 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.
hideLogoHide the powered-by footer.
hideConfettiDisable the success confetti.
tokensOverride 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).
darkTokensRaw token overrides applied only while dark mode is active.
customCopyPer-locale copy overrides: { [locale | "*"]: { [key]: string } }. A lookup for the active locale falls back to its base language, then to "*".
iconsIcon 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.disableArray of optional screens to hide (e.g. "manualConfiguration", "existingRecords"). Unknown names are ignored; core-flow screens can't be disabled.
delegateCloseThe widget never tears itself down; it posts customdomain:request-close and you call close().
embeddedRender as a flat, full-bleed panel inside your container instead of a fullscreen modal.
customPropertiesGranular, 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.

On this page