Skip to main content

Hosted UI

Hosted UI is the fastest way to launch a secure, branded authentication experience without building login and verification screens from scratch. It provides a framework-agnostic SDK for white-labeled authentication, face-liveness verification, and onboarding.

New to Hosted UI? Start with What is Hosted UI? and Benefits & Features. This page is the integration guide for engineers.


Managed onboarding in your branding

Hosted UI
  • How the hosted UI presents sign-up, sign-in, OTP, and policy steps in sequence.
  • Where face liveness and ID capture appear when enabled for your tenant.
  • How branding (logo and colors) carries across screens without custom frontend work.

This clip walks through the Hosted UI experience end to end: your user launches the SDK-driven flow, completes OTP, agreements, and verification steps inside ChainIT-managed screens styled with your brand, then returns tokens to your app through the SDK callback.

Fully Managed Authentication

Hosted UI covers OTP, face liveness, and ID capture in ChainIT-hosted screens—no custom auth UI. This page walks through portal properties, creating the application, when to adopt Hosted UI, handling tokens from the SDK, branding, then integration architecture and the interactive guide.

Building this with an AI coding agent?

Connect it to the ChainIT SDK MCP server first. It answers with the canonical package name, real SDK declarations, and vetted scaffold files. Read-only, no API key.

Hosted UI Properties in Developer Portal

When creating a Hosted UI application, you can configure the following:

PropertySupportedDescription
brandingLogo, colors, display name, font, and optional per-screen copy for the hosted UI. See Branding customization.
originURLOptionalURL whitelist for websites that are allowed to host the Hosted UI flow (including IDScan iframe). Configure one origin per line under URL White Listing (CORS); include each environment origin explicitly. See the origin whitelisting guide.
tokenManagementConfigure token expiration and refresh policies.
callbackURLNot required for Hosted UI. Completion is handled by SDK callbacks (onSuccess / onError), not OAuth redirect URIs.
scopesHandled automatically by the hosted flow logic.

Application access policy

When the Developer Portal application has User Type and Member Access configured (Employee/Member apps), the SDK's first call — POST /users/v1/hosted-auth/app-config — returns top-level policy fields alongside branding:

FieldTypeDescription
allowSignupbooleanWhether the hosted UI may offer sign-up. Server-derived — do not recompute client-side.
authUserType"employees" | "members"Which ledger and rules apply at verify time.
memberAccessMode"sign_in_only" | "sign_in_and_sign_up"Member apps only; drives allowSignup.

Derived allowSignup (authoritative on server):

authUserTypememberAccessModeallowSignup
employees(ignored)false
memberssign_in_onlyfalse
memberssign_in_and_sign_uptrue

The SDK hides the Sign Up link when allowSignup === false and coerces initialScreen: 'signup' to sign-in. Server-side enforcement on initiate and verify remains mandatory — UI hiding is UX only.

Group IDs are not in the SDK contract. Partners configure Member Groups in the Developer Portal; authorization runs in users-ms against session snapshots. End users never pick groups in the hosted UI.

Package naming

The SDK package is @chainitservices/hosted-ui. Older docs and READMEs mention @mohitebiz/white-labeled-auth; that package is superseded and should not be installed.


Policy error codes

Configured Employee/Member apps may return 403 Forbidden with these stable codes during initiate or verify (distinct from bootstrap errors like origin_not_allowed or invalid_configuration):

CodeHTTPWhenIntegrator action
SIGNUP_NOT_ALLOWED403Sign-up attempted when allowSignup is falseHide sign-up in UI; show message if user deep-links to signup
USER_NOT_FOUND_IN_LEDGER403User not in employee/membership ledger for the app orgExplain no access; contact org admin
NOT_IN_ALLOWED_MEMBER_GROUP403Member has ledger but not in any configured access groupSame user-facing copy; log distinct code for support

The SDK passes the backend code on callbacks.onError as error.error (SCREAMING_SNAKE). Branch on it explicitly:

callbacks: {
onError: (error) => {
if (error.error === "NOT_IN_ALLOWED_MEMBER_GROUP") {
// Member not in allowed group — distinct from ledger-only denial
} else if (error.error === "USER_NOT_FOUND_IN_LEDGER") {
// No ledger row for this org
} else if (error.error === "SIGNUP_NOT_ALLOWED") {
// Sign In Only or Employee app
}
},
},

