Expo / React Native SDK
Two ways to put the flow on a phone, sharing one implementation:
- Embed the SDK (
@baqshi/kyc-expo, this page) in your own Expo app — users never leave your product. - The BAQSHI Verify app (
apps/verifyin the repo) — the standalone store app for users arriving via desktop QR or a link. It’s built on this same SDK; if your product doesn’t want to embed capture, just show thehandoff_urland either the installed app or the hosted web page takes over.
@baqshi/kyc-expo is a small typed client for Expo apps. It holds only a handoff
token — never an API key — so it can ship safely inside any app bundle.
npm install @baqshi/kyc-expo expo-cameraGetting the token into the app
Section titled “Getting the token into the app”Three common routes:
- Your backend response — app asks your backend to start KYC; backend calls
POST /v1/verificationsand returns thehandoff_token. - Deep link — your backend sends the
handoff_urlby email/SMS; your app is registered for the link. - QR scan — desktop shows the QR; your app scans it:
import { BaqshiKyc } from "@baqshi/kyc-expo";
const token = BaqshiKyc.tokenFromUrl(scannedUrl); // null if not a handoff URLThe full flow
Section titled “The full flow”import { BaqshiKyc } from "@baqshi/kyc-expo";
const kyc = new BaqshiKyc({ baseUrl: "https://kyc-server-production.up.railway.app" });const session = kyc.session(token);
// 1. Where are we? (safe to call any time — resume after app restarts)const { next_step } = await session.status(); // "upload_document" | "upload_selfie" | …
// 2. Document photo (expo-camera or expo-image-picker uri)await session.uploadDocument(docPhoto.uri, "front");
// 3. Liveness — the SDK fetches the pose challenge and drives it;// you supply the camera and the UI prompt:await session.runLivenessFlow(async (pose, index, prompt) => { setPromptText(prompt); // "Turn your head to your left" const photo = await cameraRef.current.takePictureAsync({ quality: 0.9 }); return photo.uri;});
// 4. Wait for the verdict (polls; webhook hits your backend in parallel)const result = await session.waitForDecision({ timeoutMs: 120_000 });switch (result.status) { case "approved": /* celebrate */ break; case "rejected": /* show retry/support path */ break; case "manual_review": /* "we'll notify you shortly" */ break;}Capture quality guidance (put this in your UI)
Section titled “Capture quality guidance (put this in your UI)”The single biggest driver of manual-review rates is capture quality. Coach users:
- Document: flat surface, all four corners in frame, no glare from lights,
no fingers over text. The MRZ (the two lines of
<<<text on passports) must be fully readable. - Selfie: face the camera at eye level, even lighting, no hat/sunglasses, one person in frame. For the turn steps, a clear turn (~30–45°) — not a tilt.
Error handling
Section titled “Error handling”All failures throw BaqshiKycError with status and detail:
status | Typical cause | Handle by |
|---|---|---|
| 401 | Token expired (30 min) or revoked | Restart the flow via your backend |
| 409 | Step out of order / duplicate frame | Call status() and resume from next_step |
| 413 / 415 / 422 | Bad image (too large, wrong type, too small) | Re-capture |
| 429 | Rate limited | Back off; the limiter resets within a minute |