Skip to content

React

The SDK ships a React wrapper at @checktiv/sdk-web/react. It provides two ways to add verification to a React or Next.js app:

  • ChecktivJourney - the recommended zero-lifecycle component. It wraps Checktiv.mount(), so you write one fetchToken callback and nothing else. The SDK owns the whole journey: it obtains the session token once, refreshes it silently, and renders the modules the server declares. No provider, no context, no cleanup code.
  • ChecktivProvider + ChecktivIdv + useChecktiv() - the Checktiv.init()-path wrapper. Use this when you need the ChecktivClient directly (for example to drive mount('fraud', ...) yourself). ChecktivProvider holds the client in React context; useChecktiv() returns it; ChecktivIdv renders the managed IDV module.

Both paths render the managed IDV module, which self-registers on import. Add a one-time side-effect import of @checktiv/sdk-web/idv in your app so the module is registered before the first render. Without it the mount fails with sdk_load_failed.

Terminal window
npm install @checktiv/sdk-web react react-dom

react and react-dom are peer dependencies. The SDK does not bundle them.

ChecktivJourney is the simplest way to render a verification in React. Give it the cell scope (a publishableKey, or a region and mode on the first-party path) and a fetchToken callback that returns the client token your server minted. The component renders one <div>, drives the journey inside it, and tears everything down when it unmounts. It is safe under server-side rendering and React StrictMode.

import { ChecktivJourney } from '@checktiv/sdk-web/react';
import '@checktiv/sdk-web/idv'; // registers the IDV module the journey renders
export function VerifyPage() {
return (
<ChecktivJourney
publishableKey="ah_pk_us_test_..."
fetchToken={async () => {
// Call your server, which mints the session and returns its client token.
const res = await fetch('/api/checktiv/session', { method: 'POST' });
return (await res.json()).clientToken;
}}
theme={{ primaryColor: '#0b5fff' }}
onComplete={({ sessionId }) => {
// Capture is done. Wait for the webhook before acting on the outcome.
console.log('Verification submitted', sessionId);
}}
onEvent={(event) => {
if (event.type === 'checktiv.idv.error') {
console.error('IDV error:', event.error);
}
}}
/>
);
}

fetchToken runs exactly once for a cold start. The component does not re-mount when you pass a fresh onEvent, onComplete, theme, layout, maxWidth, or copy on a later render, so an in-progress capture is never interrupted. Changing the scope (a different publishableKey, or region / mode) does start a fresh journey.

Prop Required Purpose
publishableKey one scope Customer path. The SDK reads the region and mode from the key.
region, mode one scope First-party path (no publishable key). Supply both.
fetchToken yes Returns the client token your server minted. Called once for the cold start.
onComplete no Fires when the applicant finishes capture. Terminal, not a verdict; wait for the webhook.
onEvent no Receives every checktiv.* journey event (navigation and diagnostics).
onConsent no The consent gate a server-declared fraud module needs. See Fraud consent.
layout no 'immersive' lets capture take over the phone viewport; 'inline' (the default) renders in document flow.
maxWidth no A CSS length (for example '32rem' or '480px') that sets the capture frame’s width uniformly across every screen size and orientation. An invalid value falls back to the default sizing; omit to keep it.
theme no White-label theming, for example { primaryColor: '#0b5fff' }.
copy no Overrides the capture UI text (status line, error messages, “Try again” label, coaching banners, frame title). Every key is optional and falls back to English.
resumeKey no Opt-in resume. The SDK persists the token so an abandon-and-return re-mount resumes the current step.
shortCode no The applicant short code. Enables the requestResend self-service recovery (see below).
crossDeviceCopy no Host-injected strings for the cross-device overlay. Required for openCrossDevice() to render; unavailableMessage is mandatory.
onOpenCrossDevice no Optional mint override for the cross-device handoff link. Omit to let the SDK self-mint on the working-token plane.
apiBase no Advanced. Explicit API origin override for a first-party non-production deployment.
fetchImpl no Advanced. The fetch used for network calls. Defaults to the global fetch; used for dependency injection.

If you pass a shortCode, an applicant can request a fresh verification link by email with no backend of your own. Reach it through a ref:

import { ChecktivJourney, type ChecktivJourneyHandle } from '@checktiv/sdk-web/react';
import { useRef } from 'react';
export function VerifyWithResend() {
const ref = useRef<ChecktivJourneyHandle>(null);
async function resend() {
const result = await ref.current?.requestResend('applicant@example.com');
// result.requested is true when the request was accepted. The new link is emailed.
}
return (
<>
<ChecktivJourney
ref={ref}
publishableKey="ah_pk_us_test_..."
shortCode="ABCD-1234"
fetchToken={fetchToken}
/>
<button onClick={resend}>Email me a new link</button>
</>
);
}