Bootstrap / app-config failures may still surface invalid_configuration or origin_not_allowed — policy codes above apply to onboarding initiate/verify after app-config succeeds.


Create the application

Register the Hosted UI application from the Developer Portal UI:

  1. Sign in to the portal — see Developer Portal — Getting Started.
  2. Open ApplicationsCreate / Register application.
  3. Pick Hosted UI as the type.
  4. Fill in Name and Description.
  5. Under Application Configuration, add allowed origins in URL White Listing (CORS) (one origin per line). See the origin whitelisting guide for rules. Do not use *.
  6. Open the Branding tab and apply your logo, colors, brand name, and font — see Branding customization.
  7. Adjust Token management (expiration, refresh policy) if the org defaults do not fit.
  8. Save the application and copy clientId / clientSecret immediately — the secret is shown only once.

Step-by-step screens are in Create application — step-by-step.

Treat the client secret as a server-only credential

Your backend needs clientSecret for POST /users/v1/hosted-auth/client-session. It must never ship to the browser. See Retrieve and store credentials.


When to Use Hosted UI

Choose Hosted UI when you want ChainIT-hosted onboarding with your branding, and a thin frontend that relies on SDK callbacks while your backend holds clientSecret and issues client sessions.


Handling the Response

Once the user completes the flow, the SDK's onSuccess callback returns standard OIDC tokens.

  • id_token: Contains the user's identity claims. Validate this token.
  • access_token: Used to call the UserInfo API to fetch additional profile details. The SDK provides a built-in fetchUserInfo() / useUserInfo() that handles the token attachment automatically — see the UserInfo API reference.

Branding customization

Hosted UI runs entirely inside ChainIT-managed screens (sign-in, sign-up, OTP, terms, face liveness, ID capture). Branding is how you make that journey feel like your product instead of a generic page: your logo, palette, product name, typography, and—when you need it—custom titles and button labels per step.

What you can customize

AreaWhat it controls
LogoShown on auth and verification steps; use a horizontal or square mark that reads clearly at small sizes.
Primary / secondary colorsPrimary drives buttons, links, and key accents; secondary supports banners, chips, and secondary actions. Pick pairs that stay legible for text-on-filled-button (WCAG contrast).
Brand nameShort label shown in the hosted UI (for example your product or company name).
Font familyWeb font name available to the hosted bundle (defaults such as Inter match the portal).
Per-screen copyOptional overrides for titles, subtitles, helper text, and primary buttons per screen type (for example different OTP copy vs. face-scan instructions). In the Developer Portal this is organized by screen; programmatically it maps to screen configurations with a screenType and requiredTexts.

Screen types align with the onboarding path—for example login, signup, verify (OTP), terms, face instructions and capture, and ID scan. Tuning copy is useful when legal, locale, or tone-of-voice requirements differ from the defaults.

Where to configure it

Branding lives on the application itself and is managed entirely from the Developer Portal:

  1. Open your Hosted UI application in Applications.
  2. Open the Branding tab.
  3. Upload your logo, set primary / secondary colors, brand name, and font family. Optionally override per-screen titles and button labels under the screen configurations section.

  4. Save — the next hosted-auth session loads the new branding without changing clientId.

After you ship v1, iterate on branding without changing clientId: update colors or copy in the portal and re-test the hosted flow on staging before production.


Integration Architecture

Origin enforcement (X-Origin)

ChainIT validates the website running the SDK against your application's URL whitelist using an X-Origin header. Integrators do not need to wire this up — the SDK's HTTP layer attaches X-Origin: window.location.origin to every browser-side hosted-auth call automatically.

The check runs on the SDK's first browser-side call (/users/v1/hosted-auth/app-config) — before any branding, screens, or user input is exposed. If the origin isn't allowlisted, the call fails with 401 Unauthorized (Origin is not allowed) and the SDK shows a branded "Origin is not allowed" screen with the same code an integrator would see in the iframe host.

The X-Origin check intentionally does not apply to POST /users/v1/hosted-auth/client-session. That call runs server-to-server from your backend, where forwarding a browser-origin header isn't reliable; credentials (clientId + clientSecret) authenticate that hop.

