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

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

Your host supplies a mint hook that calls your own backend to produce a one-time link URL:

```ts
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

If you build your own IDV renderer on the capture core (see [Advanced capture](/developers/sdks/capture)), open the overlay with `openCrossDeviceOverlay`:

```ts
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

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

If you only need the panel itself (for example, on a screen you fully control), mount it with `mountCrossDevice`:

```ts
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

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

- [Modules overview](/developers/sdks/modules) - the full public surface
- [Advanced capture](/developers/sdks/capture) - build a custom IDV renderer that opens this overlay
- [Error reference](/developers/sdks/error-reference) - the errors whose recovery is `cross_device`
- [Quickstart](/developers/sdks/quickstart) - the managed integration path