requestResend never throws. It resolves { requested: true } when the request was accepted, and { requested: false } when there is no recovery channel (no shortCode) or the request was rate-limited, so you always have an actionable next step.

The handle also exposes openCrossDevice(), which lets a desktop applicant continue on their phone. Pass a crossDeviceCopy prop (and optionally an onOpenCrossDevice mint override), render your own “Continue on your phone” trigger, and call ref.current?.openCrossDevice() from it. The SDK owns the mint, the QR overlay, and the completion poll, and fires onComplete when the phone finishes. On the npm or bundler path, add the side-effect import import '@checktiv/sdk-web/idv/cross-device'; (registers the opener, like the /idv and /fraud imports) before the ref’s openCrossDevice() will work; the CDN bundle registers it automatically.

The trigger is desktop only: openCrossDevice() ignores the call on a touch device (a phone already gets immersive capture), so gate your control to a fine-pointer device. Wire onEvent to recover a handoff that dies mid-flow: a checktiv.idv.error with error.code === 'session_expired' means the session died on the phone, and checktiv.idv.cross_device_capped means the completion poll timed out while the panel stays open. See Cross-device handoff for the full contract.

Collect user info before the journey (useCollectUserInfo)

Section titled “Collect user info before the journey (useCollectUserInfo)”

Some workflows begin with a collect_user_info step that gathers the applicant’s identity details before verification. The embedded capture modules do not render this step. Collect those fields in your own React form and satisfy the step with the useCollectUserInfo hook, then render <ChecktivJourney> so the journey opens on the verification step.

A credit_history workflow always begins that way: the check declares a collect_user_info step and a date of birth as required inputs, so the step is always there. Satisfy it with useCollectUserInfo, or seed the applicant fields on the session create, and add the side-effect import import '@checktiv/sdk-web/credit-history'; so <ChecktivJourney> can render the module the server declares. There are no credit-specific props and no credit-specific component: the journey component renders it like any other declared module. See Credit history.

The hook takes the same zero-lifecycle configuration object as <ChecktivJourney>: a scope (a publishableKey, or a region and mode on the first-party path) and a fetchToken that returns your durable client token. Pass the SAME client token you give <ChecktivJourney>, and pass the SAME memoized session object to both. Each mounts cold (the hook once per mount, the journey when it first renders), so fetchToken runs more than once across the flow; cache the in-flight mint so every call resolves to the SAME client token instead of minting a fresh one.

import { useCollectUserInfo, ChecktivJourney } from '@checktiv/sdk-web/react';
import '@checktiv/sdk-web/idv'; // registers the module <ChecktivJourney> mounts
import { useCallback, useMemo, useRef, useState } from 'react';
type CollectFields = {
familyName: string;
firstName: string;
middleNames: string;
suffix: string;
email: string;
};
export function VerifyFlow() {
// Cache the in-flight client-token request so every caller, cold-start or
// not, resolves to the SAME durable client token instead of minting a new
// one per call.
const clientTokenPromise = useRef<Promise<string> | null>(null);
const fetchToken = useCallback(() => {
if (!clientTokenPromise.current) {
clientTokenPromise.current = (async () => {
const res = await fetch('/api/checktiv/session', { method: 'POST' });
if (!res.ok) throw new Error(`client-token mint failed: ${res.status}`);
const { clientToken } = await res.json();
if (typeof clientToken !== 'string' || clientToken.length === 0) {
throw new Error('client-token mint returned no clientToken');
}
return clientToken;
})().catch((err) => {
// Clear the cache on failure so the next call retries instead of
// replaying a rejected promise forever. (A token_expired recovery
// that remounts this component also gets a fresh ref.)
clientTokenPromise.current = null;
throw err;
});
}
return clientTokenPromise.current;
}, []);
// Memoize the session so the collector and the journey share ONE stable
// object (and therefore one client token), not two separately-minted ones.
const session = useMemo(
() => ({ publishableKey: 'ah_pk_us_test_...', fetchToken }),
[fetchToken],
);
const collector = useCollectUserInfo(session);
const [collected, setCollected] = useState(false);
const [fields, setFields] = useState<CollectFields>({
familyName: '',
firstName: '',
middleNames: '',
suffix: '',
email: '',
});
const [error, setError] = useState('');
const [submitting, setSubmitting] = useState(false);
// Once the collect step is satisfied, render the journey with the SAME
// session: it opens on the verification step, since the collect step no
// longer needs the applicant.
if (collected) {
return (
<ChecktivJourney
{...session}
onComplete={() => {
/* verification finished */
}}
/>
);
}
async function handleSubmit(event: React.FormEvent) {
event.preventDefault();
setError('');
setSubmitting(true);
// One box, one name. Never split a box on whitespace: "Mary Ann" typed
// into First name is ONE given name, and splitting it would invent a
// boundary the applicant did not state. An empty array is a complete
// answer for a person with one name.
const result = await collector.submit({
familyName: fields.familyName,
givenNames: [fields.firstName, fields.middleNames].map((v) => v.trim()).filter(Boolean),
// Its own field, never a givenNames entry: it is displayed last. Omit
// the key when the applicant has none; a blank value is rejected.
...(fields.suffix.trim() ? { nameSuffix: fields.suffix.trim() } : {}),
email: fields.email,
});
setSubmitting(false);
if (result.ok) {
setCollected(true);
} else {
// result.code is a stable, machine-readable reason; result.message is user-visible.
setError(result.message);
}
}
return (
<form onSubmit={handleSubmit}>
<label>
Family name
<input
value={fields.familyName}
required
onChange={(event) => setFields({ ...fields, familyName: event.target.value })}
/>
</label>
<label>
First name
<input
value={fields.firstName}
onChange={(event) => setFields({ ...fields, firstName: event.target.value })}
/>
</label>
<label>
Middle name(s)
<input
value={fields.middleNames}
onChange={(event) => setFields({ ...fields, middleNames: event.target.value })}
/>
</label>
<label>
Suffix
<input
value={fields.suffix}
onChange={(event) => setFields({ ...fields, suffix: event.target.value })}
/>
</label>
<label>
Email
<input
type="email"
value={fields.email}
onChange={(event) => setFields({ ...fields, email: event.target.value })}
/>
</label>
{error && <p role="alert">{error}</p>}
<button type="submit" disabled={submitting}>
Continue
</button>
</form>
);
}

