A lightweight JavaScript/TypeScript SDK for embedding Didit identity verification in web applications.
- 🎯 Simple API - Singleton pattern with easy-to-use methods
- 🔐 Flexible Integration - Use UniLink URL directly or create sessions via backend
- 💸 Transaction Submission - Submit monitored transactions (incl. Travel Rule) with a scoped token, automatic device intelligence, and an auto-launched wallet-ownership flow
- 🛡️ Event Monitoring - Send sign-ups, logins, failed logins and other end-user events to Didit in batches, tied to the same person as their verifications and transactions
- 📱 Responsive - Works on desktop and mobile browsers
- 🎨 Customizable - Configuration options for styling and behavior
- 📦 Multiple Formats - ESM, CommonJS, and UMD builds
- 🔷 TypeScript - Full type definitions included
This repository also holds Didit Authentication for web apps:
@didit-protocol/auth: the headless browser client and server-side token verification.@didit-protocol/auth-react: React components and hooks.@didit-protocol/auth-nextjs: the Next.js App Router integration, with server-sideauth().examples/react: a browser app using@didit-protocol/auth.examples/nextjs: an app with sign-up, sign-in, password reset, signing in again and connected agents.
npm install @didit-protocol/sdk-web
# or
yarn add @didit-protocol/sdk-web<script src="https://unpkg.com/@didit-protocol/sdk-web/dist/didit-sdk.umd.min.js"></script>Event monitoring and device signals live in @didit-protocol/signals, a small package that @didit-protocol/sdk-web depends on and re-exports. Install it on its own when you only need events: npm install @didit-protocol/signals.
Inspect the installed SDK version at runtime:
import { SDK_VERSION } from '@didit-protocol/sdk-web';
console.log(SDK_VERSION);SDK_VERSION is injected from package.json during the build. Release maintainers should update the package with npm version <version> --no-git-tag-version, then run npm run test:build-version. That check validates ESM, CommonJS, UMD, minified UMD, and the emitted TypeScript declaration against both the current version and a simulated future version.
import { DiditSdk } from '@didit-protocol/sdk-web';
// Set up completion callback
DiditSdk.shared.onComplete = (result) => {
switch (result.type) {
case 'completed':
console.log('Verification completed!');
console.log('Session ID:', result.session?.sessionId);
console.log('Status:', result.session?.status);
break;
case 'cancelled':
console.log('User cancelled verification');
break;
case 'failed':
console.error('Verification failed:', result.error?.message);
break;
}
};
// Start verification with URL (from your backend or UniLink)
DiditSdk.shared.startVerification({
url: 'https://verify.didit.me/session/session-token'
});<script src="https://unpkg.com/@didit-protocol/sdk-web/dist/didit-sdk.umd.min.js"></script>
<script>
const { DiditSdk } = DiditSDK;
DiditSdk.shared.onComplete = (result) => {
if (result.type === 'completed') {
alert('Verification completed: ' + result.session.status);
}
};
function startVerification() {
DiditSdk.shared.startVerification({
// You can get this link by clicking on "copy link" in the workflows view
url: 'https://verify.didit.me/u/WORKFLOW_ID_IN_BASE_64'
});
}
</script>
<button onclick="startVerification()">Verify Identity</button>There are two ways to integrate the SDK:
Use your workflow's UniLink URL directly from the Didit Console. No backend required.
DiditSdk.shared.startVerification({
// You can get this link by clicking on "copy link" in the workflows view
url: 'https://verify.didit.me/u/WORKFLOW_ID_IN_BASE_64'
});Get your UniLink URL from the Didit Console → Your Workflow → Copy Link.
The session_id generated will be sent to you by an event. Check event reference here
Your backend creates a session via the Didit API and returns the verification URL. This gives you more control over session creation, user tracking, and security. Read more about how the create session API works here: https://docs.didit.me/reference/create-session-verification-sessions
// Get the verification URL from your backend
const { url } = await yourBackend.createVerificationSession();
DiditSdk.shared.startVerification({
url,
configuration: {
loggingEnabled: true
}
});interface DiditSdkConfiguration {
/**
* Enable SDK logging for debugging
* @default false
*/
loggingEnabled?: boolean;
/**
* Custom container element to mount the modal
* @default document.body
*/
containerElement?: HTMLElement;
/**
* Z-index for the modal overlay
* @default 9999
*/
zIndex?: number;
/**
* Show close button on modal
* @default true
*/
showCloseButton?: boolean;
/**
* Show exit confirmation dialog when closing
* @default true
*/
showExitConfirmation?: boolean;
/**
* Automatically close modal when verification completes
* @default false
*/
closeModalOnComplete?: boolean;
/**
* Render verification inline instead of modal overlay
* @default false
*/
embedded?: boolean;
/**
* Container element ID for embedded mode
* Required when embedded is true
*/
embeddedContainerId?: string;
/**
* Camera the document capture opens first: "front" or "back"
* @default "back"
*/
defaultDocumentCamera?: CameraLens;
/**
* Camera the face (liveness) capture opens first: "front" or "back"
* Applies to passive liveness; devices without a rear camera keep the front one
* @default "front"
*/
defaultLivenessCamera?: CameraLens;
/**
* Show the in-capture camera switcher on the document step
* false locks the user to defaultDocumentCamera
* @default true
*/
showDocumentCameraSwitchButton?: boolean;
/**
* Show the in-capture camera switcher on the liveness step
* false locks the user to defaultLivenessCamera
* @default true
*/
showLivenessCameraSwitchButton?: boolean;
}
type CameraLens = "front" | "back";The same four camera options the native SDKs expose. By default the document capture opens the rear camera and the face capture opens the front (selfie) camera, and both steps offer an in-capture camera switcher on devices with more than one camera.
| Option | Type | Default | Description |
|---|---|---|---|
defaultDocumentCamera |
CameraLens |
'back' |
Lens used when first entering the document capture screen |
defaultLivenessCamera |
CameraLens |
'front' |
Lens used when first entering the liveness (passive face) capture screen |
showDocumentCameraSwitchButton |
boolean |
true |
Show the in-capture camera switcher on the document screen; false locks the user to defaultDocumentCamera |
showLivenessCameraSwitchButton |
boolean |
true |
Show the in-capture camera switcher on the liveness screen; false locks the user to defaultLivenessCamera |
// A kiosk: the operator points the device at the person, on the rear camera, with no way to switch
DiditSdk.shared.startVerification({
url,
configuration: {
defaultLivenessCamera: 'back',
showLivenessCameraSwitchButton: false
}
});CameraLensis'front'or'back'; any other value is ignored, as is a non-boolean switcher flag.- A device without the requested camera keeps the one it has (a laptop asked for
'back'still uses its webcam), and the switcher is hidden on single-camera devices regardless of the flags. - The liveness options apply to the passive liveness check. Active liveness is not affected.
- The SDK forwards the options to the verification page as query parameters of the verification URL:
document_camera,liveness_camera,document_camera_switchandliveness_camera_switch. If you open the verification URL directly instead of through the SDK, set them on the URL yourself (with&when the URL already has a query string, or through theURLAPI'ssearchParams).
Modal mode uses a maximum width of 500px and a maximum height of 700px. On shorter desktop and landscape viewports, the iframe tracks 90% of the visible viewport so the modal never extends beyond its container. The hosted flow owns the scrollbar inside the iframe, keeping later steps reachable without scrolling the page behind the modal.
Browsers that support dynamic viewport units use dvh, while older supported browsers fall back to vh. At widths of 540px and below, the modal fills the visible viewport. The close button and exit confirmation stay inside the modal at every size.
When the hosted verification page reports its layout with a didit:layout message, the modal sizes itself around it:
| Window width | Modal |
|---|---|
| 1024px and wider | The handoff card: 960px by 660px in every state, at most 90% of the window's width and height |
| 541px to 1023px | 500px wide, as tall as the page's content, at most 90% of the window's height |
| 540px and below | Full screen, as above |
The close button becomes a 44px circle over the page's top bar. It follows the page's light or dark theme and moves to the left for right-to-left languages. If a large text size makes the exit confirmation taller than a short page, the confirmation scrolls inside the modal. Until the page reports its layout, and for pages that never do, the modal keeps the 500px by 700px size above. Embedded mode keeps filling its host element. There is nothing to configure, and every existing option keeps working.
The SDK tells the page how it is shown with two query parameters on the verification URL: sdk_presentation (modal or embedded) and, in a modal, sdk_close_button (true or false, following showCloseButton). The SDK handles the didit:layout message itself and does not pass it to onEvent.
Render verification inline instead of a modal overlay. Embedded mode does not apply the modal's 500px by 700px cap; the SDK fills the host element's content box. Give the host an explicit or otherwise resolved height:
<div id="verification-container" style="width: 500px; height: 700px;"></div>DiditSdk.shared.startVerification({
url: 'https://verify.didit.me/u/...',
configuration: {
embedded: true,
embeddedContainerId: 'verification-container'
}
});If the host element has no resolved height, the embedded iframe also has no usable height. Resize the host element when your layout changes; the SDK continues to fill it automatically.
If embeddedContainerId is missing or no element carries it, the launch fails with the failed result (and the error state) instead of silently rendering nothing: embedded mode never falls back to the modal overlay.
The SDK returns three types of results:
Verification flow finished (approved, pending, or declined).
{
type: 'completed',
session: {
sessionId: 'session-uuid',
status: 'Approved' | 'Pending' | 'Declined'
}
}User closed the verification modal.
{
type: 'cancelled',
session: {
sessionId: 'session-uuid',
status: 'Pending'
}
}An error occurred during verification.
{
type: 'failed',
error: {
type: 'sessionExpired' | 'networkError' | 'cameraAccessDenied' | 'unknown',
message: 'Your verification session has expired.'
}
}You can observe the SDK state for custom UI:
DiditSdk.shared.onStateChange = (state, error) => {
switch (state) {
case 'idle':
// Ready to start
break;
case 'loading':
// Loading verification iframe
showLoadingSpinner();
break;
case 'ready':
// Verification in progress
hideLoadingSpinner();
break;
case 'error':
// Error occurred
showError(error);
break;
}
};
// Check current state
console.log(DiditSdk.shared.state);
// Check if verification is presented
console.log(DiditSdk.shared.isPresented);Submit monitored transactions — including Travel Rule transfers and crypto screening — straight from the browser. Your backend mints a short-lived scoped token with POST /v3/transactions/sdk-token/; the SDK does the rest: it binds the subject to the token server-side, attaches device intelligence automatically, and auto-launches the wallet-ownership flow when the transaction requires proof of wallet control.
import { DiditSdk, DiditTransactionError } from "@didit-protocol/sdk-web";
try {
const result = await DiditSdk.shared.submitTransaction({
transactionToken: sdkToken, // minted by YOUR backend - never ship an API key
transaction: {
txnId: "wd-2026-07-07-0042",
category: "travelRule",
details: {
direction: "out",
amount: "0.25",
currency: "ETH",
currencyType: "crypto",
cryptoParams: { crypto_chain: "ETH" },
},
subject: { type: "individual", fullName: "Jane Doe" },
counterparty: {
type: "individual",
fullName: "Ana Diaz",
paymentMethod: { type: "unhosted_wallet", accountId: "0xBeneficiaryWallet01" },
},
travelRule: {
beneficiaryData: { wallet_address: "0xBeneficiaryWallet01", name: "Ana Diaz" },
},
includeCryptoScreening: true,
},
autoLaunchAction: true, // default - auto-launches wallet_ownership in the SDK modal
onActionCompleted: (refreshed) => {
console.log(refreshed.status, refreshed.travelRuleStatus);
},
});
console.log(result.transactionId, result.status);
// verification_session actions are NEVER auto-launched - start them yourself:
if (result.actionRequired?.type === "verification_session") {
DiditSdk.shared.startVerification({ url: result.actionRequired.url });
}
} catch (error) {
if (error instanceof DiditTransactionError) {
// error.type: "expired_token" | "invalid_token" | "validation" | "network"
// error.fieldErrors carries per-field details for validation errors
}
}Key points:
transactionToken— scoped to one user (vendor_data) at mint time; the subject identity is enforced server-side from it, so a tampered client cannot submit for another user.autoLaunchAction(defaulttrue) — auto-launches onlywallet_ownershipactions (in the SDK's overlay modal); averification_sessionaction always comes back inresult.actionRequiredfor your app to launch withstartVerification.- Device intelligence — a privacy-safe fingerprint is attached to every submission automatically; IP and user agent are derived server-side.
onActionCompleted— receives the refreshed transaction after an auto-launched flow completes or its window closes.
Full guide: SDK Transaction Submission.
Send what your end users do (sign-up, login, failed login, logout, password reset, MFA, device change) to Didit's event monitoring, where accepted events are associated with the same person as their verifications and transactions.
Your backend mints a short-lived monitoring token for the signed-in person with POST /v3/events/sdk-token/, then hands it to the page:
import { DiditSdk, DiditEventError, configureEvents, flush } from "@didit-protocol/sdk-web";
// Optional: hear about events Didit refused or the SDK had to drop.
configureEvents({
onError: (error: DiditEventError) => console.warn(error.type, error.eventIds, error.errors),
});
DiditSdk.shared.submitEvent({
monitoringToken, // minted by YOUR backend for this person - never ship an API key
event: { category: "auth", actionType: "login", payload: { method: "password" } },
});
DiditSdk.shared.submitEvent({
monitoringToken,
event: { category: "auth", actionType: "login_failed", payload: { reason: "wrong_password" } },
});
// Before a redirect you control, send what is waiting and read the outcome of each event:
const { acceptedCount, rejectedCount, pendingCount, results } = await flush();How events travel:
- Batched to
POST /v1/events/batch/with theX-Monitoring-Tokenheader: as soon as 25 events are waiting, after 5 seconds, and when the page is hidden or closed. Change the numbers withconfigureEvents({ batchSize, flushIntervalMs }). - The person comes from the token, never from the page: Didit binds every event to the
vendor_datathe token was minted for. Each event keeps the token it was submitted with, so events from before and after a sign-in never mix people. - Device intelligence is attached to every batch (the same fingerprint as transaction submissions); IP address, user agent and origin are taken server-side.
- Leaving the page starts sending waiting events immediately with
fetchandkeepalive, which outlives the page (browsers allow 64 KiB per page for this). If fingerprint hashing is still pending, the request retains the persistent device id and collected signals but omits the unfinished hash. See the device-context tradeoff for stored events.sendBeaconis not used because it cannot carry the token header. - Safe retries: on a network error, HTTP timeout, rate limit or server error the same events are sent again after 10, 20 and 40 seconds, then every minute (or right away when you call
flush()). An event Didit already stored comes back asreplayed, never as a duplicate. - Refusals are final: an invalid, expired, revoked or used-up token (
invalid_token,expired_token), event monitoring switched off for the application (monitoring_disabled) or an event that fails validation (validation) is reported throughonErrorand in theflush()results, and is not sent again. - Queue pressure: the queue targets 1,000 waiting events (
maxQueueSize). When the queue is full, page views and other low-value events are dropped first (dropped); security, payment, dispute, responsible-gambling and bet events are never dropped, so protected events can exceed that target.
| Event field | Description |
|---|---|
category |
auth, session, security, payment, dispute, gameplay, bonus, affiliate, responsible_gambling or custom |
actionType |
What happened, e.g. signup, login, login_failed, logout, password_reset, mfa_challenge |
eventId |
Your idempotency key; a UUID is generated when omitted. submitEvent returns it |
occurredAt |
Date or ISO 8601 string; defaults to now |
payload |
Category-specific keys, e.g. payment_reference_id, amount, currency, payment_method and status for payments |
session, transactionContext, customValues, subject, business, timeZone |
Optional context; subject only adds profile hints, the person always comes from the token |
Delivery is best effort: the queue is in memory, and browsers can terminate pending requests. Await flush() and check pendingCount before a redirect you control. Browser delivery requires the API to allow X-Monitoring-Token and the fingerprint headers in its CORS policy.
Full guide: SDK Event Submission.
The singleton SDK instance.
| Method | Description |
|---|---|
startVerification(options) |
Start the verification flow |
submitTransaction(options) |
Submit a monitored transaction with a scoped token (guide) |
submitEvent(options) |
Queue an end-user event for event monitoring with a monitoring token; returns the event id (Event Monitoring) |
close() |
Programmatically close the verification modal |
destroy() |
Destroy the SDK instance and clean up |
| Property | Type | Description |
|---|---|---|
state |
DiditSdkState |
Current SDK state |
configuration |
DiditSdkConfiguration |
Current configuration |
isPresented |
boolean |
Whether verification modal is open |
errorMessage |
string |
Error message (when in error state) |
| Callback | Parameters | Description |
|---|---|---|
onComplete |
(result: VerificationResult) |
Called when verification finishes |
onStateChange |
(state: DiditSdkState, error?: string) |
Called on state changes |
onEvent |
(event: VerificationEvent) |
Called on granular verification events |
Track verification progress with the onEvent callback:
DiditSdk.shared.onEvent = (event) => {
console.log('Event:', event.type, event.data);
};| Event | Description | Data Payload |
|---|---|---|
didit:ready |
The page is ready and the sessionId is available | { sessionId } |
didit:started |
User started verification | { sessionId? } |
didit:step_started |
A verification step began | { step, sessionId? } |
didit:step_completed |
A step finished successfully | { step, nextStep?, sessionId? } |
didit:step_changed |
Current step changed | { step, previousStep?, sessionId? } |
didit:media_started |
Camera/media capture started | { step, mediaType } |
didit:media_captured |
Photo/video captured | { step, isAuto } |
didit:document_selected |
User selected document type and country | { documentType, country } |
didit:verification_submitted |
Data submitted for processing | { step, sessionId? } |
didit:code_sent |
OTP code sent | { step, channel?, codeSize?, sessionId? } |
didit:code_verified |
OTP code verified | { step, sessionId? } |
didit:status_updated |
Session status changed | { status, previousStep?, sessionId? } |
didit:completed |
Verification flow completed | { sessionId?, status?, country?, documentType? } |
didit:cancelled |
User cancelled verification | { sessionId? } |
didit:error |
An error occurred | { error, step?, sessionId? } |
didit:close_request |
User requested to close modal | (no data) |
The step field can be one of:
document_selection- Document type selectiondocument_front- Front side of documentdocument_back- Back side of documentface- Face/liveness verificationemail- Email verificationphone- Phone verificationpoa- Proof of addressquestionnaire- Questionnaire step
The channel field in code_sent can be:
email- Code sent via emailsms- Code sent via SMSwhatsapp- Code sent via WhatsApp
The codeSize field in code_sent indicates the OTP code length (e.g., 4 or 6 digits).
- Chrome 70+
- Firefox 65+
- Safari 12+
- Edge 79+
Each release starts from a tag pushed to a commit on master. The workflow checks that the tag matches the package versions and that each package's CHANGELOG.md has a section for that version, then creates the GitHub release and publishes to npm with trusted publishing.
| Tag | Publishes | Workflow |
|---|---|---|
signals-v<version> |
@didit-protocol/signals |
release-signals.yml |
auth-v<version> |
@didit-protocol/auth, then @didit-protocol/auth-react, then @didit-protocol/auth-nextjs, all at the same version |
release-auth.yml |
v<version> |
@didit-protocol/sdk-web |
release.yml |
When versions depend on each other, release in this order and wait for each to reach npm: signals, then auth, then sdk-web. The auth and sdk-web workflows stop before publishing if the @didit-protocol/signals version they depend on is not on npm yet. @didit-protocol/auth-nextjs pins @didit-protocol/auth and @didit-protocol/auth-react at exactly its own version, so bump all three together; npm run test:release checks that before a tag does.
Copyright © 2026 Didit. All rights reserved.