Add every environment origin you ship to (production, staging, local dev, etc.) under URL White Listing (CORS) when you create or edit the Hosted UI application. Subdomain wildcards (https://*.acme.com) are supported; bare * (any origin) is rejected. See the full origin whitelisting guide.

Mobile QR face handoff

Overview

Some users don't have a working webcam on their desktop, or the camera quality is too low for face liveness. Hosted UI supports a device handoff: a face-verification leg started on the desktop can be finished on the user's phone. The experience is fully white-label and stays on your own domain the whole time — there is no ChainIT-branded page and no separate verification URL to configure.

The handoff works for both the login / onboarding face step and standalone face re-verification.

How it works

  1. Desktop reaches face liveness without a usable camera. The SDK requests a QR session (POST /users/v1/hosted-auth/liveness/qr-session) and renders the returned QR code.
  2. The backend mints a single-use handoff code and a qrId. The QR encodes a return URL that points back to the same page that started Hosted UI (origin + pathname) with the handoff code on a query param — https://your-app.com/login?h=<code> (or your handoffParam).
  3. The user scans the QR with their phone and lands on your page.
  4. The SDK resumes automatically. HostedAuthProvider detects the handoff code, clears any stale request token, resolves the code (GET /users/v1/hosted-auth/liveness/qr-session/resolve, origin-validated), and mounts the face surface in place — no extra route or integration code. The phone runs liveness and verifies.
  5. Completion. The backend burns the handoff code, the SDK strips the param from the phone URL, and the waiting desktop is notified over Centrifugo (with status polling as a fallback). The desktop's QR status flips from pending to completed.

Customer setup

Setup is minimal. Keep HostedAuthProvider mounted on the page where Hosted UI runs, and make sure that page's origin is whitelisted under URL White Listing (CORS). Because the QR returns to the current page, there is no verificationUrl to set and no dedicated route to build — the provider renders the handoff surface in place when it sees the handoff param. This holds whether the phone browser is logged in or logged out; the phone can only complete the face scan for the waiting desktop session.

If you must own the handoff route yourself, pass disableFaceHandoffAutoMount to HostedAuthProvider and mount the exported FaceHandoff component. Most integrations should keep the default auto-mount.

Customizing the handoff query param

The handoff code is carried on a query param named h by default. If h collides with a param your application already uses, rename it with handoffParam:

const hostedAuthConfig: HostedAuthConfiguration = {
clientId: "<<your_client_id>>",
session: { getSession },
handoffParam: "cv", // QR returns to https://your-app.com/login?cv=<code>
};

The same name is sent to the backend when the QR is minted, so the QR image, the returned URL, and the SDK's detection all use it consistently. It must be a URL-safe key (letters, digits, _, -; max 32 characters); any invalid value falls back to h. Whatever name you pick is reserved on those pages exactly like h (see the warning below).

The handoff query parameter is reserved

The handoff query parameter (h by default, or your handoffParam) on pages wrapped by HostedAuthProvider is reserved for the mobile face handoff. Do not reuse it for your own routing, analytics, or feature flags on those pages — any value present on load makes the SDK render the handoff surface instead of your app. If you need the param free, render Hosted UI on a route that never receives it, pick a distinct handoffParam, or opt out with disableFaceHandoffAutoMount.

<HostedAuthProvider config={hostedAuthConfig}>
{/* Normal app / Hosted UI UI. If the page is opened with ?h=<code>,
the provider automatically renders the mobile face handoff instead. */}
<InitiateAuthFlow />
</HostedAuthProvider>

Responsibilities

CustomerSDKBackend
Whitelist the page origin and keep HostedAuthProvider mounted (default auto-mount). No verification URL or extra route.Request the QR session, render the QR, detect and resolve the handoff code, run liveness in the embedded face iframe, relay the result, and strip the spent code from the URL.Mint the handoff code + qrId, build the return URL, validate X-Origin on resolve, track status, notify the desktop, and burn the code on completion.

Status and notifications

The desktop learns the handoff is finished two ways:

  • Realtime push over Centrifugo as soon as the phone verifies.
  • Polling fallback: GET /users/v1/hosted-auth/liveness/qr-session/status?qrId=<qrId> returns a status of:
    • pending — the phone has not finished the face scan yet.
    • completed — the phone verified; the desktop can advance.
    • expired — the qrId no longer matches the active session.

Security and cleanup

  • The QR returns to the current page's origin + pathname; there is no separate handoff URL to configure.
  • The mobile face scan runs inside the embedded-host secondary face iframe.
  • The iframe sends X-Origin with the integrator origin, and the backend validates it against the Hosted UI application's URL whitelist before resolving the handoff code — an arbitrary page that scrapes a scanned code cannot redeem it.
  • Any stale Hosted UI request token on the integrator origin is cleared before the iframe loads.
  • The resolved request token and qrId are kept in iframe memory only and cleared when the handoff completes or is cancelled.
  • On success, the SDK removes the handoff param from the phone URL, and the backend burns the single-use handoff code so it cannot be reused.

Benefits

  • Fully white-label — runs entirely on your domain with your branding.
  • Customer-owned URLs; no separate verification URL or route to build.
  • Seamless transfer from desktop to phone for the face step only.
  • Single-use, origin-validated handoff codes that are burned on completion.
  • Consistent verification experience across login and re-verification.

Prerequisites

Never expose clientSecret

Never send the secret to the browser or embed it in a SPA. The browser calls your session route; your server calls ChainIT.


Integration Guide

Before you integrate

Register a Hosted UI application in the Developer Portal and capture clientId / clientSecret. Configure branding from the portal’s Branding tab when you are ready for logos and per-screen copy.

Step-by-step walkthrough: Create application — step-by-step.

React implementation

Prerequisites

Your project must have these dependencies installed (the SDK lists them as peerDependencies — they are never bundled):

PackageRequired versionMandatory
react>=18 <20Yes
react-dom>=18 <20Yes
axios^1.0.0Yes
@tanstack/react-query^5.0.0Yes
npm install @chainitservices/hosted-ui@latest

NPM package entry points (exports)

Import / assetWhen to use
@chainitservices/hosted-uiReact apps: HostedAuthProvider, InitiateAuthFlow, ReverifyFace, etc. (import / require via module / main).
@chainitservices/hosted-ui/style.cssStylesheet export when dist/style.css is present in the tarball you install; otherwise theme CSS may ship via the JS bundle—confirm for the tarball you resolve.

Optional peer dependencies

These are marked optional in the package manifest — install only if your build resolves those code paths:

PackageSupported range
zod^3.0.0 or ^4.0.0
react-hook-form^7.0.0
jwt-decode^4.0.0
uuid^9, ^10, ^11, or ^13
React single-copy requirement
The SDK externalizes react and react-dom as peer dependencies — it never bundles its own React copy. Make sure your project has exactly one installed version. Frameworks like Next.js resolve their own React build; the SDK will use that automatically. If styles are missing, confirm your bundler is not dropping side-effect imports from node_modules/@chainitservices/hosted-ui.
2
Backend: ChainIT client-session proxy
1

Expose POST /api/hosted-auth/client-session (or similar) for the browser.

2

Read clientId / clientSecret from server env vars only.

3

Forward JSON to ChainIT https://staging-api.chainit.online/users/v1/hosted-auth/client-session and return the session payload.

Never expose clientSecret
The ChainIT request runs only on your server. The browser calls your wrapper route, not ChainIT with the secret.
import express from "express";

const app = express();
app.use(express.json());

const CHAINIT_SESSION_URL = "https://staging-api.chainit.online/users/v1/hosted-auth/client-session";

app.post("/api/hosted-auth/client-session", async (req, res) => {
try {
const response = await fetch(CHAINIT_SESSION_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
clientId: process.env.CHAINIT_HOSTED_AUTH_CLIENT_ID,
clientSecret: process.env.CHAINIT_HOSTED_AUTH_CLIENT_SECRET,
}),
});
const data = await response.json();
res.status(response.status).json(data);
} catch {
res.status(500).json({ error: "Session creation failed" });
}
});

