Embed @arkyc/widget

Bundle the verification flow into your own frontend and drive it from your own backend. Two pieces: your backend mints a one-time session token with the server SDK, and your frontend renders @arkyc/widget, which talks directly to the Arkyc Client API with that token. You get full control of layout overlay, inline, or fullscreen and you ship widget updates by upgrading the package.

Don’t want to bundle anything? Use the hosted launcher (@arkyc/sdk/browser) instead, it loads the hosted widget in an iframe.

Install

// Bundler
import { ArkycWidget } from '@arkyc/widget'

no-bundler.html
<!-- Standalone IIFE; exposes window.Arkyc -->
<script src="https://cdn.jsdelivr.net/npm/@arkyc/widget/standalone"></script>
<script>Arkyc.mount({ token, container: "#verify" })</script>

1 · Mint a session (your backend)

The widget never sees your secret key. On your server, create a session with the SDK and return only the clientToken to the browser.

server.ts
import { Arkyc } from '@arkyc/sdk'

const arkyc = new Arkyc({ secretKey: process.env.ARKYC_SECRET_KEY! })

// e.g. POST /verify/start on your backend
app.post('/verify/start', async (req, res) => {
  const { clientToken } = await arkyc.sessions.create({
    userReference: req.user.id,        // your id for the user
    workflowId: 'wf_...',              // optional: a custom workflow
  })
  res.json({ clientToken })            // hand this to the frontend
})

2 · Point the widget at the API (optional)

baseUrl is optional; it defaults to the hosted Arkyc API, so the hosted product is zero-config (the browser calls it cross-origin with the token; the API allows it via CORS). You only set it to self-host or proxy. It’s a base: the widget appends a fixed path per call (/session, /document/front, …) and adds no version prefix.

  • A relative path (e.g. /api/v1/client) → resolved against your page’s current origin. Same-origin, so no CORS; use this when your backend proxies the Client API under your own domain.
  • An absolute URL (e.g. https://api.example.com/api/v1/client) → used as-is. The API must allow your origin via CORS.

To route through your own backend, set baseUrl to a prefix you control and proxy each path to Arkyc’s /api/v1/client/* (see Endpoints). Self-hosting the whole stack? You can instead bake your API as the widget’s default by building it with ARKYC_API_URL. Serve your page over HTTPS; browsers only grant camera (and geolocation, for the address step) on a secure origin.

3 · Render the widget (your frontend)

Overlay: full-screen modal

ArkycWidget.open(options) mounts a dimmed overlay and returns a WidgetHandle. Pass fullscreen: true for edge-to-edge (no card max-size).

overlay.ts
import { ArkycWidget } from '@arkyc/widget'

const { clientToken } = await fetch('/verify/start', { method: 'POST' }).then((r) => r.json())

const handle = ArkycWidget.open({
  token: clientToken,   // required; baseUrl defaults to the hosted API
  fullscreen: true,     // optional: edge-to-edge
  onComplete: (r) => done(r.status),
  onError: (e) => showError(e),
  onClose: () => {},
})

Inline: mounted into a container

ArkycWidget.mount(options) renders into an element you provide, for a page-embedded flow instead of an overlay.

inline.ts
ArkycWidget.mount({
  token: clientToken,
  container: '#verify',   // string selector or an HTMLElement
  onComplete: (r) => done(r.status),
})

The widget takes ownership of the container: it sizes the card to the host (not the viewport) and gives the element its own stacking context above sibling content, so it renders reliably even if the container is otherwise unstyled or shares the page with a full-screen app root. It only fills gaps (a container that sets its own position or z-index keeps them), so you stay in control of placement. Put the container where you want the flow to appear and size it as you like; the widget does the rest.

Hosted page: redirect & cross-device

ArkycWidget.hosted() reads ?token= (and an optional ?baseUrl=) from the URL and runs the overlay flow, posting results to the opener. This is what a first-party /verify page (and the desktop→phone hand-off) uses.

verify-page.ts
// On your https://app.example.com/verify page:
ArkycWidget.hosted()

Endpoints to proxy

If you route the Client API through your own backend, set baseUrl to a prefix you control and forward each of these to Arkyc’s /api/v1/client/*, pass the X-Client-Token header and the request body through unchanged.

Method{baseUrl} + …Proxy to (Arkyc)
GET/session/api/v1/client/session
POST/document/front/api/v1/client/document/front
POST/document/back/api/v1/client/document/back
POST/liveness/api/v1/client/liveness
POST/address/api/v1/client/address
POST/complete/api/v1/client/complete
POST/realtime/auth/api/v1/client/realtime/auth

/realtime/auth is only called when realtime runs over an authed transport (pusher/firebase). Against Arkyc directly (no proxy), just set baseUrl to …/api/v1/client and skip this.

Options

open and mount share BaseWidgetOptions (mount adds container):

OptionTypeNotes
tokenstringRequired. The session’s one-time client token from arkyc.sessions.create.
baseUrlstringOptional, defaults to the hosted Arkyc API. Client API base; the widget appends /session, … (no version prefix). Relative ⇒ current origin (no CORS); absolute ⇒ as-is (needs CORS).
containerstring | HTMLElementmount only where to render inline.
fullscreenbooleanopen only edge-to-edge, no card max-size or backdrop gap.
handoffbooleanCross-device desktop→phone hand-off (QR), on by default. Uses a first-party hosted page; you host nothing. Set false to disable.
brandingProjectBranding | nullColors, logo, radius, theme. Sourced from project config by default.
onComplete / onError / onClose(…) => voidTerminal result, session error/expiry, and overlay teardown.
onEvent(event: WidgetEvent) => voidLive firehose of session.transition + lifecycle events.

The handle

open and mount return a WidgetHandle: close() to tear it down, and on(name, listener) to subscribe to a single event (returns an unsubscribe function). name is a known event name (session.transition · complete · error · close) and the listener’s data is typed from it.

handle.ts
const handle = ArkycWidget.open({ token: clientToken })

const off = handle.on('session.transition', (data) => updateProgress(data))
// later…
off()
handle.close()

Result & events

onComplete receives a WidgetResult whose status is approved · requires_review · rejected (or processing / expired / cancelled). Treat it as a UX signal; use webhooks as your source of truth.

onError receives a WidgetApiError with the HTTP status and a stable error key; branch on it, e.g. mint a fresh token when err.error === 'session_expired'. It fires the moment the flow fails (including an expired session or token), so you can react without waiting for the user to dismiss the screen. See Error codes.

Theming

The widget themes itself from your project branding (primary color, logo, corner radius) configured in the dashboard, or pass branding explicitly. No CSS required.

Capture-only

Run a workflow with OCR/decisioning off to use Arkyc purely for high-quality document + selfie capture, then forward the artifacts (signed URLs on the session) to your own KYC provider, see the server SDK.