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.

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
1import { footprint } from "@onefootprint/footprint-expo";
2
3const result = await footprint.initialize("obtok_...");
4
5if (result.validationToken) {
6  // The session was already complete — send this token to your backend.
7} else if (result.requiresAuth) {
8  // The user still needs to identify — run createChallenge + verify (below).
9}

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.

typescript
1const { 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.

typescript
1await footprint.initialize("obtok_...", {
2  sandboxOutcome: { id: "test1", overallOutcome: "pass", documentOutcome: "pass" },
3});
  • id distinguishes sandbox users. The SDK generates an alphanumeric one when omitted; a value you pass must be alphanumeric.
  • overallOutcome is the onboarding fixture result. Omitting it evaluates the playbook's rules, which can fail, rather than forcing a pass.
  • documentOutcome applies 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.

typescript
1const 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:

typescript
1await 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:

typescript
1await 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:

typescript
1const 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:

bash
1npm 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:

json
1{
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:

typescript
1import { 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:

typescript
1const 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.