Some applicants start a verification on a desktop that has no usable camera. Cross-device handoff lets them continue on their phone: the SDK shows a QR code and a copy-link control, the applicant opens the link on their phone, and your host page reloads once the phone finishes.
The SDK provides two related subpaths:
./cross-device- the standalone handoff panel (mountCrossDevice). It renders the QR, the copy-link field, and the device-aware controls../idv/cross-device- the overlay orchestration for a custom IDV renderer (openCrossDeviceOverlay,preloadCrossDeviceChunk). It manages the overlay lifecycle, calls your mint hook, and renders the panel from the correct instance.
Cross-device handoff is host-orchestrated: the SDK never navigates and never mints the handoff link itself. Your backend mints the one-time link (it holds the session cookie the browser SDK cannot use), and your host polls for completion and reloads. This is an advanced surface; the internal Checktiv verify journey uses it, and white-label hosts building a custom IDV renderer can use it too.
When to use it
Section titled “When to use it”Offer cross-device handoff when a desktop applicant needs a phone camera: for example, after a camera_denied or sdk_load_failed error whose recovery is cross_device, or as a proactive “continue on your phone” affordance next to the capture frame. On a phone, the panel drops the QR (scanning your own screen is circular) and instead offers a try-again control, an open-in-browser hint, and a copy-link.
The mint hook
Section titled “The mint hook”Your host supplies a mint hook that calls your own backend to produce a one-time link URL:
import type { CrossDeviceHandoffHook } from '@checktiv/sdk-web';
const onOpenCrossDevice: CrossDeviceHandoffHook = async () => { const res = await fetch('/api/cross-device/mint-otl', { method: 'POST' }); if (!res.ok) return { kind: 'unavailable' }; const { url } = await res.json(); return { kind: 'ok', url }; // url MUST be https:};The URL must be https:. The SDK validates it and collapses any javascript:, http:, or data: scheme to the unavailable state, so a bad URL can never reach the QR image or the clipboard. The hook is re-callable: the panel offers a “refresh link” affordance, and the overlay caps the number of re-mints per open so a stuck flow cannot churn links.
Your backend owns the mint because the browser token (bt_*) cannot mint a handoff link. The mint route accepts only a server-held session credential, so this hook is meaningful only on a host that runs a mint endpoint.
Open the overlay from a custom IDV renderer
Section titled “Open the overlay from a custom IDV renderer”If you build your own IDV renderer on the capture core (see Advanced capture), open the overlay with openCrossDeviceOverlay:
import { openCrossDeviceOverlay, preloadCrossDeviceChunk,} from '@checktiv/sdk-web/idv/cross-device';
// Warm the lazy chunk when the IDV step becomes active so the overlay opens instantly.preloadCrossDeviceChunk();
const overlay = openCrossDeviceOverlay({ target: containerElement, // the module's root container onOpenCrossDevice, // your mint hook from above copy: crossDeviceCopy, // host-injected strings; see below isMobile: false, // true suppresses the QR on a phone emit: (event) => onEvent(event), // forward to your event handler onClose: () => closeOverlay(), // the applicant pressed back});The returned handle has two methods:
setCompleting()- mark the session as completing. The panel keeps the QR mounted with a waiting label instead of disappearing while your page reloads.destroy()- tear the overlay down. Idempotent.
The overlay emits two token-free events (neither carries the URL, the link, or any code):
| Event type | Meaning |
|---|---|
checktiv.idv.cross_device_opened |
The mint hook returned ok and the overlay is shown. |
checktiv.idv.cross_device_unavailable |
The mint hook returned unavailable, or URL validation failed. |
Completion is the host’s job
Section titled “Completion is the host’s job”There is no completion event. When the phone finishes, your backend advances the session; your host detects that by polling your own status endpoint and calls window.location.reload() exactly once when it reports the step advanced. Call setCompleting() on the handle just before you reload so the overlay shows a completing state rather than vanishing abruptly. The SDK never writes window.location.
Mount the panel directly
Section titled “Mount the panel directly”If you only need the panel itself (for example, on a screen you fully control), mount it with mountCrossDevice:
import { mountCrossDevice } from '@checktiv/sdk-web/cross-device';
const handle = mountCrossDevice(containerElement, { url: 'https://verify.us.checktiv.com/handoff/...', // the https: link you minted copy: crossDeviceCopy, isMobile: false,});
// Later, re-render with new props (for example a refreshed url), or tear down:handle.update({ url: newUrl, copy: crossDeviceCopy, isMobile: false });handle.destroy();Always render the panel through mountCrossDevice (never render its internal component yourself): the chunk owns its own rendering so its interactive controls work correctly.
Copy is injected
Section titled “Copy is injected”Every user-facing string comes from a copy map your host supplies, resolved to the applicant’s locale. All keys are optional except unavailableMessage, which is required so the unavailable state is never blank. Omitting an optional key hides that affordance rather than showing a placeholder.
Related pages
Section titled “Related pages”- Modules overview - the full public surface
- Advanced capture - build a custom IDV renderer that opens this overlay
- Error reference - the errors whose recovery is
cross_device - Quickstart - the managed integration path