app.listen(3000, () => {
console.log("Backend listening on :3000");
});
3
Wire HostedAuthProvider + InitiateAuthFlow
1

Implement session.getSession to POST to your backend route and return JSON.

2

Handle onSuccess / onError for tokens and failures.

import {
HostedAuthProvider,
InitiateAuthFlow,
useLogout,
type HostedAuthConfiguration,
} from "@chainitservices/hosted-ui";

const config: HostedAuthConfiguration = {
clientId: "<<your_client_id>>",
session: {
getSession: async () => {
const res = await fetch("/api/hosted-auth/client-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
});
if (!res.ok) throw new Error("Session fetch failed");
return res.json();
},
},
callbacks: {
onSuccess: (tokens) => console.log(tokens),
onError: (error) => console.error(error),
},
};

function SignOutButton() {
const logout = useLogout();
return <button onClick={() => logout()}>Sign out</button>;
}

export default function App() {
return (
<HostedAuthProvider config={config}>
<InitiateAuthFlow initialScreen="signin" />
<SignOutButton />
</HostedAuthProvider>
);
}
4
Handle tokens after login
1

On success you receive accessToken, idToken, and refreshToken.

2

Validate and store them using your app’s session strategy.

3

Use the SDK's built-in fetchUserInfo() / useUserInfo() to fetch the user profile.

