AI coding agents integrate the Checktiv Web SDK best when handed the rules and runnable recipes below. The same content ships inside the package so an agent finds it without the docs:
AGENTS.mdand a machine-readablemanifest.jsonship in@checktiv/sdk-web, resolvable via the@checktiv/sdk-web/agentsexport. The manifest carries the module composition contract and the full code for every recipe below.- This page mirrors that content for humans and is included in the docs
/llms-full.txt.
-
Prefer the zero-lifecycle Checktiv.mount entry. The simplest integration is two pieces of code. (1) On YOUR backend, call
sessions.create({ templateId, applicant }, { idempotencyKey })- a thin wrapper overPOST /v1/sessionswith yourah_sk_*secret key - and return ONLY the resultingclientTokento the browser. (2) In the browser, callChecktiv.mount(target, { fetchToken, onComplete }), wherefetchTokenreturns thatclientToken.fetchTokenruns ONCE for the cold / new-device start, NOT on every expiry: the SDK exchanges the durableclientTokenfor a short-lived working token and refreshes it internally, so you write NO token-refresh endpoint, NO expiration handler, and NO resume endpoint. Keysessions.createidempotently per user or reservation so retries and new-device re-entry never double-bill. The legacyChecktiv.init({ ... }).mountProvisioned({ target })with agetSessionTokenbrowser-token callback stays fully supported for existing integrations. -
The server mints the session, never the browser. Your backend calls the public API
POST /v1/sessionswith your secret key (ah_sk_*) and receives the session. The secret key is server-only: NEVER putah_sk_*in browser code. To hand control to the frontend, your backend mints a short-lived browser token (bt_*) by callingPOST /v1/sessions/{id}/browser_tokenwith the sameah_sk_*; that route returns{ data: { browser_token, expires_at } }. Only thebt_*string crosses to the browser. -
Use mountProvisioned as the default path. Call
Checktiv.init({ ... }).mountProvisioned({ target }). It reads the server-declaredsession.modulesfromGET /sdk/v1/sessions/meand renders exactly what the server composed, so the server drives composition.mount('idv')/mount('fraud')is the explicit override / escape hatch, not the first choice. -
Completion is not a verdict: trust the signed webhook. The client event
checktiv.idv.submittedis terminal-for-capture only; it means the applicant finished the capture step, NOT that they passed. The decision arrives on your server through the signedkyc.session.*webhook. Trust the signed webhook as the only outcome anchor; never infer a pass/fail from a client event. -
Satisfy each applicant-info requirement once, through any one path. When a session declares applicant-info requirements (name, date of birth, address, and similar fields), each requirement is satisfied by ANY ONE of the supported collection paths, not by all of them. Supply a field through one path and the SDK treats that requirement as met; do not double-collect the same field. Let the server-declared session decide which fields are required rather than hard-coding a form.
-
Develop local-first with the synthetic driver. Use a test-mode publishable key (
ah_pk_<region>_test_*) PLUS the synthetic opt-in. Test mode is selected by the key itself, not by amodeproperty oninit. A test-mode key without the synthetic option renders a BLANK embed on a local origin because the capture license is domain-bound, whereas the synthetic path runs the fullchecktiv.idv.*flow locally with no camera, no license, and no deployed environment. Ensure your own session-mint backend and token endpoint are running before the SDK callsgetSessionToken. Build and verify locally with the synthetic driver, then graduate to a deployed environment for real capture and ship through the standard npm / CDN path. -
getSessionToken supplies a fresh bt_ on demand.*
getSessionTokenisstring | ((ctx: { sessionId; reason: 'initial' | 'expired' | '401' }) => Promise<string>). The SDK calls it on mount and again on expiry or a 401, then retries the failed request once. Always return a freshly mintedbt_*from your backend; never hold a long-lived credential in the browser. If your callback rejects, the SDK emitstoken_expiredwithrecovery: 'refresh_session'and halts cleanly. -
Add your frontend origin to the publishable key allowlist first. Each publishable key carries an origin allowlist that you set in the console / dashboard. Your frontend’s origin MUST be on that allowlist before the first mount, or EVERY mount fails with
origin_not_allowed(thebt_*data-plane routes reject an off-allowlistOrigin). Add your origin to the publishable key allowlist before you integrate. -
The SDK adds X-Publishable-Key itself on the customer path. Pass
publishableKeytoChecktiv.init(...)and stop there. On the customer (pk-present) path the managed transport attaches theX-Publishable-Keyheader to its ownbt_*data-plane calls internally. Do NOT also hand-add anX-Publishable-Keyheader yourself; double-adding it is wrong. (The first-party hosted path used internally by the verify app carries no publishable key and sends none.) -
Cross-device QR handoff is built into the managed IDV module. Cross-device handoff (a desktop applicant finishes on their phone via a QR + copy-link overlay) is OWNED BY THE MANAGED IDV MODULE. Render it with
client.mount('idv', { crossDeviceCopy, onEvent })and the returned handle exposesopenCrossDevice(): calling it opens the overlay OVER the still-live capture frame, runs the completion poll, and emitschecktiv.idv.submittedwhen the phone finishes so your host reloads. You MUSTimport '@checktiv/sdk-web/idv/cross-device';to enable the overlay (the CDN script-tag bundle already includes it); without that importopenCrossDevice()is a graceful, actionable no-op, as it also is before the applicant verification resolves or when nocrossDeviceCopywas supplied.IdvMountOptionscarriescrossDeviceCopy(host-injected strings; theunavailableMessagekey is required so no arm renders blank) and the OPTIONALonOpenCrossDevicemint override;IdvHandleexposesopenCrossDevice?(), so call it ashandle.openCrossDevice?.(). The zero-lifecycleChecktiv.mount(...)and its React wrapper<ChecktivJourney>ALSO exposeopenCrossDevice()on their handle (reach the React handle via aref), carrying the samecrossDeviceCopy/ optionalonOpenCrossDeviceoptions; on that path the SDK self-mints the handoff on the working-token plane by default (see below), and the CDN script-tag bundle registers the opener too. The trigger is a DESKTOP affordance:openCrossDevice()is a graceful warn-no-op on a coarse-pointer (touch) device, because immersive phone capture and the desktop cross-device overlay are mutually exclusive per device, so gate your trigger to desktop.
Who mints the one-time handoff link depends on the token plane:
• Working-token plane: the module mints the link ITSELF (it calls its own handoff endpoint POST /sdk/v1/sessions/me/handoff, authenticated by the applicant’s short-lived working token, and caps the per-overlay “refresh link” re-mints). Supply onOpenCrossDevice ONLY to point the mint at your own backend instead.
• Browser-token (bt_*) plane: the SDK CANNOT self-mint (the handoff route rejects a bt_* bearer), so you MUST supply onOpenCrossDevice backed by a server-held credential.
onOpenCrossDevice: CrossDeviceHandoffHook is () => Promise<{ kind: 'ok'; url: string } | { kind: 'unavailable' }>. Return { kind: 'ok', url } with the journey URL (which MUST be https:: the SDK collapses javascript:, http:, and data: schemes to 'unavailable') or { kind: 'unavailable' } when the mint fails. The hook is re-callable for the overlay’s “refresh link” affordance; the SDK caps re-mints per overlay.
For a FULLY CUSTOM IDV renderer built on @checktiv/sdk-web/capture (NOT the managed module), the STANDALONE exports drive the overlay yourself and you always supply the mint hook: @checktiv/sdk-web/idv/cross-device (openCrossDeviceOverlay(opts) opens the overlay, preloadCrossDeviceChunk() warms the lazy chunk) and @checktiv/sdk-web/cross-device (mountCrossDevice(target, props) renders the bare panel on a screen you fully control). openCrossDeviceOverlay takes { target, onOpenCrossDevice, copy, isMobile, emit, onClose } plus an optional poll config, and returns a handle with setCompleting() (keep the QR mounted with a waiting label while your page reloads) and destroy() (idempotent). Set isMobile: true to suppress the QR on a phone (a same-device scan is circular). The SDK NEVER writes window.location; navigation is always the host’s, off the emitted event.
Four token-free overlay events (NONE carries the url / OTL / short-code / expiry hint):
• checktiv.idv.cross_device_opened: the mint returned ok and the overlay is shown.
• checktiv.idv.cross_device_unavailable: the mint returned unavailable or URL validation failed.
• checktiv.idv.cross_device_capped: the completion poll hit its total time cap without the phone finishing. NOT an error and NOT a verdict: the QR panel stays mounted so the applicant can still finish on their phone.
• checktiv.idv.cross_device_closed: the applicant dismissed the overlay (back on desktop, try-again on mobile). Fires only on an applicant-initiated dismissal, never on a destroy() you call for an unrelated reason such as tearing the overlay down after the phone already completed.
- The credit history check renders in a hosted frame your page must not reach into. The credit-history step renders inside a Checktiv-hosted frame that your page embeds but does not own. Every value the applicant enters (identifiers, address, the authorization itself) is typed into that frame and posted from it, and the frame is sandboxed and origin-pinned, so your page never sees a credit form value. Write the integration accordingly: do NOT try to read, log, screenshot, session-replay, prefill, re-submit or otherwise capture credit form fields from the host page, and do NOT build your own “resume where they left off” store for them. The frame keeps its own per-screen draft and the SDK re-establishes the frame after a reload, so a host-side copy would be a duplicate of regulated data with none of the protection. Render the step by adding the self-registering import
import '@checktiv/sdk-web/credit-history';(the same side-effect import shape as/idvand/fraud; the CDN script-tag bundle loads it for you) and give the SDK a container to mount into. There is NO credit-specific option onChecktiv.mount(...)or on its React wrapper<ChecktivJourney>: the step is composed by the server and takes no host config.
The event stream is checktiv.credit_history.* and it is STATE ONLY: no member ever carries a field value, an identifier the applicant typed, a score or a finding. Narrow it with the exported isChecktivCreditHistoryEvent(event) guard rather than an event.type === ... comparison, because the SDK event union is forward-open and a bare equality check does not narrow the payload. Three members are the ones to get right:
• checktiv.credit_history.submitted: the step was submitted. It is NOT a verdict and carries no outcome, score or finding. The decision reaches your server on the signed webhook, exactly like every other check.
• checktiv.credit_history.declined: the applicant declined authorization. Nothing was requested and nothing was billed. This is a normal end state and NOT an error: the link stays valid and the session stays resumable, so give a neutral next step, never a failure screen.
• checktiv.credit_history.skipped: the step ended inside the frame with no request and no charge, for a closed reason code. Also a normal state and NOT an error.
Only checktiv.credit_history.error is a failure, and only it reaches an onError callback. Routing declined or skipped to your error path shows the applicant a fault where they made a choice.
Runnable recipes
Section titled “Runnable recipes”The full, copy-pasteable code for each step ships in the package manifest
(@checktiv/sdk-web/agents):
- (e) Zero-lifecycle: sessions.create on the backend, Checktiv.mount on the frontend
- (a) Backend: mint a session with your secret key
- (b) Backend: mint a bt_ browser token and hand it to the frontend*
- (c) Frontend: init and mountProvisioned
- (d) Backend: verify the signed webhook (HMAC) - the only outcome anchor