submit() never throws: it resolves { ok: true } or { ok: false, code, message }, so branch on the result. describe() reports three things, two of which you build a form from. fields lists which of the optional contact fields (email, phone, dob, address) the workflow template collects, so you render only the fields the workflow gathers rather than guessing. nameComponents reports what the server enforces about the name parts for this verification: 'required' means a submission without familyName and givenNames is rejected with name_components_required, 'requested' means the template asks for them but a submission without them is still accepted, and 'optional' means a legalName on its own is accepted. Never read any of the three as permission to hide the name inputs. Every submission must name the applicant, as either familyName plus givenNames or legalName alone. A nameSuffix is optional and never counts as a name on its own. The third field, captureStructuredName, is deprecated and is removed in 1.10.0: it reports what the workflow template declares, never what the server enforces, and its default is false. Do not branch on it, and remove any code that does before you upgrade. Satisfy the collect step before you render <ChecktivJourney>, so the journey opens on the verification step. See Collect user info for the full contract and the list of submit() error codes.

init()-path wrapper (ChecktivProvider / ChecktivIdv)

Section titled “init()-path wrapper (ChecktivProvider / ChecktivIdv)”

Use this wrapper when you need the ChecktivClient directly. It is built on Checktiv.init() and the session-token callback.

import { ChecktivProvider, ChecktivIdv } from '@checktiv/sdk-web/react';
import '@checktiv/sdk-web/idv'; // registers the IDV module that ChecktivIdv mounts
export function App() {
return (
<ChecktivProvider
publishableKey="ah_pk_us_test_..."
getSessionToken={async (ctx) => {
const res = await fetch('/api/checktiv/token', {
method: 'POST',
body: JSON.stringify({ reason: ctx.reason }),
headers: { 'Content-Type': 'application/json' },
});
return (await res.json()).token;
}}
theme={{ primaryColor: '#0b5fff' }}
>
<VerificationPage />
</ChecktivProvider>
);
}
function VerificationPage() {
function handleEvent(event) {
if (event.type === 'checktiv.idv.submitted') {
// Capture complete. Wait for the webhook before acting on the outcome.
console.log('Verification submitted');
}
if (event.type === 'checktiv.idv.error') {
console.error('IDV error:', event.error);
}
}
return (
<div>
<h1>Verify your identity</h1>
<ChecktivIdv onEvent={handleEvent} />
</div>
);
}

ChecktivProvider is SSR-safe. On the server (Node.js / edge), init() detects the absence of window and returns null. The provider renders nothing server-side; the client is created and mounted only in the browser.

In Next.js, you do not need "use client" on ChecktivProvider itself - the wrapper handles the guard internally. You do need "use client" on any component that calls useChecktiv() directly or renders <ChecktivIdv> (because those depend on the browser DOM).