import { useUserInfo } from "@chainitservices/hosted-ui";

function Profile() {
const { data: userInfo, isLoading, error, refetch } = useUserInfo();

if (isLoading) return <p>Loading profile...</p>;
if (error) return <p>Failed to load profile: {error.message}</p>;

return (
<div>
{userInfo?.picture && (
<img src={userInfo.picture} alt="" width={64} style={{ borderRadius: "50%" }} />
)}
<p><strong>{userInfo?.name}</strong></p>
<p>{userInfo?.email}</p>
<button onClick={() => refetch()}>Refresh</button>
</div>
);
}

The SDK's axios interceptor attaches the stored access token automatically. No token parameter needed. See the UserInfo API docs for the full field reference.

Reference tables follow (types, props, HTTP paths).


Initiate Auth Flow — SDK reference

The full onboarding flow handles sign-in, sign-up, OTP verification, face liveness, and ID document scanning in a managed UI.

Configuration

The SDK is configured via the HostedAuthConfiguration object:

interface HostedAuthConfiguration {
clientId: string;
session: {
getSession: () => Promise<{
sessionId: string;
sessionSignature: string;
}>;
};
callbacks?: {
onSuccess?: (tokens: AuthTokens) => void;
onError?: (error: HostedAuthError) => void;
};
config?: {
face?: {
timeout?: number;
};
};
handoffParam?: string;
}
FieldRequiredDescription
clientIdOAuth client ID for the application. Sent on every hosted-auth call; servers validate it.
session.getSessionReturns { sessionId, sessionSignature } from your backend's /client-session proxy.
callbacks.onSuccessReceives { accessToken, idToken?, refreshToken? } once onboarding completes.
callbacks.onErrorReceives fatal SDK errors that the built-in retry UI could not recover from.
config.face.timeoutLiveness detection timeout in ms. Default 30000.
handoffParamQuery-param name for the mobile QR face-handoff code. URL-safe key, max 32 chars. Default h. Reserve it for the handoff only.

HostedAuthProvider props

PropTypeDefaultDescription
configHostedAuthConfigurationRequiredSession resolver, callbacks, and optional feature config.
disableFaceHandoffAutoMountbooleanfalseOpt out of automatic mobile QR handoff handling when the page opens with ?h=<code>.

InitiateAuthFlow props

PropTypeDefaultDescription
initialScreen'signin' | 'signup' | 'face''signin'The first screen to show when the flow starts

Logout — SDK reference

Once the user is signed in, sign them out via the SDK's useLogout() hook (React) or HostedAuth.logout() (UMD / vanilla JS). Both call POST /oauth/logout with the stored refresh + ID tokens, send the access token as Authorization: Bearer so the backend can blacklist it, and clear local token storage.

import { useLogout } from "@chainitservices/hosted-ui";

function SignOutButton() {
const logout = useLogout();
return <button onClick={() => logout()}>Sign out</button>;
}

logout() returns { success, message }. Local tokens are cleared even if the server call fails, so the user is always signed out locally.

See RP-Initiated Logout for the full endpoint contract and Token revocation for the lower-level POST /oauth/revoke endpoint.


Face Re-verification — SDK reference

Step-up face liveness for users who are already signed in. Runnable React and JavaScript snippets live in the Hosted UI guide when you select Face Re-verification.

When to use

  • High-value or sensitive actions (payments, account changes)
  • Periodic re-proofing of the same user

ReverifyFace props

PropTypeRequiredDescription
onSuccess() => void✅ YesFired when the face liveness check is confirmed and the user is successfully re-verified
onClose() => void✅ YesFired when the user cancels or closes the camera UI (either by pressing close or completing verification)
handleRetryError(error: Error) => boolean○ NoReturn true to handle errors manually instead of the SDK default UI

Mobile WebView (camera, viewport, destroy): see the JavaScript stack in the guide when Face Re-verification is selected.


Next steps