Native onboarding components
5 min read
The native onboarding components run a KYC flow inline in your own UI: you collect identity data, custom data, and identity documents with your own screens instead of handing off to the hosted onboarding flow. The SDK owns the API calls (auth, vaulting, upload, processing); you own the screens.
id.*), custom data, and documents. KYB (business data and beneficial owners) is not supported natively. initialize rejects a KYB playbook up front, because the collect_business_data and create_business_onboarding requirements can't be fulfilled inline and vault has no business.* fields. Use the hosted flow for KYB playbooks.@onefootprint/footprint-expo 3.7.0 or higher. Native document capture also requires the camera modules described in Collect documents.The flow is always the same: initialize → identify (if required) → read requirements → collect (vault data, capture documents, or both) → process.
Initialize
Start from an onboarding session token created by your backend. initialize returns { requiresAuth }; when it is true, run the OTP flow below before continuing.
typescript
Identify (OTP)
When initialize returns { requiresAuth: true }, send a one-time passcode and verify it. The onboarding session token already identifies the user, so call createChallenge with no arguments; the passcode goes to the method the playbook authenticates with.
typescript1const { challengeKind } = await footprint.createChallenge(); 2// challengeKind -> "sms" | "email" (where the passcode was sent) 3 4await footprint.verify("123456"); // in sandbox the code is always 000000 5// The user is now identified; continue with getRequirements / vault / captureDocument / process.
There is no resend. If a passcode isn't delivered, createChallenge throws rather than returning silently, and the only recovery is to call createChallenge again. If the token still needs a contact method collected, the user can't be identified inline: createChallenge throws, and you should fall back to the hosted flow.
Sandbox testing
For a sandbox playbook, pass sandboxOutcome to initialize to choose the fixture result. Live playbooks ignore it.
typescript1await footprint.initialize("obtok_...", { 2 sandboxOutcome: { id: "test1", overallOutcome: "pass", documentOutcome: "pass" }, 3});
iddistinguishes sandbox users. The SDK generates an alphanumeric one when omitted; a value you pass must be alphanumeric.overallOutcomeis the onboarding fixture result. Omitting it evaluates the playbook's rules, which can fail, rather than forcing a pass.documentOutcomeapplies only when the playbook collects a document; it's dropped otherwise.
Use the sandbox fixture contacts (+15555550100 / fp@example.com): no real SMS or email is sent, and the passcode is always 000000.
Read requirements
getRequirements() tells you what the playbook still needs: the pending requirement kinds, the concrete data fields, and any pending document config.
typescript1const req = await footprint.getRequirements(); 2// req.fields.missing -> DataIdentifiers still required (e.g. "id.first_name") 3// req.pendingKinds -> e.g. ["collect_data", "collect_document", "process"] 4// req.documentConfig -> present when a document is pending
Collect identity data
Build your own form for the missing id.* fields, then vault them:
typescript1await footprint.vault({ 2 "id.first_name": "Jane", 3 "id.last_name": "Doe", 4 "id.dob": "1990-01-01", 5 "id.address_line1": "123 Main St", 6});
Collect custom data
Custom fields work the same way, under the custom.* namespace:
typescript1await footprint.vault({ "custom.membership_id": "AB-12345" });
You can read previously vaulted values back with getVaultData. id.ssn9, id.ssn4, id.us_tax_id, and document.* are omitted because they require a step-up the SDK doesn't support:
typescript1const data = await footprint.getVaultData(["id.first_name", "custom.membership_id"]);
Collect documents
Native document and selfie capture comes from a separate camera module. Install both packages:
bash1npm install @onefootprint/footprint-expo-camera-module @onefootprint/footprint-native-camera-module
Add the config plugin to app.json; it injects the camera and photo-library permissions:
json1{ 2 "expo": { 3 "plugins": [ 4 [ 5 "@onefootprint/footprint-expo-camera-module", 6 { 7 "cameraPermission": "Take a photo of your ID", 8 "photoLibraryPermission": "Pick an ID photo from your library" 9 } 10 ] 11 ] 12 } 13}
Pass the camera module to initialize, then capture. If the playbook requires consent, submit it first. The text you submit is recorded as the disclosure the user agreed to, so pass the full consent language you displayed:
typescript1import { footprint } from "@onefootprint/footprint-expo"; 2import { captureDocument } from "@onefootprint/footprint-expo-camera-module"; 3 4await footprint.initialize("obtok_...", { cameraModule: { captureDocument } }); 5 6const docConfig = footprint.getDocumentConfig(); 7if (docConfig?.shouldCollectConsent) { 8 await footprint.submitConsent({ consentLanguageText: MY_CONSENT_TEXT, mlConsent: true }); 9} 10 11const result = await footprint.captureDocument({ kind: "passport", countryCode: "US" }); 12// result.uploadedSides -> e.g. ["front", "selfie"]; result.canceled -> user backed out
The camera module owns the capture screen and the per-side loop; the SDK owns the upload and processing and enforces the backend retry limit. You render the document-type and country picker.
Finalize
Once all requirements are met, process() finalizes the onboarding and returns a validation token to send to your backend:
typescript1const validationToken = await footprint.process();
process() throws if requirements remain that the native flow can't satisfy, such as passkey registration or bank linking. Fall back to the hosted flow for those.
Next steps
The native components expose the same surface across Expo, Swift, and Android, including native document capture (captureDocument and submitConsent), which each platform provides through an opt-in camera package. See the Swift and Android guides.