// app/verify/page.tsx (Server Component)
import { ChecktivProvider } from '@checktiv/sdk-web/react';
import { VerifyClient } from './VerifyClient';
export default function VerifyPage() {
return (
<ChecktivProvider publishableKey="ah_pk_us_test_..." getSessionToken={...}>
<VerifyClient />
</ChecktivProvider>
);
}
// app/verify/VerifyClient.tsx
'use client';
import { ChecktivIdv } from '@checktiv/sdk-web/react';
import '@checktiv/sdk-web/idv'; // registers the IDV module that ChecktivIdv mounts
export function VerifyClient() {
return <ChecktivIdv onEvent={(e) => console.log(e)} />;
}

ChecktivIdv calls handle.destroy() automatically when the component unmounts. You do not need to clean up manually. Under React’s StrictMode double-invoke, the component mounts, destroys, and remounts cleanly.

If you need the client for custom mount logic, call useChecktiv():

'use client';
import { useChecktiv } from '@checktiv/sdk-web/react';
import '@checktiv/sdk-web/idv'; // registers the IDV module that mount('idv') resolves
import { useEffect, useRef } from 'react';
export function CustomIdvMount() {
const client = useChecktiv();
const containerRef = useRef(null);
useEffect(() => {
if (!containerRef.current) return;
const handle = client.mount('idv', {
target: containerRef.current,
onEvent: (event) => console.log(event),
});
return () => handle.destroy();
}, [client]);
return <div ref={containerRef} />;
}

The fraud module is consent-gated (see Fraud consent). <ChecktivProvider> and <ChecktivIdv> do not expose an onConsent prop; wire it through useChecktiv() and the imperative mount('fraud', { onConsent }) call instead.

'use client';
import { useChecktiv } from '@checktiv/sdk-web/react';
import '@checktiv/sdk-web/fraud'; // registers the fraud module mount('fraud') resolves
import { useEffect } from 'react';
export function FraudConsentGate() {
const client = useChecktiv();
useEffect(() => {
const handle = client.mount('fraud', {
onConsent: async () => {
const granted = await showConsentDialog();
return granted;
},
onEvent: (event) => {
if (event.type === 'checktiv.fraud.error') {
console.error('Fraud module error:', event.error);
}
},
});
return () => handle.destroy();
}, [client]);
return null;
}

Mount <FraudConsentGate /> alongside <ChecktivIdv> in the same subtree (both under the same <ChecktivProvider>). If the session’s declared modules include fraud but this gate is never mounted, the SDK emits a loud checktiv.fraud.error rather than silently skipping collection - see Fraud consent: Default-deny behavior.

onEvent receives every checktiv.idv.* event:

Event type Meaning
checktiv.idv.ready The capture frame loaded and is ready for the applicant. Carries event.config.
checktiv.idv.submitted The applicant finished capture. Terminal for capture; the verdict arrives via webhook.
checktiv.idv.error An error occurred. Check event.error.code and event.error.recovery.

Do not exhaustively switch on event.type. New event types may be added without a major version bump. Handle known types and ignore unknown ones.

checktiv.idv.ready carries the resolved config

Section titled “checktiv.idv.ready carries the resolved config”

checktiv.idv.ready is the one event with a payload beyond its type. Its config is the settings the workflow template resolved for this verification, narrowed to the fields that change what the applicant is about to be shown, so your page can describe the flow without fetching anything:

interface ResolvedIdvConfig {
documentTypes: readonly string[]; // which documents this verification accepts
biometricMode: 'selfie' | 'liveness' | 'none'; // whether a face check follows the document
allowDocumentUpload: boolean; // whether an upload option sits beside the camera
}
<ChecktivIdv
onEvent={(event) => {
if (event.type === 'checktiv.idv.ready') {
setAcceptedDocuments(event.config.documentTypes);
setNeedsCamera(event.config.biometricMode !== 'none' || !event.config.allowDocumentUpload);
}
}}
/>

allowDocumentUpload reflects the workflow template’s Allow photo upload setting. When it is true the applicant may supply an existing photo of the document instead of photographing it, and the capture frame renders that option for you: you never render the control, and there is nothing to enable on your side.

Two things it does not mean, both worth stating because a host page can easily imply otherwise in its own copy:

  • It is not “this verification needs no camera.” Upload covers the document. When biometricMode is selfie or liveness the applicant still reaches a face capture that requires a working camera, as the example above accounts for.
  • It is not a weaker or stronger result. An uploaded document runs the same checks as a photographed one and can pass automatically. The difference is recorded rather than scored: the verification stores how its document arrived and your reviewers see it in the console and on the exported report.

config is a subset of the verification’s settings, not all of them. Server-side gates (a minimum age, for example) are deliberately not surfaced here, because a host page has nothing to render differently for them. Read the fields you need and ignore the rest.

See Document upload for what the setting does and when to turn it on.