# Introduction to Footprint ## Add Footprint to your app Footprint unifies onboarding, identity verification, bank linking, and data security. Onboard your customers without worrying about storing their sensitive data afterwards. ### Quick start Learn how to get Footprint up and running in your project. Quick and easy --- # The Integration Guide Footprint makes it easy to onboard your users, whether you need KYC, KYB, identity document verification, or document collection. Security, compliance, and risk/fraud prevention come bundled in. This is the definitive guide to integrating Footprint into your product, including the advanced options and features that let you fully customize and own your flow. # Core concepts ### Vault Each entity in Footprint is backed by a secure data **Vault** that stores identity, financial, documents, and custom data attributes. When a user onboards via a Footprint flow all data collected is automatically stored in the user’s vault. Vaults store both structured and unstructured data, and keep track of each change to every attribute with a versioned history. Footprint's vaulting aims to be flexible while providing base-level validation logic for structured vault data such as Identity or PCI data. All attribute values are referenced by specific **data identifiers** (such as `id.ssn9` references the full SSN of a user). Vault data fields are a core part of the Footprint platform: they support granular role-based access controls (RBACs) for data access and can be used to transmit data securely (via the Vault Proxy) to third-party destinations. Read more about all the [vault data fields here](/articles/vault/fields). ### The "fp\_id" Also known as the “Footprint ID”. This is the unique, per-user identifier for an entity inside of Footprint. It appears in API request, responses, and on the dashboard to uniquely identify an entity. Similar to `fp_id`, an `fp_bid` is the unique identifier for a business entity. ### The "external\_id" This is an identifier that **you provide** for a user or business. This provides a mechanism which you can map entities in your system to Footprint without storing/knowing the Footprint ID for that entity. We strongly recommend maintaining a one-to-one mapping from users in your own database to users in Footprint. One easy way to guarantee this is to provide an external ID when creating users. ### Playbook A Playbook defines the end-to-end onboarding flow powered by Footprint: (a) what information needs to be collected, (b) what verification checks need to run, and (c) the rules that define decisioning based on the verification checks. A `playbook_key` is the unique, publishable identifier for a playbook and appears in API requests, responses, and in the dashboard. ### Onboarding An onboarding represents a user/business onboarding onto a Playbook. Each time a user/business goes through a playbook, it creates a new onboarding. # Integration overview A user's journey with Footprint starts via the POST /onboardings API. It runs one of your playbooks against a user or business and returns the result. How much of the flow runs headlessly depends on how much data you've already collected: 1. **Fully headless:** if your application already collects all the data the playbook needs, vault that data and run the onboarding entirely server-side, with no Footprint UI shown at all. The decision comes back synchronously inline or asynchronously via webhooks, depending on your settings. 2. **Interactive:** if there is still information to collect from the user, the API returns a `continue_onboarding` token and link. Hand these to your frontend so the user can finish the remaining steps in one of two ways: * **Hosted:** a web page hosted by Footprint that includes the rest of the onboarding flow. Send the `link` to your user via email, SMS, or a button in your app. * **Embedded:** use one of our many SDKs across web, iOS, and Android to embed the rest of the flow inside your product, launched with the `token`. ### Customization The interactive flow supports customization to varying degrees: 1. **Hosted:** Coming soon, use our appearance editor in the dashboard to control every element just like in the embedded flow. 2. **Embedded:** use our SDK to fully customize the look and feel of each element including fonts, colors, borders, and much more. We support over 100 attributes of customization. See here for full details: [https://docs.onefootprint.com/articles/integrate/customization](/articles/integrate/customization). ### Identity documents & document collection One important caveat: Footprint handles the complexity of document collection and verification. Document collection is considered a singular component which includes everything from: device handoff (leveraging a mobile device when starting the flow on desktop), automatic capture and liveness, document classification, and selfie capture. The customization options extend to the full document collection and scanning experience. # The end-to-end integration This guide covers all the most common steps to getting Footprint fully up and running in just a few minutes. ## Step 1: Get your secret API key Go to [the Footprint dashboard](https://dashboard.onefootprint.com/api-keys) and **create a secret key**. Store this key somewhere safe. ## Step 2: Create a playbook Go to [the Footprint dashboard](https://dashboard.onefootprint.com/playbooks) and create a Playbook. Playbooks define the onboarding: (a) what information needs to be collected, (b) what verification checks need to run, and (c) the rules that define decisioning based on the verification checks. ## Step 3: Create the user and vault their data Create the user with POST /users (or a business with POST /businesses) and write whatever data you have already collected: ```bash curl -X POST https://api.onefootprint.com/users \ -u : \ -d '{ "id.first_name": "Jane", "id.last_name": "Doe", "id.dob": "1990-01-01", "id.ssn9": "123-45-6789", "id.address_line1": "1 Main St", "id.city": "San Francisco", "id.state": "CA", "id.zip": "94105", "id.country": "US" }' # -> { "id": "fp_id_K0q6Eh6Rr3WOOfFBLPiHsr" } ``` See [Vault fields](/articles/vault/fields) for the full list of fields you can vault, and [Migrating user data](/articles/integrate/migrate-existing-data) to bring over data you already hold. If you expect the user to provide most of their information through the interactive flow, it's fine to vault only what you have, or nothing at all. To keep a one-to-one mapping with your own records, pass your own identifier as the `x-external-id` header when creating the user. You can then reference the user by `external_id` in later API calls instead of storing the `fp_id`. ## Step 4: Run the playbook Call POST /onboardings with the entity and the playbook key. Set `synchronous_timeout_secs` (max 30) to wait for the decision and receive it inline: ```bash curl -X POST https://api.onefootprint.com/onboardings \ -u : \ -d '{ "fp_id": "fp_id_K0q6Eh6Rr3WOOfFBLPiHsr", "key": "", "synchronous_timeout_secs": 30 }' ``` ```json { "id": "ob_SRFT2a1mN7DAWJ0VPXkiqK", "status": "pass", "requires_manual_review": false, "continue_onboarding": null, "error_message": null } ``` If you omit `synchronous_timeout_secs`, the call returns immediately with `status: "pending"` and the run continues in the background; the final decision arrives via webhook (step 6). You'll generally only run a playbook asynchronously when you have already collected all the information it needs. This mode is useful for latent operations, like running an AI agent, that may not finish within the synchronous timeout. For a business, put the `fp_bid` in the `fp_id` field; there is no separate `fp_bid` field. The `key` also accepts a version tag (`pb_live_xxx:v3`) to run a specific playbook version, though we recommend deploying the version you want from the dashboard rather than pinning it in code. A KYB playbook that also verifies its beneficial owners can't run here: the owners have to complete their own KYC, which this API can't prompt for. Run those playbooks through an onboarding session instead. ### Step 4a: Optional configuration | Attribute | Description | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `external_id` | Reference the user or business by your own identifier instead of `fp_id`. This is the same `external_id` set when the entity was created. Cannot be provided alongside `fp_id`. | | `onboarding_external_id` | Use to control onboarding idempotency and to associate an onboarding with an event in your application (for example, an account application ID). If an onboarding with this ID already exists on the playbook, its result is reused and returned. If not, a new onboarding is created and the entity re-onboards. | | `synchronous_timeout_secs` | Wait up to this many seconds (maximum 30) for the run to finish and return the decision inline. Omit to run asynchronously. | | `prerequisite_data` | If your playbook requires additional information from your backend, configure a prerequisite node on the playbook and pass that data here. Only accepted when the playbook has a prerequisite node. | By default, an entity may only onboard onto a playbook one time. This prevents you from incurring accidental charges for repeat onboardings. Running `POST /onboardings` again without a new `onboarding_external_id` returns a `409` with the error "Already onboarded onto playbook". See [Run a playbook](/articles/integrate/onboardings) for the full reference on `POST /onboardings`, including reonboarding patterns, KYB, and passing onboarding data. ## Step 5: Handle the decision Your playbook defines a set of rules used to evaluate the entity and to decide whether it should be flagged for manual review. Use the `status` and `requires_manual_review` fields in the response to decide whether or not to onboard this user to your product. | **STATUS** | **WHAT DOES THIS MEAN?** | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `pass` / `fail` / `none` | An action node in your playbook executed and set the user's status. Conventionally, these are the output of rules that you define. | | `pending` | Either you ran asynchronously, or the playbook took longer than `synchronous_timeout_secs` to execute. Certain steps of a playbook, like AI agents or verification checks, may take longer. The final decision will be delivered via [webhooks](/articles/integrate/webhooks). | | `incomplete` | The onboarding could not finish headlessly because more information is needed from the user. Use `continue_onboarding` to let them finish (see below). | | `error` | The run failed due to logic configured on your playbook. See `error_message`. | | **REVIEW** | **WHAT DOES THIS MEAN?** | | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | False | Your playbook's rules have made a decision automatically. | | True | Your playbook's rules have requested that this user is reviewed manually before onboarding OR the user’s previous onboarding status caused the review flag to remain enabled. | If Footprint still needs information from the user (for example, your playbook collects a field you didn't vault, or requires an identity document), the status is `incomplete` and the response carries a `continue_onboarding` object: ```json { "id": "ob_SRFT2a1mN7DAWJ0VPXkiqK", "status": "incomplete", "requires_manual_review": false, "error_message": null, "continue_onboarding": { "token": "obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH", "link": "https://verify.onefootprint.com/?type=user#obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH", "expires_at": "2025-05-08T12:00-07:00" } } ``` The token resumes this exact onboarding: the user picks up where the headless run stopped, and the completed run keeps the same `id`. Treat the token as a secret, and use it before `expires_at` (12 hours). Hand it to your frontend to let the user finish the remaining steps, using either of the two launch options below. ### Step 5a: Finish via Hosted If using the **Hosted launch option**, extract the `link` and deliver it to your end user via email, SMS, or through a button in your app. ### Step 5b: Finish via Embedded If using one of our embedded SDKs, extract the `token` and use that to launch Footprint. Below find some examples of different SDKs. **Install:** Web (JS/TS/React/Vue/Angular): `npm install @onefootprint/footprint-js`. iOS: via [Swift Package Manager](/articles/sdks/swift-introduction#installation-swift-package-manager) or [Cocoa Pods](/articles/sdks/swift-introduction#installation-cocoa-pods). Android: see the [installation instructions](/articles/sdks/android-introduction#installation). ```tsx filename="Web (React)" import "@onefootprint/footprint-js/dist/footprint-js.css"; import { onboarding } from "@onefootprint/footprint-js"; const App = () => { const launch = () => { onboarding.initialize({ // replace with `continue_onboarding.token` from the api response onboardingSessionToken: "obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH", onComplete: (validationToken) => { console.log("completed", validationToken); }, }); }; return ; }; ``` ```swift filename="iOS (Swift UI)" import SwiftUI import Footprint struct ContentView: View { var body: some View { VStack { Button("Verify Identity") { Task { do { // replace with `continue_onboarding.token` from the api response try await Footprint.shared.initialize(authToken: "obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH") try await FootprintHosted.shared.launchHosted( onComplete: { validationToken in print("Handoff completed successfully with token: \(validationToken)") }, onCancel: { print("Handoff was canceled by the user") }, onError: { error in print("Error occurred during handoff: \(error)") } ) } catch { print("Error: \(error)") } } } } } } ``` ```kotlin filename="Android (Kotlin)" class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Column { val coroutineScope = rememberCoroutineScope() Button(onClick = { coroutineScope.launch { try { Footprint.initialize( // replace with `continue_onboarding.token` from the api response authToken = "obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH", context = this@MainActivity ) FootprintHosted.launchHosted( context = this@MainActivity, onComplete = { token: String -> println("VerificationResult: the flow has completed. The validation token is $token") }, onCancel = { println("VerificationResult: the flow was canceled") }, onError = { error -> println("Footprint error occurred: ${error}") } ) } catch (e: FootprintException) { println("Error initializing Footprint SDK: ${e.message}") } } }) { Text("Verify identity") } } } } } ``` When the user finishes the remaining steps, the SDK invokes the `onComplete` handler with a `validation_token`. To get the state of the onboarding immediately, validate that token (step 5c). The final decision is also delivered via webhook (step 6), and you can fetch it at any time with GET /users/{fp_id}/onboardings/{id} using the onboarding `id` from step 4. ### Step 5c: Process the validation\_token Note: this only applies if you are using the **Embedded** SDK. If using **Hosted**, skip to step 6. At the end of the onboarding flow, the SDK invokes an `onComplete` completion handler that passes a `validation_token` to your code. Send the `validation_token` to your backend and validate it using the POST /onboarding/session/validate API. This is the recommended way to get the state of the onboarding immediately after the user finishes the flow. ```bash curl -X POST https://api.onefootprint.com/onboarding/session/validate \ -u : \ -d '{"validation_token": ""}' ``` Note: This is a short-lived token that represents a completed onboarding. It can be exchanged for the result of the onboarding (the decision in the case of KYC/KYB) along with the `fp_id`/`fp_bid` of the corresponding entity that was created or reused. The response will look like: ```json { "user": { "fp_id": "fp_id_GSxJr68GAf5jUT3pdL9ndjf7TLkA3GCX", "onboarding_id": "ob_SRFT2a1mN7DAWJ0VPXkiqK", "playbook_key": "pb_test_VMooXd04EUlnu3AvMYKjMW", "requires_manual_review": false, "status": "pass" }, ... } ``` ## Step 6: Listen to webhooks Every onboarding's decision is delivered via webhook when it reaches a terminal status. For asynchronous runs or synchronous runs that returned `pending`, webhooks are how you receive the final result. ### Step 6a: Subscribe a webhook endpoint To enable webhook events, you need to register webhook endpoints in the [Footprint dashboard](https://dashboard.onefootprint.com/webhooks). After you register them, Footprint can push real-time event data to your application's webhook endpoint when events happen in Footprint. To get started, see the [guide to consuming webhooks](https://docs.svix.com/receiving/introduction). > We recommend that you secure your integration by always verifying that all > webhook requests are generated by Footprint. Please see our guide on > [securely verifying webhook signature](https://docs.svix.com/receiving/verifying-payloads/how)s. ### Step 6b: Recommended events For the core Footprint integration, we recommend listening to the following three event types. * **Onboarding completed** We recommend subscribing to the footprint.onboarding.completed event, which will fire when an onboarding has reached a terminal status. In most cases, a synchronous `POST /onboardings` call will return a terminal `pass`, `fail`, or `none` [decision](#the-end-to-end-integration-step-5-handle-the-decision) inline. This webhook event delivers the final decision when you run an onboarding asynchronously or when identity verification vendors are taking longer to verify a user. * **Manual review** Employees at your company may change a user's status while manually reviewing a user in the Footprint dashboard. This will fire the footprint.user.manual\_review event. Upon receiving this webhook, we recommend consulting the GET /users/{fp_id} API for information on the user's new manual status. * **Watchlist checks** If your playbook has continuous monitoring enabled, Footprint will regularly check if any of your users are found on AML watchlists. Updates on watchlist checks are sent using the footprint.watchlist\_check.completed event. # Appendix ## Advanced integration options Below we’ve high-lighted some additional, more advanced options available to you as you integrate Footprint into your product. All of these are optional and using these methods typically indicate you are using Footprint in less common ways. Note some of these APIs may be gated, so please reach out to us if an API you need is not enabled on your account. ## Use the API to fetching Onboardings, PII, Decisions, Risk signals, Documents, and more In more advanced integration cases, you may want to process more detailed verification data and results from Footprint (instead of only viewing it in the dashboard). This can be useful if you are embedding results from Footprint in your application (i.e. a seller marketplace app who needs to pass risk signals to the seller who will verify a buyer for large transactions). ### The Onboarding object Every time a user or business onboards onto a playbook, an “onboarding” is generated, and each onboarding has a unique ID often referenced as `onboarding_id`. These IDs can be used to fetch risk signals, decisions, documents and data collected, and more at a specific “onboarding”. An onboarding ID lets you track the user as they update and modify data over time. The two main places where onboarding IDs will be provided to you are: 1. The `id` field of the POST /onboardings response 2. A Webhook when an onboarding is completed. You can list onboardings for a user/business by using the GET /users/{fp_id}/onboardings and GET /businesses/{fp_bid}/onboardings APIs. ### Fetch risk signals, and list collected data and documents by onboarding ID Once you have a particular `onboarding_id` you can use the GET /users/{fp_id}/onboardings/{onboarding_id}/risk\_signals and GET GET /users/{fp_id}/documents?onboarding\_id=ob\_xyz.. APIs to fetch onboarding specific risk signals and captured documents. ### Fetch decisions Given a user or a business, you can fetch all the decisions (including manual review decisions that occurred with a human in the loop). Use the [list all decisions](/api-reference#get-users-fp-id-decisions) API (for users/businesses). The response will look like following: ```json { "data": [ { "kind": "playbook_run", "playbook_key": "pb_live_fZvYlX3JpanlQ3MAwE45g0", "status": "fail", "timestamp": "2022-01-04T12:00-07:00" }, { "kind": "manual", "status": "pass", "timestamp": "2022-01-04T12:00-07:00" } ], "meta": { "next_page": 2 } } ``` ### Decrypt PII/documents from the vault at a specific onboarding Footprint’s information systems are built around highly-secured vaulting infrastructure with granular access controls. Use the [decrypt API](/api-reference#post-users-fp-id-vault-decrypt) to access plaintext sensitive user/business data. ```bash curl -X POST https://api.onefootprint.com/users/{fp_id}/vault/decrypt \ -u : \ -d '{ "fields": [ "id.ssn9", "id.last_name", "document.passport.front.image", "document.passport.dob" ], "reason": "compliance", "at_onboarding_id": "ob_id_xyz..." }' ``` You may specify the `at_onboarding_id` field to decrypt a user's historical information at the time of the provided onboarding. ## Linking users to businesses via API If you are using KYB playbooks but not verifying beneficial owners (i.e. you are not using Footprint’s feature of verifying business owners together with the business), you can still take advantage of linking users to their businesses (and vice-versa) so that you can connect entities in the API and see the connections in the Footprint dashboard. Use the [link a business owner](/api-reference#post-businesses-fp-bid-owners) API after onboarding both a business and the user(s). --- # Introduction # Review Platform Footprint's Review Platform is a comprehensive human and AI agent review management system designed to help your organization efficiently handle reviews across your onboarding processes. Whether you're reviewing identity documents, assessing risk, or verifying business information, the platform ensures that the right reviews reach the right team members at the right time. Built around a sophisticated prioritization engine and fair work distribution system, the Review Platform helps your operations team resolve more reviews in less time while maintaining high quality standards and preventing reviews from sitting unaddressed in queues. ## Key Benefits **Maintain real-time operational visibility** Know exactly where your team stands at any moment with live queue metrics, SLA proximity tracking, and resource allocation insights. Unlike retrospective analytics that tell you what happened yesterday, real-time dashboards help you make staffing decisions now—before reviews breach their SLAs. **Ensure fair work distribution** A configurable pull-based assignment system prevents cherry-picking and ensures equitable workload distribution across your review team. Agents request their next review rather than being automatically assigned work, which eliminates the common problem of easier cases getting claimed first while complex reviews sit in the queue. This approach also gives you better visibility into actual work-in-progress versus theoretical capacity. **Automatically prioritize with multi-factor intelligence** Reviews dynamically escalate in priority based on multiple factors simultaneously: how close they are to SLA breach, their base priority level, and when they were created. This multi-factor approach reflects real operational needs better than simple sorting by creation date or SLA alone. You can customize escalation logic to match your operational needs—whether you need steady escalation or rapid prioritization near deadlines. **Route reviews to specialized teams** Multiple queues and review kinds let you segment work by complexity, risk level, or required expertise. High-risk cases can be routed to your fraud specialists, while routine verifications go to your general review team, ensuring the right expertise is applied to each case. **Track performance and maintain compliance** Complete audit trails track every review from creation to completion, while performance metrics help you monitor agent productivity, queue health, and SLA adherence. This visibility helps you optimize team performance and meet compliance requirements. ## What's Next * [Core Concepts](/articles/guides/manual-review-platform-core-concepts) - Learn about Review Kinds, Queues, and Prioritization * [Review Agents and Teams](/articles/guides/manual-review-platform-review-agents-teams) - Understand agent management and team organization * [Reviewer Workflow](/articles/guides/manual-review-platform-reviewer-workflow) - Understand how agents work with the system * [Operations Management](/articles/guides/manual-review-platform-operations-management) - Real-time monitoring and management * [Integration with Playbooks](/articles/guides/manual-review-platform-integration-workflows) - Learn how the platform integrates with your existing playbooks * [Getting Started](/articles/guides/manual-review-platform-getting-started) - Setup and configuration guide --- # Core Concepts ### Review Kinds Review Kinds define the different types of reviews your team performs. Think of them as templates that specify what type of review is needed and how urgently it should be completed. Each Review Kind includes: * **Name & Description**: Clear identification and instructions that help reviewers understand what they're evaluating * **Base Priority** (1-10 scale): Starting priority level that determines the review's initial importance relative to other review types * **SLA Timeline**: Hours from creation until the review's due date * **Escalation Parameters**: Controls how the review's priority increases as it approaches its deadline By configuring different Review Kinds, you can ensure that time-sensitive fraud investigations receive appropriate urgency compared to routine document checks. *** ### Review Queues Review Queues organize reviews for different teams or workflows. While Review Kinds define *what* needs to be reviewed, Queues determine *who* reviews it and *how* work is prioritized within that team. **Key capabilities of Review Queues:** * **Team Segmentation**: Create separate queues for different departments (e.g., Compliance Team, Fraud Specialists, L1 Reviewers) * **Priority Configuration**: Choose how reviews are ordered within each queue using different sorting strategies * **Review Kind Binding**: Specify which types of reviews should flow into each queue * **Review Agent Assignment**: Control which team members have access to pull reviews from each queue **Example queue structure:** * **General Queue**: Handles standard document verifications and low-risk reviews for your L1 team * **Escalations Queue**: Receives cases that AI agents couldn't resolve or that reviewers manually escalated * **Specialist Compliance Queue**: Routes high-risk, complex cases to your most experienced reviewers This segmentation ensures that your specialized resources focus on cases that truly need their expertise, while routine reviews move efficiently through your general team. *** ### Queue Prioritization Strategies Understanding how reviews are ordered within each queue is critical for operational efficiency. The Review Platform offers four distinct sorting strategies, each designed for different operational needs. ### SLA Sort Orders reviews by earliest due date first. This strategy ensures you never miss a deadline but doesn't account for the relative importance of different review types. **Best for**: Teams where all review types have similar importance and deadline compliance is the primary concern. ### Relative Priority Sort Orders reviews by highest base priority first, regardless of when they were created or when they're due. **Best for**: Teams handling both routine and critical reviews where certain types (like fraud investigations) should always take precedence. ### Created Sort Orders reviews by most recent first, functioning as a simple FIFO (first-in, first-out) queue. **Best for**: Teams with consistent review types where age alone is the best indicator of urgency. ### Hybrid Prioritization (Recommended) Dynamically calculates priority using multiple factors simultaneously: base priority level, proximity to SLA deadline, and creation time. This is the most sophisticated option and reflects real operational complexity. **How Hybrid Prioritization works:** The system continuously recalculates each review's effective priority using this formula: ``` Priority = Base Priority × (1 + (Max Multiplier - 1) × Progress^SLA Ramp Factor) ``` **Parameters:** * **Base Priority**: The review kind's starting priority (1-10) * **Max Multiplier**: How much priority can increase (e.g., 2x means priority can double) * **Progress**: Percentage of time elapsed toward SLA deadline (0-100%) * **SLA Ramp Factor**: Controls the escalation speed **SLA Ramp Factor options:** * **Linear (1.0)**: Steady, predictable escalation over time. A review's priority increases proportionally as it approaches its deadline. * **Exponential (>1.0)**: Slow escalation early, then rapid increase near deadline. Use this when you want to give newer reviews time to be addressed before older reviews become urgent. * **Logarithmic (\<1.0)**: Fast escalation early, then gradual increase. Use this when you want older reviews to quickly gain priority. **Why Hybrid Prioritization matters:** Consider two reviews in your queue with identical basic metrics: **Review A: High-Value Transaction** * Base priority: 5 * SLA: 4 hours * Created: 3.5 hours ago * $50,000 wire transfer flagged for manual review * Customer is a VIP with urgent business needs **Review B: Standard Transaction** * Base priority: 5 * SLA: 4 hours * Created: 3.5 hours ago * $500 credit card transaction flagged for manual review * Routine verification needed ### Hybrid Prioritization Advantage With **simple sorting strategies**, both reviews would be treated identically, where you'd ideally want to review the high value transaction first: * **SLA sorting**: Both reviews have the same time due * **Review creation sorting**: Both reviews have the same creation time * **Relative priority sorting**: Both reviews have the same relative priority With **Hybrid Prioritization**, the system can incorporate additional contextual factors to make sure important reviews get done first *** ## Reviewers and Teams Overview The Review Platform supports both human reviewers and AI agents working together to handle your onboarding workflows efficiently. ### Human Review Agents **Review Agents** are your team members who perform manual reviews. The platform provides comprehensive management capabilities: * **Queue Access Control**: Assign agents to specific queues based on their expertise and specialization * **Performance Tracking**: Monitor individual productivity, review quality, and throughput metrics * **Daily Quotas**: Set realistic targets to help manage workload and expectations * **Permission Levels**: Control what actions agents can perform (review, escalate, approve, etc.) ### Review Teams **Review Teams** organize agents for better management and operational efficiency: * **Specialization Groups**: Create teams like Fraud Specialists, Document Reviewers, or Compliance Teams * **Performance Comparison**: Track and compare metrics across different operational groups * **Escalation Management**: Define clear paths for complex cases that need expert attention * **Workload Distribution**: Ensure equitable work distribution across team members ### AI Agents Integration The platform seamlessly integrates with AI agents that can handle routine verifications and automated decisioning: * **Automated Processing**: AI agents can resolve straightforward cases without human intervention * **Escalation Routing**: Cases that AI agents cannot resolve automatically flow to appropriate human review queues * **Hybrid Workflows**: Combine AI efficiency with human expertise for optimal coverage * **Quality Assurance**: Human reviewers can validate AI decisions and provide feedback for continuous improvement This multi-agent approach ensures that simple cases are processed quickly by AI while complex or uncertain cases receive the attention of your specialized human reviewers. --- # Human Agent Workflow ### The Pull-Based Assignment Model The Review Platform uses a pull-based assignment model rather than automatically pushing work to agents. This design choice addresses several common operational challenges that occur with automatic assignment systems. **Why pull-based assignment matters:** **Prevents cherry-picking**: When reviewers can see and select from a list of available reviews, they naturally gravitate toward easier or more familiar cases, leaving complex reviews sitting in the queue. With pull-based assignment, the system assigns the highest-priority unassigned review automatically when an agent requests work. **Improves work-in-progress visibility**: Push-based systems often assign work based on theoretical capacity ("Agent has fewer than 10 cases assigned"), but this doesn't reflect actual work-in-progress. An agent might be deep in research on one complex case while having 5 others "assigned" but not actively worked. Pull-based assignment ensures agents only receive new work when they explicitly request it. **Maintains fairness**: The system ensures equitable distribution because no agent can claim easier work ahead of others. Everyone receives the next highest-priority review in their queue when they're ready. **How it works:** 1. **Request Next Review**: Reviewer clicks "Pull Next Review" button in their queue 2. **Automatic Assignment**: System assigns the **highest-priority** unassigned review from that queue 3. **Complete or Unassign**: Agent must finish the review or release it back to the queue before requesting another This workflow ensures that urgent reviews don't sit unassigned because agents are working on easier cases or have work assigned but not actively in progress. ### Self-Assignment for Special Cases While pull-based assignment handles the majority of reviews, the platform also supports self-assignment for specific scenarios: * **Follow-up Reviews**: An agent who reviewed a customer previously can self-assign related reviews * **Specialized Expertise**: Subject matter experts can claim reviews that require specific knowledge * **Escalated Cases**: Supervisors can self-assign cases escalated from their team --- # Real-time Operations Management ### Understanding Your Queue Health The Review Platform provides real-time operational visibility to help you make staffing and prioritization decisions proactively rather than reactively. **Key metrics available in real-time:** **SLA Proximity Tracking**: See how many reviews are within 1 hour, 4 hours, or 24 hours of breaching their SLA. This helps you identify when you need to reallocate resources or bring in additional reviewers before breaches occur. **Queue Velocity**: Understand how quickly reviews are moving through each queue. If velocity is dropping while volume is steady, it may indicate your team needs additional training or that review complexity has increased. **Resource Allocation**: See which queues have the most urgent reviews and where your team's attention is currently focused. This helps you make real-time decisions about moving agents between queues or reprioritizing work. **Work-in-Progress vs. Unassigned**: Know exactly how many reviews are actively being worked on versus sitting unassigned in the queue. This distinction is critical for understanding whether your team is at capacity or if reviews are waiting for someone to pull them. **Why real-time visibility matters:** Retrospective analytics tell you what happened yesterday—that 20% of reviews breached their SLA or that average resolution time was 6 hours. While valuable for long-term optimization, this information doesn't help you solve today's operational challenges. Real-time dashboards answer questions like: * "We have 50 reviews in the queue right now—how many are about to breach?" * "Should I pull someone from the general queue to help with escalations?" * "Is the current staffing level adequate for today's volume?" This operational intelligence helps you prevent problems rather than analyzing them after they occur. --- # Review Agents and Teams ### Review Agents **Review Agents** are your team members who perform reviews. The platform gives you fine-grained control over how agents work: * **Queue Access**: Assign agents to one or multiple queues based on their expertise * **Daily Quotas**: Set target review counts to help manage workload and expectations * **Performance Tracking**: Monitor individual agent productivity, review quality, and throughput * **Team Membership**: Organize agents into teams for easier management and reporting * **Permission Levels**: Control what actions agents can perform (review, escalate, approve, etc.) ### Review Teams **Review Teams** help organize agents for better management and reporting. Teams are particularly useful for: * Grouping agents by specialization (Fraud Team, Document Team, Risk Team) * Comparing performance across different operational groups * Setting team-wide goals and tracking collective progress * Managing escalation paths and subject matter expert availability --- # Integration with Onboarding Engine Reviews logic is defined in [Playbooks](/articles/guides/playbooks-playbook-rules) in Action nodes. The system handles the entire routing process: 1. **Identifies the review kind** based on your Playbook configuration 2. **Selects the appropriate queue** for that review type 3. **Calculates the due date** using the review kind's SLA settings 4. **Assigns the base priority** from the review kind definition 5. **Begins dynamic prioritization** as the review ages toward its deadline This automation ensures consistent routing and prioritization without manual intervention. --- # Getting Started ### Initial Configuration Setting up the Review Platform involves six key steps: ### 1. Configure Review Kinds Define the types of reviews your team performs, including appropriate SLAs and base priorities. Consider your operational needs: * What types of reviews do you perform? * Which reviews are time-critical versus routine? * How should priorities escalate as reviews age? ### 2. Set Up Queues Create queues that match your team structure and workflow segmentation: * Do you have specialized teams that should handle certain review types? * Should high-risk reviews be routed separately from routine verifications? * What prioritization strategy makes sense for each queue? ### 3. Add Review Agents Invite team members and assign them to appropriate queues: * Which agents have expertise for specialized reviews? * Should agents have access to multiple queues? * What are reasonable daily review quotas for each agent? ### 4. (Optional) Create Teams Organize agents into teams for management and reporting: * How is your organization structured? * Do you need separate teams for different reporting needs? ### 5. Configure Playbook Logic Configure the logic to actually raise reviews in your playbooks. See our [Integration with Playbooks](/articles/guides/manual-review-platform-integration-workflows) introduction ### 6. Start Reviewing Once configured, agents can begin pulling and completing reviews. Monitor queue health and adjust configuration as you learn what works best for your operation. ### Best Practices **Start with conservative SLAs**: It's easier to tighten SLAs once you understand your team's capacity than to constantly breach overly aggressive timelines. **Use Hybrid Prioritization**: Unless you have a specific reason to use simpler sorting, Hybrid Prioritization provides the most operationally flexible behavior. **Monitor real-time metrics**: Check queue health throughout the day, especially during high-volume periods, to identify when you need to adjust staffing. **Set realistic agent quotas**: Daily quotas should be targets, not rigid requirements. Factor in review complexity variation and allow flexibility for difficult cases. --- # Introduction Footprint vaults are structured as a key-value store. We define a fixed set of predefined keys that you can use for structured data: like SSN (`id.ssn9`), address (`id.address_line1`, `id.address_line2`, etc), and card number (`card.*.number`). These fields have validation specific to the type of data that they're storing, allowing you to read and write data to these fields with confidence that the data is in the correct format. For any other kinds of unstructred data, you may always use `custom.*` attributes. You'll find the list of supported structured data keys and their validators [here](/articles/vault/fields). ### Identity Identity data that originates from user and business embedded onboarding flows (KYC and KYB) is verified with our decisioning platform and then vaulted. Identity data is comprised of specific attributes that are validated, vaulted, tokenized, and in some attribute types fingerprinted to become matchable. Identity vaults can also be standalone and provisioned via API or vault-proxy, which can be useful when migrating sensitive user data. Regardless of origination, the APIs are identical to provide unified secure PII vaulting. PII reserves the namespace of vault data identifiers prefixed with `id.`. ### Payment Cards In many financial applications, it's required to collect or store payment card data associated with users that your onboard (KYC or not). Footprint provides a PCI-compliant vaulting solution for payment card holder data like: card number, expiration authorization/security code (CVV), and more. The payment card vaulting APIs are consistent with all Footprint vaulting APIs and reserve a specific namespace of vault data identifiers: `card.*`. Unlike identity data, users may have multiple cards, so you can give each card type an alias namespace such as `card.primary.*`. Like identity data, card data is validated prior to vaulting. ### Custom Key-Value Footprint also supports custom key-value attributes that are provided by you and are not validated. Unstructured data are keyed by the format: `custom.` in Footprint’s API requests. You can use custom data to **securely vault** any associated sensitive user or businesses in the vault using a unified vaulting API. ## Update a user vault ```bash curl https://api.onefootprint.com/users/fp_id_GSxJr68GAf5jUT3pdL9ndjf7TLkA3GCX/vault \ -X PATCH \ -u sk_test_CXUsbCR8j2kH6e5GeEl8eSBnQTIPCUaKpv: \ -d '{ "custom.ach_account": "111122224444", "card.primary.expiration": "10/2025", "card.primary.cvc": "424" }' ``` This endpoint has a JSON body limit of 32KB. ## Store large objects in a user vault For custom objects that are larger (up to 10MB), use the `POST /users//vault//upload` API. ```bash curl https://api.onefootprint.com/users/fp_id_GSxJr68GAf5jUT3pdL9ndjf7TLkA3GCX/vault/custom.card_transaction_history/upload \ -X POST \ -u sk_test_CXUsbCR8j2kH6e5GeEl8eSBnQTIPCUaKpv: \ --data-binary @transactions.csv ``` The raw contents of body will be encrypted and stored in the user vault. Use the same decrypt method as defined below to retrieve the contents. Note that upon decryption, the raw contents of the large object are `base64` encoded. ## List available data in a user's vault Check what fields exist on a user's vault. ```bash curl https://api.onefootprint.com/users/fp_id_GSxJr68GAf5jUT3pdL9ndjf7TLkA3GCX/vault?fields=id.ssn9,custom.ach_account,card.primary.number \ -u sk_test_CJvsN1kaZH3GGtYkaZH3GGtY: ``` ```json { "id.ssn9": true, "custom.ach_account": true, "card.primary.number": true } ``` ## Decrypt data from a user's vault Footprint’s API provides attribute-level decryption. API keys are configurable to have certain attribute-level scopes. ```bash curl https://api.onefootprint.com/users/fp_id_GSxJr68GAf5jUT3pdL9ndjf7TLkA3GCX/vault/decrypt \ -X POST \ -u sk_test_CXUsbCR8j2kH6e5GeEl8eSBnQTIPCUaKpv: \ -d '{ "fields": ["id.last_name", "id.dob", "id.ssn9", "custom.ach_account"], "reason": "direct deposit verification" }' ``` ```json { "id.last_name": "Doe", "id.dob": "1988-12-25", "id.ssn9": "121211212", "custom.ach_account": "111122224444" } ``` ## Search across users' vaults Footprint lets you search across all of your users' vaults by specific fields that are fingerprinted. This lets you easily search across all of your users vaults privately without building complicated decryption procedures. ```bash curl https://api.onefootprint.com/users/search -X POST -d '{"search": "Doe"}' \ -u sk_test_CJvsN1kaZH3GGtYkaZH3GGtY: ``` ```json { "data": [ { "id": "fp_id_XyEJ6CF7UNl6K2ymIq8YQS" } ], "meta": { "next": null, "count": 1 } } ``` --- # Creating new vaults If you're backfilling your existing users into Footprint, you may need to vault PII data directly via API. In this case, Footprint supports "Standalone" user vaults that did not onboard through a KYC/IDV flow using our Frontend SDK. The first step is to create a new vault for your user: ```bash curl https://api.onefootprint.com/users \ -X POST \ -u sk_test_CXUsbCR8j2kH6e5GeEl8eSBnQTIPCUaKpv: ``` ```json { "id": "fp_id_K0q6Eh6Rr3WOOfFBLPiHsr" } ``` Save this `id` and associate it with the user in your database. ### Initial data If you have initial data to seed in the vault, you may provide it in the HTTP body: ```bash curl https://api.onefootprint.com/users \ -X POST \ -d '{ "id.first_name": "Jane", "id.last_name": "Joe", "id.dob": "1988-12-30", "id.ssn9": "12-121-1212", "custom.ach_account": "111122224444" }' \ -u sk_test_CXUsbCR8j2kH6e5GeEl8eSBnQTIPCUaKpv: ``` ```json { "id": "fp_id_K0q6Eh6Rr3WOOfFBLPiHsr" } ``` ### Idempotency ID To safely support retrying this `POST /user` request without creating a second user, we support an `x-idempotency-id` header. Requests made with the same `x-idempotency-id` value will no-op and return the same `fp_id`. If you are creating a user vault in Footprint for an existing user record in your database, we recommend using your user record's unique ID as the `x-idempotency-id` to guarantee that only one Footprint user vault is ever created per user. Note, when using `x-idempotency-id`, you are not able to provide initial data in the HTTP body. ```bash curl https://api.onefootprint.com/users \ -X POST \ -H 'x-idempotency-id: my_user_id_1234' -u sk_test_CXUsbCR8j2kH6e5GeEl8eSBnQTIPCUaKpv: ``` ```json { "id": "fp_id_K0q6Eh6Rr3WOOfFBLPiHsr" } ``` This value is safe to store in plaintext! ### Update a standalone user vault With the `id` given from creating a user, you may always update and add new data to your standalone vaults: ```bash curl https://api.onefootprint.com/users/fp_id_GSxJr68GAf5jUT3pdL9ndjf7TLkA3GCX/vault \ -X PATCH \ -u sk_test_CXUsbCR8j2kH6e5GeEl8eSBnQTIPCUaKpv: \ -d '{ "id.email": "jane@acmebank.com", "custom.ach_account": "111122224444" }' ``` For listing, decrypting, and updating -- all the APIs above are identical for standalone user vaults. --- # Vault fields Footprint's vaulting aims to be flexible while providing base-level validation logic for structured vault data such as Identity or PCI data. Below we outline the allowed data identifiers and any validation rules enforced. Note that not all fields may be populated in the vault. ### Identity fields | Data Identifier | Description | Format | | ------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `id.phone_number` | Primary phone number | [E.164](https://www.twilio.com/docs/glossary/what-e164) formatted phone number | | `id.email` | Primary email address | [Email address](https://datatracker.ietf.org/doc/html/rfc3696) | | `id.first_name` | First name | Any valid UTF-8 string \<1KB | | `id.middle_name` | Middle name | Any valid UTF-8 string \<1KB | | `id.last_name` | Last name | Any valid UTF-8 string \<1KB | | `id.ssn9` | Full SSN | 9-digit string (hyphens will be ignored and stripped out) | | `id.ssn4` | Last four of the SSN | 4-digit string | | `id.itin` | Individual taxpayer identification number | 9-digit string beginning with a `9` (hyphens will be ignored and stripped out) | | `id.dob` | Date of birth | Formatted as `%Y-%m-%d` | | `id.address_line1` | First line of the user's street address | Any valid UTF-8 string \<1KB | | `id.address_line2` | Second line of the user's street address | Any valid UTF-8 string \<1KB | | `id.city` | City of the user's street address | Any valid UTF-8 string \<1KB | | `id.state` | State of the user's street address | For US addresses, 2-character [USPS](https://pe.usps.com/text/pub28/28apb.htm) format. Otherwise, any valid UTF-8 string \<1KB | | `id.zip` | Postal code for the user's street address | Postal code `^([A-Za-z0-9\- ]*)$` | | `id.country` | Country for the user's street address | Any valid [ISO3166-alpha-2 Country Code](https://datahub.io/core/country-codes) | | `id.us_legal_status` | The user's legal status in the US | `citizen`, `permanent_resident`, or `visa` | | `id.nationality` | The user's nationality | Any valid [ISO3166-alpha-2 Country Code](https://datahub.io/core/country-codes) | | `id.citizenships` | A list of countries for which the user holds citizenship | A list of valid [ISO3166-alpha-2 Country Codes](https://datahub.io/core/country-codes) | | `id.visa_kind` | The type of US visa held by the user | One of `j1`, `b1`, `b2`, `e1`, `e2`, `e3`, `f1`, `g4`, `h1b`, `l1`, `o1`, `tn1`, or `other` | | `id.visa_expiration_date` | The expiration date of the user's visa | Formatted as `%Y-%m-%d` | ### Business fields | Data Identifier | Description | Format | | --------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `business.name` | Name of the business | Any valid UTF-8 string \<1KB | | `business.dba` | Doing-business-as alias | Any valid UTF-8 string \<1KB | | `business.website` | Website of the business | A valid absolute URL. | | `business.phone_number` | Phone number | [E.164](https://www.twilio.com/docs/glossary/what-e164) formatted phone number | | `business.tin` | Taxpayer identification number | 9-digit string (hyphens will be ignored and stripped out) | | `business.address_line1` | First line of the business's street address | Any valid UTF-8 string \<1KB | | `business.address_line2` | Second line of the business's street address | Any valid UTF-8 string \<1KB | | `business.city` | City of the business's street address | Any valid UTF-8 string \<1KB | | `business.state` | State of the business's street address | For US addresses, 2-character [USPS](https://pe.usps.com/text/pub28/28apb.htm) format. Otherwise, any valid UTF-8 string \<1KB | | `business.zip` | Postal code for the business's street address | Postal code `^([A-Za-z0-9\- ]*)$` | | `business.country` | Country for the business's street address | Any valid [ISO3166-alpha-2 Country Code](https://datahub.io/core/country-codes) | | `business.corporation_type` | The type of corporation | One of `c_corporation`, `s_corporation`, `b_corporation`, `llc`, `llp`, `partnership`, `sole_proprietorship`, `non_profit`, `unknown`, `trust`, `agent` | | `business.formation_state` | US State of formation for the Business | Any valid 2 Character US State Code | | `business.formation_date` | Date of business formation | `YYYY-MM-DD` date format | ### Investor profile fields | Data Identifier | Description | Format | | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `investor_profile.employment_status` | Information on the user's employment status | One of `employed`, `unemployed`, `student`, `retired` | | `investor_profile.employer` | The name of the user's employer, if any | Any valid UTF-8 string \<1KB | | `investor_profile.occupation` | The name of the user's occupation, if any | Any valid UTF-8 string \<1KB | | `investor_profile.annual_income` | Information on the user's annual income | One of `le25k`, `gt25k_le50k,`, `gt50k_le100k`, `gt100k_le200k`, `gt200k_le300k`, `gt300k_le500k` or `gt500k_le1200k` | | `investor_profile.net_worth` | Information on the user's net worth income | One of `le50k`, `gt50k_le100k`, `gt100k_le200k`, `gt200k_le500k`, `gt500k_le1m`, `gt1m_le5m` or `gt5m` | | `investor_profile.funding_sources` | Information on the user's funding sources | One of `employment_income`, `investments`, `inheritance`, `business_income`, `savings` or `family` | | `investor_profile.investment_goals` | Information on the user's investment goals. | List with at least one of `growth`, `income`, `preserve_capital`, `speculation`, `diversification`, or `other` | | `investor_profile.risk_tolerance` | The user's stated risk tolerance | One of `conservative`, `moderate`, `aggressive` | | `investor_profile.declarations` | Self-proclaimed declarations | An array of `affiliated_with_us_broker`, `senior_executive` or `senior_political_figure`. May be empty if none apply | | `investor_profile.brokerage_firm_employer` | If the user selected `affiliated_with_us_broker` in declarations, the name of the brokerage | Any valid UTF-8 string \<1KB | | `document.finra_compliance_letter` | If the user selected `affiliated_with_us_broker` in declarations, the uploaded FINRA compliance document | PDF document | | `investor_profile.senior_executive_symbols` | If the user selected `senior_executive` in declarations, the list of associated symbols for which the user is a senior executive | An array of symbols, each 3-5 ASCII alphabetic characters | | `investor_profile.family_member_names` | If the user selected `senior_political_figure` in declarations, the names of the user's immediate family members | An array of names, each UTF-8 strings \<1KB | | `investor_profile.political_organization` | If the user selected `senior_political_figure` in declarations, the name of the political organization | Any valid UTF-8 string \<1KB | ### Document extracted fields If a document is uploaded during onboarding, Footprint will attempt to extract the following fields from respective documents. These extracted fields can be accessed through the document's type, which is one of `id_card`, `drivers_license`, `passport`, `permit`, `visa`, or `residence_document`. For example, `document.drivers_license.document_number` represents the driver's license document number. | Data Identifier | Description | Format | | ---------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------- | | `document.*.full_name` | The extracted name | Any UTF-8 string \<200B | | `document.*.dob` | The extracted date of birth | Formatted as `%Y-%m-%d` | | `document.*.gender` | The extracted gender | Any UTF-8 string \<200B | | `document.*.full_address` | The extracted full address | Any UTF-8 string \<200B | | `document.*.document_number` | The extracted document number | Any UTF-8 string \<200B | | `document.*.issued_at` | The date on which the document was issued | Formatted as `%Y-%m-%d` | | `document.*.expires_at` | The date at which the document expires | Formatted as `%Y-%m-%d` | | `document.*.issuing_state` | The name of the state that issued the document | Any UTF-8 string \<200B | | `document.*.issuing_country` | The country that issued the document | Any valid [ISO3166-alpha-2 Country Code](https://datahub.io/core/country-codes) | | `document.*.nationality` | The nationality extracted from the document | Any UTF-8 string \<200B | ### Document images If a document is uploaded during onboarding or vaulted via the API, the document images can be accessed via the following data identifiers. Fields can be accessed through the document's type, which is one of `id_card`, `drivers_license`, `passport`, `permit`, `visa`, or `residence_document` | Data Identifier | Description | Format | | ------------------------ | ------------------------------------------------------------------------ | -------------------- | | `document.*.front.image` | The front image of the document | Base64 encoded image | | `document.*.back.image` | The back image of the document (note: not all document types have backs) | Base64 encoded image | ### Card fields Each individual card is assigned an alias by your application. Each card alias must match `^([A-Za-z0-9\-_]+)$`. | Data Identifier | Description | Format | | -------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `card.*.name` | The name of the cardholder | Any UTF-8 string \<200B | | `card.*.number` | The full card number | Valid credit or debit card number (length and Luhn check). Supports Visa, Mastercard, Amex, MIR, Diners Club, Discover, UnionPay, JCB, Visa Electron, Maestro, Forbrugsforeningen, Dankort. | | `card.*.cvc` | The verification code for the credit card | Valid 3- or 4-digit numeric code | | `card.*.expiration` | The date at which the card expires | `MM/YYYY`. Accepts as input any of `MM/YYYY`, `MM-YYYY`, `MM/YY`, `MM-YY`, `M/YY`, or `M-YY`, and canonicalized as `MM/YYYY` before vaulting, with `MM/YY` interpreted as `MM/20YY`. | | `card.*.billing_address.zip` | The billing ZIP code for the card | Postal code `^([A-Za-z0-9\- ]*)$` | | `card.*.billing_address.country` | The billing country for the card | Any valid [ISO3166-alpha-2 Country Code](https://datahub.io/core/country-codes) | | `card.*.number_last4` | The last four digits of the card number. Automatically populated | 4-digit string. Cannot be specified, can only be read. | | `card.*.issuer` | The issuer of the card. Automatically populated | One of `visa`, `master_card`, `amex`, `discover`, `mir`, `diners_club`, `union_pay`, `jcb`, `visa_electron`, `maestro`, `forbrugsforeningen`, `dankort`, or `unknown` | ### Bank account fields Each individual bank account is assigned an alias by your application. Each bank alias must match `^([A-Za-z0-9\-_]+)$`. | Data Identifier | Description | Format | | --------------------------- | --------------------------------------------------------------- | ----------------------- | | `bank.*.name` | The name of the bank account | Any UTF-8 string \<200B | | `bank.*.account_type` | The type of the bank account; usually 'checking or 'savings' | Any UTF-8 string \<200B | | `bank.*.account_last4` | The last four characters/digits of the account | Any UTF-8 string \<200B | | `bank.*.ach_routing_number` | The ACH routing number as a string | Any UTF-8 string \<200B | | `bank.*.ach_account_number` | The ACH account number as a string | Any UTF-8 string \<200B | | `bank.*.ach_account_id` | The identifier of the ach account | Any UTF-8 string \<200B | | `bank.*.institution_id` | The identifier of the financial institution | Any UTF-8 string \<200B | | `bank.*.institution_name` | The name of the financial institution | Any UTF-8 string \<200B | | `bank.*.iban` | The IBAN account number | Any UTF-8 string \<200B | | `bank.*.bic` | The BIC bank code | Any UTF-8 string \<200B | | `bank.*.owners` | An JSON object representing the owners on the account | Any JSON object | | `bank.*.link_id` | The corresponding link id if this account was connected via BAL | Any UTF-8 string \<200B | | `bank.*.closed` | Boolean string representing if this account is open or closed | 'true' or 'false | ### Custom fields Each piece of custom data is assigned an alias by your application. Each custom data alias must match `^([A-Za-z0-9\-_\.]+)$`. | Data Identifier | Description | Format | | --------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `custom.*` | Any piece of data you'd like to provide. | Any UTF-8 string. Note that custom field name represented by `*` can be any UTF-8 string matching `^([A-Za-z0-9\-_\.]+)$`. | --- # Vault Proxy In certain cases, your application may need to communicate sensitive data (like PII or PCI data) to third-party services (like payment gateways). With Footprint's Vault Proxy, you can avoid sensitive data touching your application code and infrastructure in plaintext. Not only does vault proxying help strengthen your security posture and avoid data leaks/compromises, but this also helps you satisfy strict compliance requirements (like PCI) by never having this data touch your networks or infrastructure. Please ensure you are familiar with our [server-side API authentication guide](/api-reference#authentication) to securely connect to the Footprint API from your backend. The vault proxy supports the following main features: | Feature | Overview | | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | **Any HTTPS URL and method** | Works with any HTTPS resource | | **Flexible detokenization** | Supports secure detokenization for any type of request body, content-type agnostic | | **Fixed IP range** | Requests from Footprint Vault Proxy always come from a fixed set of IP addresses that you can whitelist on the proxy destination | | **Custom headers** | Attach custom headers | | **Authentication secrets** | Securely attach custom authentication secrets | | **Client Certififcates (mTLS)** | Securely attach a ceritificate + key for mTLS Client Certififcate authentication connections | | **Server Certificate Pinning** | Specify one or more Root CAs and leaf certificates to securely validate the destination | | **Ingress Vaulting** | Securely vault data that comes back on the ingress. Support for JSONPath (XPath and Regex coming soon) rules | # Configuration The proxy can be configured in two main ways: define the configuration using the dashboard/admin API or dynamically specify all the configuration values via headers "just-in-time." Both methods support the same fundamental capabilities, but the two options offer flexibility in defining proxy configurations and simplifying the management of sensitive data and authentication credentials. ## Just-in-time In order to dynamically proxy data anywhere, without defining a configuration ahead of time, you must specify the following header: | Header | Usage | Description | | ----------------------- | -------------------------- | -------------------------------------------------------------------- | | `x-fp-proxy-target-url` | Required if "just-in-time" | The target destination HTTPS URL to which the request will be routed | ## By configuration Alternatively, if using a vault proxy configuration either via the API or the dashboard, you can invoke a specific configuration by grabbing it's `id` as follows: ```bash curl https://api.onefootprint.com/vault_proxy/proxy_id_AY5ec7I5QDUFWG8BAQG78k \ ...other parameters... ``` # Basics ## Making a proxy request A vault proxy request consists of a typical authenticated HTTP `POST` request to either `/vault_proxy/jit` or `/vault_proxy/{proxy_config_id}` endpoint. For example: ```bash curl https://api.onefootprint.com/vault_proxy/jit \ -u sk_test_CXUsbCR8j2kH6e5GeEl8eSBnQTIPCUaKpv: \ -X POST \ -H 'x-fp-proxy-target-url: https://payments.acmebank.com' \ -H 'x-fp-proxy-fwd-custom-header: custom value' \ -H 'x-fp-proxy-fwd-content-type: application/json' \ --data '{ "full_name": "{{ fp_id_tctecBEvGc98V7Vx4MhZU.id.first_name }} {{ fp_id_tctecBEvGc98V7Vx4MhZU.id.last_name }}", "last4_ssn": "{{ fp_id_tctecBEvGc98V7Vx4MhZU.id.ssn4 }}", "cc": "{{ fp_id_tctecBEvGc98V7Vx4MhZU.custom.credit_card }}", "cc_exp": "{{ fp_id_tctecBEvGc98V7Vx4MhZU.custom.credit_card_exp }}", "cc_cvc": "{{ fp_id_tctecBEvGc98V7Vx4MhZU.custom.credit_card_cvc }}" }' ``` The above request detokenizes each of the fields in the request body in the Footprint vault enclave, and then makes an upstream request with the corresponding in-place-updated plaintext body to the target (in this case `https://payments.acmebank.com`) with custom headers `'Custom-Header: custom value'` and `'Content-type: application/json'`. You can add additional control headers to configure how the upstream proxy request behaves: | Header | Required? | Function | | -------------------------- | --------- | --------------------------------------------------------------------------------------------------------- | | `x-fp-proxy-fwd-
` | Optional | Sends a header named `
` with the corresponding value | | `x-fp-proxy-method` | Optional | Selects the HTTP Method Verb to use (defaults to POST if unspecified) | | `x-fp-proxy-access-reason` | Optional | The decryption reason to use for access/security logs during detokenization. Defaults to empty/no-reason. | The proxy request will fail if the API key does not have permission to detokenize the supplied fields or if any of the supplied fields do not exist on the object. The body of the proxy request does **NOT** need to be JSON. Any UTF-8 body will work and tokens will be substituted in-place. See the token template format for a detailed description on how to specify tokens in the body. ## Token body template format Footprint's Vault Proxy will attempt to substitute in-place any tokens found in the body of the request with the corresponding detokenized, plain-text values. A token expression starts with `{{` and ends with `}}`. Footprint's vault proxy APIs support two methods of specifying tokens in the request body: ### Fully-qualified tokens Each token is specified as `{{ . }}`. `` is the user's footprint token. `` can be any identity KYC attribute (prefixed with `id.`) or any custom attribute defined by you (prefixed with `custom.`) | Fully-qualified format | | ------------------------------------------------------ | | `{{ fp_id_tctecBEvGc98V7Vx4MhZU.id.last_name }}` | | `{{ fp_id_tctecBEvGc98V7Vx4MhZU.id.ssn9 }}` | | `{{ fp_id_tctecBEvGc98V7Vx4MhZU.id.dob }}` | | `{{ fp_id_tctecBEvGc98V7Vx4MhZU.custom.credit_card }}` | ### Inferred-user tokens If you're operating on a single Footprint user vault, you can simplify the token body formatting by specifying the following header to identify which user vault is referenced. | Header | Value | | --------- | ----------------------------------------------------- | | `x-fp-id` | The Footprint user token used to infer request tokens | In this case, the token can omit the `` above and specify tokens as `{{ }}`. | Inferred-user format | | -------------------------- | | `{{ id.last_name }}` | | `{{ id.ssn9 }}` | | `{{ id.dob }}` | | `{{ custom.credit_card }}` | ## Filter Functions Similar to common template languages like Jinja and Handlebars, Footprint's templates support filter functions to help transform data in and out of the vault. Using the token template format above, simply concatentate zero or more filter functions, separated by `|` | Filter Function | Description | | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `to_lowercase` | Converts the target utf-8 string to lowercase characters | | `to_uppercase` | Converts the target utf-8 string to UPPERCASE characters | | `to_ascii` | Converts the target utf-8 string to ASCII characters only | | `prefix(n)` | Returns the first `n` characters of the target utf-8 string where `n` is a positive integer. | | `suffix(n)` | Returns the last `n` characters of the target utf-8 string where `n` is a positive integer. | | `replace(from,to)` | Replaces all matches of the string `from` with the string `to` in the target utf-8 string. | | `date_format(from_format,to_format)` | Parses the target string in date format `from_format` and then converts it to date format `to_format`. Supports [`strftime` format strings](https://strftime.org). Uses of this filter will error if the target string is not codable in the supplied formats. | | `hmac_sha256(key)` | HMAC-SHA256 algorithm where the `key` argument is parsed as a HEX-encoded string, and the output is HEX-encoded. | | `encrypt(algorithm, public_key)` | Asymmetrically encrypts the contents to a public-key, where algorithm is either `rsa_pkcs1v15` or `ecies_p256_x963_sha256_aes_gcm` and the corresponding public key is a HEX-encoded DER-formatted public key. RSA public-keys must be formatted with PKCS#8. | String arguments to filter functions must be enclosed in either `"` or `'` quotations. ### Filter function examples Below find several examples for how to use the filter functions syntax: * `{{ id.last_name | to_ascii | to_uppercase }}` Converts `Doè` to `DOE` * `{{ id.dob | date_format("%Y-%m-%d", "%A in %B of %y") }}` Converts `1988-12-30` to `Friday in December of 88` * `{{ custom.ach_account| replace("-", "") }}` Converts `12-1212-1212` to `1212121212`. # Ingress Vaulting Ingress vaulting rules define how to tokenize and vault certain sensitive fields that appear in the response from the upstream proxy destination. This allows the vault proxy to not only de-tokenize outgoing data but to also tokenize incoming data to vault it just in time. For simplicity, you can also define all of your ingress vaulting rules via the developer dashboard or admin API and simply invoke the proxy configuration by it's `id` using the `api.onefootprint.com/vault_proxy/{proxy_id}` endpoint. However, when you declare ingress vaulting rules in the configuration, you still need to denote in the proxy request into which Footprint user vault to tokenize the data. Therefore, the following header value is required when using ingress rules for pre-configured proxy: | Header | Format | value | | --------- | --------- | --------------------------------------------------------------- | | `x-fp-id` | `` | The Footprint user token for which the vaulting rules apply too | For example: add the header `'x-fp-id: fp_id_tctecBEvGc98V7Vx4MhZU'` where the rule in the proxy configuration would be defined as `custom.credit_card_number=$.data.card.number`. ## Just-in-time ingress vaulting The format of the rule is defined as `.custom.=` where the path format is relative to the content-type being used. Note if using a dynamic proxy configuration (just-in-time), you **must specify** a single `x-fp-proxy-ingress-content-type` header if one more `x-fp-proxy-ingress-rule` headers are present. Ingress rules use a similar token format to the body format above, except without the `{{` and `}}` delimiters. ## JSONPath Currently ingress only supports JSON and the associated [JSONPath](https://github.com/json-path/JsonPath) format for specifying a target value. ```bash -H 'x-fp-proxy-ingress-rule: fp_id_tctecBEvGc98V7Vx4MhZU.custom.credit_card_number=$.data.card.number' ``` The above token would extract `4242424242424` from the following response object and vault it under the `custom.credit_card_number` field for `fp_id_tctecBEvGc98V7Vx4MhZU`'s user vault. ```json { "data": { "card": { "number": "4242424242424", "expiration": { "month": "04", "year": "25" } }, "processor": "amex" } } ``` | More examples | | --------------------------------------------------------------------------------- | | `fp_id_tctecBEvGc98V7Vx4MhZU.card.primary.number=$.data.card.number` | | `fp_id_tctecBEvGc98V7Vx4MhZU.card.primary.exp_month=$.data.card.expiration_month` | | `fp_id_tctecBEvGc98V7Vx4MhZU.card.primary.exp_year=$.data.card.expiration_year` | | `fp_id_tctecBEvGc98V7Vx4MhZU.card.primary.cvc=$.data.card.security_code` | ### Support for filter functions In some cases, you may need to alter or reformat ingress data prior to storing it in the vault. Ingress directives support filter functions defined above to solve this exact problem. The format is defined as above, except the filter functions are appended to the right side of the JSONPath selector: ``` . = | | | ... ``` For example, given a JSON body like ```json { "card": { "number": "4242-4242-4242-4242" } } ``` With an ingress directive like ``` fp_id_tctecBEvGc98V7Vx4MhZU.card.primary.number = $.card.number | replace("-", "") ``` Results in `fp_id_tctecBEvGc98V7Vx4MhZU.card.primary.number` having the value `4242424242424242`. # Reflection In some cases, you may not need to proxy a complex object to a third-party destination, but would still like to use the vault proxy syntax and mechanics. Footprint supports this operation with our `reflect` endpoint. The reflect endpoint can be useful for decrypting into complex objects or for even testing your vault proxy configurations. You can think of this as the just the "first hop" of the vault proxy. For example, ```bash curl https://api.onefootprint.com/vault_proxy/reflect \ -u sk_test_0Te2YtSveZpWLMjQgNRkCv6siiC86iMIkZ: \ -H 'x-fp-id: fp_id_JHSfbHz7VdxfoPXuaOlZqb' \ --data \ 'The name on my credit card is {{ card.primary.name | to_ascii | to_uppercase }}. I was born on a {{ id.dob | date_format("%Y-%m-%d", "%A in %B of %y") }}.' The name on my credit card is JANE DOE. I was born on a Friday in December of 88. ``` To use reflection: make a `POST` request to `/vault_proxy/reflect` and send your complex object with proxy token syntax (including filter functions in the body). For simplicity, you can send a `x-fp-id` header to use the inferred proxy token syntax (i.e. omitting the fp\_id in each directive). # Advanced proxy settings Footprint's vault proxy supports advanced configurations such as mTLS (client certificates), certificate pinning (custom server certificates), and automatically selecting and tokenizing parts of the response data and storing it in the vault. Note that the guide below shows how to configure these advanced features using "just-in-time" headers, but we recommend specifying these instead using the developer dashboard for simplicity and greater security (you won't need to manage authentication secrets for the destination service). ## Client certificate authentication (mTLS) Optionally, specify a client certificate and key in order to connect securely to the proxy destination using Mutual TLS (mTLS). Only a single cert/key pair can be supplied. Note the format of the certificate/key is in [PEM](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail) and then [Percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding). ## Service certificate pinning and Root CAs Optionally, specify zero or more server certificates or Root CAs to pin the upstream proxy request server certificate validation. If multiple certificates are specified, all the supplied certificates will be used in the validation process and as long as a single certificate can validate the incoming certificate chain, the connection will succeed. Note the format of the certificate/key is in [PEM](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail) format and then [`Percent-encoded`](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding). # Header and Configuration Reference Below find the complete reference guide of headers and their uses for both configuring and invoking the proxy "just-in-time". Note that some values can be defined in the proxy configuration settings and do not need to be sent each time. | Header | Usage | Description | | --------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | | `x-fp-proxy-target-url` | Required if "just-in-time" | The target destination HTTPS URL to which the request will be routed | | `x-fp-proxy-method` | Optional. | Selects the HTTP Method Verb to use (defaults to POST if unspecified). Overrides a configuration's value if present. | | `x-fp-proxy-fwd-
` | Optional. Multiple allowed. | Sends a header named `
` with the corresponding value | | `x-fp-path-and-query` | Optional. | Use this header to customize/add additional path and/or query parameters to your existing proxy target URL | | `x-fp-id` | Optional. | The Footprint user token for which the proxy rules apply to. Required if tokens in the body dont specify an `fp_id_` prefix. | | `x-fp-proxy-access-reason` | Optional. | The decryption reason to use for access/security logs during detokenization. Defaults to empty/no-reason. | | `x-fp-proxy-client-cert` | Optional. Percent-encoded PEM | The client certificate to use | | `x-fp-proxy-client-key` | Optional. Percent-encoded PEM | The client certificate private key to use | | `x-fp-proxy-pin-cert` | Optional. Percent-encoded PEM. Multiple allowed. | A root CA certificate or self-signed certificate to validate the certificate of the server | | `x-fp-proxy-ingress-content-type` | Optional. | The content-type of the response to process ingress rules. Currently only `json` is supported, but `regex` and `xml` are coming soon! | | `x-fp-proxy-ingress-rule` | Optional. `=`. Multiple allowed. | Ingress rule itself, assigning a specific namepspeced token to the corresponding path of the value to vault | # Playground Use our `ditto` server to test your proxy configurations. Sending requests to `https://ditto.footprint.dev` will replay the headers and body of any incoming requests. Give it a try! ## Basic usage ```bash $ curl -i -X POST https://ditto.footprint.dev -H 'Test-Header: FootprintRocks' --data '{"hello": "world" }' test-header: FootprintRocks content-type: application/x-www-form-urlencoded {"hello": "world" } ``` By using `https://ditto.footprint.dev` as your proxy destination target, you can test that the proxy decryption and token templating is working as expected. ## Testing with client certificates The footprint ditto server also supports testing mTLS with client certificates: use `https://ditto.footprint.dev:8443` (port 8443). Note that the server certificate is self-signed so you must either trust it or pin it the configuration. ```bash $ curl --cert client.crt --key client.key -i -k -X POST https://ditto.footprint.dev:8443 -H 'Test-Header: FootprintRocks' --data '{"hello": "world" }' x-ditto-client-cert-serial: 12431179266346922388 test-header: FootprintRocks content-type: application/x-www-form-urlencoded {"hello": "world" } ``` Note the ditto server echoes the client certificate SERIAL (if a client certificate is used) in the special header `x-ditto-client-cert-serial`. --- # Building Playbooks A Playbook defines how Footprint onboards a user or business: what data to collect, which verification checks to run, and how to decide the final outcome. You build a Playbook visually in the [Playbook builder](https://dashboard.onefootprint.com/playbooks) by connecting **nodes** into a flow. This guide is a light walkthrough of creating a Playbook and the nodes available to you. For a deep dive on the rules language used by Rules nodes, see [Playbook Rules](/articles/guides/playbooks-playbook-rules). ## Key concepts A few concepts make the rest of this guide easier to follow: * **Directed acyclic graph (DAG)** — a Playbook is a DAG with a single start node. Control flows from one node to the next and never loops back. Branching nodes split the flow into multiple paths, which merge back together so the flow can continue to a shared decision. * **Collection nodes vs. decisioning nodes** — collection nodes (identity data, business data, custom data, documents) are user-facing: they pause the flow to gather input from the person being onboarded. Decisioning nodes (verification checks, rules, branches, external API, expression, agent, and action nodes) run on Footprint's backend and don't show anything to the user. * **Vault data (`vault.*`)** — durable, encrypted attributes stored in the user's or business's [vault](/articles/vault/fields) (e.g. `vault.id.first_name`). * **Onboarding data (`data.*`)** — ephemeral values computed during a single onboarding run (by expression, rules, external API, and agent nodes) and made available to later nodes under the `data.*` prefix. Onboarding data is scoped to that onboarding and is not written to the vault unless you explicitly copy it there with a [Data assignment](#data-assignment) node. ## Creating a Playbook From the [Playbooks](https://dashboard.onefootprint.com/playbooks) page in the dashboard, click **Create** and choose a starting point: * **A template** — a ready-to-use flow (e.g. KYC, KYB) that comes pre-wired with sensible data collection, verification checks, and rules. This is the fastest way to start. * **Onboard People** — a blank person (KYC) Playbook. * **Onboard Businesses** — a blank business (KYB) Playbook. Give the Playbook a name and click **Create playbook** to open it in the builder. When you're happy with the flow, publish it to start onboarding against it. ## The Playbook graph A Playbook is a directed acyclic graph (DAG). It has a single starting node and flows sequentially from top to bottom, ending at **Onboarding Complete**. Each node runs in order, passing the user along to the next node, and some nodes execute logic to control branching. To add a node, click the **+** between any two nodes to open the **Add node** dialog. Nodes are grouped into tabs — **Recipes**, **Data**, **Documents**, **Decision**, **Advanced**, and **AI Nodes** — so you can browse by purpose. Branching nodes fan the flow out into multiple paths; those paths merge back together so the flow can continue to a shared decision. In [sandbox mode](/articles/guides/sandbox-mode), verification checks don't contact real vendors. By default a mock outcome (Pass, Fail, Manual review, etc.) is chosen and branching is short-circuited to match it. Select **Evaluate rules** to run your Playbook's rules engine normally against the mocked risk signals you choose. ## Collecting data and documents Most Playbooks begin by collecting data and documents from the user. These are user-facing nodes that pause the onboarding flow to gather input: * **Identity data** — collect identity data (name, date of birth, SSN, address, etc.) from the user. * **Business data** — collect business data from the user, for KYB Playbooks. * **Custom data** — configure a page of custom prompts to collect arbitrary data from the user. * **Identity document** — collect and verify a government-issued identity document from the user, like a driver's license or passport. * **Custom document** — configure a custom document to be collected from the user. Additional collection nodes are available for investor profiles, card data, bank account linking, and proof-of-address or proof-of-SSN documents. ## Branching Branching nodes split the onboarding into different paths so you can tailor the flow per user. There are two ways to branch: * **Rules** — express decisioning logic as CEL expressions over risk signals, vaulted data, and onboarding data. Rules are grouped, each group produces an outcome value, groups are evaluated in order, and the first match wins (falling back to a required default value). This is the most flexible option and is covered in detail in [Playbook Rules](/articles/guides/playbooks-playbook-rules). * **Branch** — a simpler match on a single piece of collected data. You pick a variable (e.g. `id.country`), then define cases with operators like `=`, `!=`, `>`, `in`, and `not in`, plus a **default branch** for anything that doesn't match. A common use case is splitting U.S. and international users into different document or verification requirements, or sending higher-risk users down a path that has nodes configured to satisfy Enhanced Due Diligence (EDD) requirements. ## Verification checks A **Verification checks** node runs one or more checks against the data collected so far and produces risk signals that downstream Rules nodes can act on. Available checks include: * **Know Your Customer (KYC)** — verifies collected identity data against government databases, credit bureaus, and other official sources. * **Know Your Business (KYB)** — verifies a business via Middesk or Baselayer, either as a full KYB check or TIN (EIN) verification only. * **Anti-Money Laundering (AML)** — screens against OFAC, PEP, and adverse media lists, with configurable fuzzy/exact matching and optional ongoing monthly screening. * **Fraud** — synthetic fraud detection with SentiLink. Some checks require specific data to be collected first (e.g. TIN for KYB, phone + SSN for certain KYC checks), so place verification checks after the relevant data collection nodes. ## External API nodes An **External API** node calls an external service during onboarding to verify or enrich user data. You can configure it two ways: * **Vault proxy** — call your own API through a [vault proxy](/articles/vault/proxy) configuration, so request and response bodies can reference vaulted data securely. * **Managed integration** — dispatch to a Footprint-managed vendor integration. You define the request (method, base URL, headers, query params) and map fields from the response back into onboarding data using JSONPath (or capture the entire body). You can require a successful `2xx` status code and supply a mock response for sandbox testing. The captured values become available to later nodes as `data.*` variables. ## Data assignment A **Data assignment** node stores or assigns data into a user's vault at runtime. Each assignment maps a **source** to a **destination** vault field. The source is a templated string, so you can combine literal text with `{{ }}` variables — for example, copying `{{ data.computed_value }}` into `vault.id.ssn9`, or building a note from multiple fields. Use this to persist computed values, normalize data, or backfill vault fields based on what happened earlier in the flow. ## Expression nodes An **Expression node** runs one or more [CEL (Common Expression Language)](https://github.com/google/cel-spec) expressions and stores each result in an onboarding data key for later nodes to use. Each expression has an **Assign to** key and an expression body. For example: ```js filename="Expression" data.risk_score > 20 && vault.id.country != "US" ? "block" : "accept" ``` assigned to `txn_decision` lets a downstream Rules or Branch node act on `data.txn_decision`. CEL supports comparisons, ternaries, string operations, list membership, and JSON parsing — useful for computing intermediate values that don't come directly from a verification check. Press `CTRL + SPACE` in the editor to see available variables. ## Action nodes An **Action** node applies one or more side effects to the user or business — it doesn't collect data. Place action nodes after your rules to carry out the decision. Available actions: * **Set decision** — set the onboarding outcome to **Pass**, **Fail**, or **None**. * **Manual Review** — flag the onboarding for a reviewer, with a reason (e.g. rule triggered, document needs review). * **Set status** — apply a [custom status](/articles/guides/custom-statuses) defined in your org settings. * **Tag** — add one or more tags to the user's profile (separate multiple tags with `;`). * **Note** — add a note to the profile, with support for `{{ }}` dynamic fields. A single Action node can carry multiple actions, so you can, for example, set a decision and add a tag at the same time. ## Agent nodes Agent nodes run a Percy AI agent as a step in your Playbook — for example, to review watchlist hits, screen adverse media, or research a business, and return a structured decision the rest of the flow can use. To add one, open the **Add node** dialog, go to the **AI Nodes** tab, and choose **AI Agent**. Select the new node to open its sidebar, then pick the agent and the agent version it should run. (An agent must have a released version to be selectable, and its latest release is chosen by default.) Configure the node by providing **Input data** — key/value pairs that feed the agent, using template variables like `{{ vault.id.first_name }}` to pass in vaulted data. When the agent runs, its structured output is written back to onboarding data so downstream Rules, Branch, or Action nodes can act on the agent's conclusion. Agent nodes pin to a specific released version of an agent, so a Playbook's behavior won't change unexpectedly when you iterate on an agent. Update the node to a newer version when you're ready to roll out changes. --- # Playbook Rules Footprint’s rules engine enables you to customize user onboarding decisioning to fit your business’s risk model. When you create a playbook from a template, rules nodes are preloaded with a default set of rules. Businesses may find this default set of rules sufficient, or they may choose to customize the onboarding flow using the Footprint rules editor. Rules are written as [CEL (Common Expression Language)](https://github.com/google/cel-spec) expressions. ## Rule evaluation Rules enable businesses to express custom logic for risk decisioning or data collection. For example, rules can be used for each of the following: * Assign final user statuses based on different risk signals. * Split an onboarding flow based on user attributes (e.g. different onboarding flows for U.S. and international users). * Define Enhanced Due Diligence (EDD) or step-up flows. A rules node sets an outcome value based on the data and risk signals available during onboarding. Rules are organized into groups, where each group has a value and a set of associated rules. Groups are evaluated in the order they are displayed (top-to-bottom), and a group matches if any of its associated rules evaluates to `true`. The first matching group's value is used, and the result is written to an onboarding data key (e.g. `data.kyc_outcome`) that the rest of the playbook can act on. If no group matches, the default value is used. A default value is required and is always displayed last. ## Rules as CEL expressions Rules are expressions written in [CEL (Common Expression Language)](https://github.com/google/cel-spec) — a simple, type-safe language from Google — that evaluate during onboarding to either `true` or `false`. They are composed of input features, comparison expressions, and boolean operators. Input features have the format `.`. There are three namespaces of features: * `risk_signal`: Risk signals derived from verification checks * `vault`: Data vaulted during onboarding * `data`: Values computed earlier in the onboarding flow (for example, by an upstream rules or expression node) Within each of the namespaces is a number of features (for example, `risk_signal.watchlist_hit_ofac`, `vault.id.country`, and `data.charge_risk_score`). Features available to a rule are suggested as you type in the rules editor. Press `CTRL + SPACE` to see the variables available in the current scope. ### Risk signal expressions Risk signal input features in the `risk_signal` namespace take on boolean values (`true` or `false`), indicating whether that risk signal was triggered by a verification check. Therefore, the simplest rule is a single risk signal. For example: ```js filename="Rule" risk_signal.watchlist_hit_ofac ``` This rule evaluates to `true` if the verification checks yielded this risk signal. The `!` operator negates a risk signal, meaning the following evaluates to `true` if the verification checks did not yield this risk signal: ```js filename="Rule" !risk_signal.watchlist_hit_ofac ``` The complete list of risk signals available can be found in the [Risk Signal Glossary](https://dashboard.onefootprint.com/home?rsg=open). ### Vault data expressions Vault data input features in the `vault` namespace take on string values. The features in this namespace map one-to-one to the vault fields listed [here](https://docs.onefootprint.com/articles/vault/fields). For example `id.zip` is available to rules as the feature `vault.id.zip` and `business.country` is available as `vault.business.country`. Since rules are boolean expressions and vault data features are strings, vault data features must be compared to a specific value. CEL uses `==` for equality: ```js filename="Rule" vault.id.country == "US" ``` This would evaluate to `true` if the country entered during onboarding is `US`. Vault data also supports inequality checks with the `!=` operator: ```js filename="Rule" vault.id.country != "CA" ``` You can also check membership in a list with the `in` operator: ```js filename="Rule" vault.id.country in ["US", "CA", "MX"] ``` ### Compound rule expressions Expressions can be composed together with the logical operators `!` (not), `&&` (and), and `||` (or), and grouped using parentheses to express more complex conditions. For example: ```js filename="Rule" risk_signal.device_high_risk && (!risk_signal.attested_device_apple || vault.id.country != "US") ``` The `&&` operator takes precedence over the `||` operator, and the `!` operator has the highest precedence. Take for example this rule without parentheses: ```js filename="Rule" !risk_signal.attested_device_apple || vault.id.country != "US" && !risk_signal.device_high_risk ``` Formatted with parentheses to clarify the order of operations, the above rule is equivalent to: ```js filename="Rule" (!risk_signal.attested_device_apple) || ( vault.id.country != "US" && (!risk_signal.device_high_risk) ) ``` Because CEL is a full expression language, rules can also use ternaries (`? :`), string functions, and helpers like `size(...)` when you need them. ## Best practices To keep your rules easy to understand, you may split up top-level `||` expressions into multiple rules within the same group. Take for example this compound rule: ```js filename="Rule" !risk_signal.document_ocr_name_matches || !risk_signal.document_selfie_matches ``` This rule can be split up into two rules in the same group: ```js filename="Rule" !risk_signal.document_ocr_name_matches ``` ```js filename="Rule" !risk_signal.document_selfie_matches ``` Since a group matches if any one of its associated rules matches, these two rule sets express the same logic. --- # Bank Account Linking (BAL) # Introduction Footprint is now excited to offer Bank Account Linking, which works seamlessly with our existing identity and vaulting technologies. Powered by open banking along with the best-in-class data aggregators, Footprint's Bank Account Linking enables you to easily connect your product to 15k+ financial institutions, cards, investment accounts, and more. Bank linking with Footprint comes with a few unqiue benefits * Use the same SDK to verify identities and link bank accounts (Native iOS/Android, React/Web, Flutter, and more). * Automatic secure vaulting for bank account numbers, ownership data, and more. Utilize the vault proxy to securely, and compliantly execute bank transfers with one of our many partners. * Built-in operations support in the Footprint dashboard: view accounts, balances, connection states, automatically re-link expired connections, decrypt bank account data, sift through transactions * Strongly integrated with Footprint's Identity, Risk, and Fraud products: when a bank account is linked, Footprint automatically extracts account ownership and financial data to hydrate additional risk and fraud signals for verifying the identity behind the account. Seamlessly use Bank account data in identity verification (and vice-versa) without writing a single line of code. * Simple unified APIs for pulling transaction data. # Link SDK The Link SDK is a widget that integrates with your frontend wherever it lives (iOS, Android, or mobile web, or desktop) to provide users with a simple, beautiful, and whitelabeled experience to connect their bank account to your product. ## Generate a bank linking session token The first step is to start the onboarding and get an onboarding token for bank account linking. Go to [the Footprint dashboard](https://dashboard.onefootprint.com/playbooks) and create a Playbook with a bank-linking node. Then get an onboarding token e.g. `obtok_vsd94fc0gksdfsdf824fx9JaGO7sgqHX` from [POST /onboardings](/articles/guide/definitive-integration-guide#the-end-to-end-integration-step-4-run-the-playbook). ## iOS (Swift UI) To integrate bank linking into your SwiftUI application, install our [Swift SDK](/articles/sdks/swift-introduction#installation-swift-package-manager) and use the `FootprintBankLinking` view component. Provide the `authToken` using the `token` obtained from the previous step, along with a `redirectUri` that includes both the scheme and host of your application (e.g., a custom URL scheme configured in your app's `Info.plist`). ```swift FootprintBankLinking( authToken: authToken, redirectUri: "footprintcomponentsdemo://banklinking", onSuccess: { response in print("Bank linking completed successfully, validation token: \(response.validationToken)") }, onError: { error in // Called when an error occurs print("Error occurred during bank linking: \(error)") }, onClose: { // Called when user closes the flow or the flow closes due to an error. If the flow closes due to an error, it will also call the onError callback print("Bank linking exited") } ) ``` The approach above lets you run bank linking separately using `FootprintBankLinkingWithAuthToken`. Just make sure it doesn’t run at the same time as any exisiting onboarding flow created using our Swift Onboarding Components SDK—since both use the same underlying object, it’s best to run them one after the other. ## Android To integrate bank linking into your Android application, install our [Android SDK](/articles/sdks/android-introduction#installation) and launch the bank linking flow using the `token` obtained from the previous step. Use the `FootprintBankLinking.launchWithAuthToken` method, providing the `authToken`, activity `context`, and relevant callbacks: ```kotlin Button( onClick = { coroutineScope.launch { try { FootprintBankLinking.launch( obSessionToken = "obtok_VlGKyL3AF7HDQfgx0j223RmNEmwNadRWn7", // Use your auth token here context = context, onSuccess = { val validationToken = it.validationToken println("Bank linked. Validation token: $validationToken") }, onError = { error -> // Called when an error occurs println("Error linking bank: ${error.message}") }, onClose = { // Called when user closes the flow or the flow closes due to an error. If the flow closes due to an error, it will also call the onError callback println("User exited bank linking") }, onEvent = { event -> // Called when an event occurs in the bank linking flow println( "Bank linking event: " + "name: ${event.name}, " + "link type: ${event.meta.linkType}, " + "institution name: ${event.meta.institutionName}, " + "institution id: ${event.meta.institutionId}, " + "timestamp: ${event.meta.timestamp}, " + "properties: ${event.properties} " ) } ) } catch (e: FootprintException) { println("Error initializing Footprint SDK: ${e.message}") } } } ) { Text("Link Bank Account") } ``` ### Handling Process Death If Android terminates your app's process during the OAuth flow, you need to resume the bank linking session when the OAuth redirect returns. Override the `onNewIntent` method in your activity to check for the intent extra `"FOOTPRINT_BANK_LINKING_STATUS"`. If the value matches `FootprintBankLinkingFlowStatus.PENDING.value`, call `FootprintBankLinking.resumePendingLinking` with the same callbacks. Here's an example: ```kotlin class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { OnboardingComponents(context = this) } } private fun handleResumeFootprintBAL(intent: Intent){ val balStatus = intent.getStringExtra("FOOTPRINT_BANK_LINKING_STATUS") println("Received new intent with BAL status: $balStatus") if(balStatus != null && balStatus == FootprintBankLinkingFlowStatus.PENDING.value) { FootprintBankLinking.resumePendingLinking( context = this, onSuccess = { val validationToken = it.validationToken println("Bank linked. Validation token: $validationToken") }, onError = { error -> println("Error linking bank: ${error.message}") }, onClose = { println("User exited bank linking") }, onEvent = { event -> println( "Bank linking event: " + "name: ${event.name}, " + "link type: ${event.meta.linkType}, " + "institution name: ${event.meta.institutionName}, " + "institution id: ${event.meta.institutionId}, " + "timestamp: ${event.meta.timestamp}, " + "properties: ${event.properties} " ) } ) } } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) handleResumeFootprintBAL(intent) } } ``` The approach above lets you run bank linking separately using `FootprintBankLinking.launchWithAuthToken`. Just make sure it doesn’t run at the same time as any exisiting onboarding flow created using our Android Onboarding Components SDK—since both use the same underlying object, it’s best to run them one after the other. ## Web Make sure to have the `@onefootprint/footprint-js` package installed (version 5.0.0 or higher): ```bash npm install @onefootprint/footprint-js ``` ```typescript import { onboarding } from "@onefootprint/footprint-js"; onboarding.initialize({ onboardingSessionToken: "obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH", onComplete: (validationToken) => { console.log(validationToken); }, onError: (error) => { console.log(error); }, onAuth: (validationToken) => { console.log(validationToken); }, onCancel: () => { console.log("User canceled the flow"); }, onClose: () => { console.log("User closed the flow"); }, }); ``` # Validation token After the SDK completes and calls the success completion handler, it will pass back a `validation_token` to you that you'll need to send to your backend and validate with the Footprint API. ```sh curl -X POST https://api.onefootprint.com/onboarding/session/validate \ -u : \ -d '{"validation_token": ""}' ``` This API call redeems the validation token from the SDK and lets you confirm that the link was completed successfully: ```json { "user": { "fp_id": "fp_id_GSxJr68GAf5jUT3pdL9ndjf7TLkA3GCX", "onboarding_id": "ob_SRFT2a1mN7DAWJ0VPXkiqK", "playbook_key": "pb_test_VMooXd04EUlnu3AvMYKjMW", }, "bank_link": { "link_id": "bank_link_xyz123...xyz321" } ... } ``` Use (and store in your own database if needed) the `link_id` returned above in the [bank linking APIs](#ap-is-and-vaulting-ap-is) to fetch data about the linked bank account(s). Note: a single bank link may connect more than one bank accounts, see the APIs linked below for the full data model accessible by a bank link. # Webhooks When a bank link connection is created or changed, Footprint sends the `footprint.user.bank_link_updated` event. Setup webhooks and inspect the webhook event payload documentation in the [webhooks tab](https://dashboard.onefootprint.com/webhooks) on the dashboard. # APIs and Vaulting Footprint provides a set of APIs to interact with bank links, accounts, and transactions. Additionally, bank account data is automatically vaulted when the link is established. ## Bank linking terminology | Name | Description | | --------- | -------------------------------------------------------------------------------- | | `link` | A link to an institution for which the user has connected one or more `accounts` | | `account` | A bank account like a checking or savings account connected via a `link` | ## APIs Footprint provides a set of APIs to get information about bank links, associated accounts, balances, and transactions. For the full API specification, take a look at the [Bank Linking API reference](/api-reference#bank-linking). ## Vaulting Once linked, a bank account's sensitive data is automatically fetched and vaulted using the `bank` account data identifier defined in the [Bank account vault fields](/articles/vault/fields#bank-account-fields). The bank `account_id` is used as the alias for the Bank account. For instance, if a the link has an account with identifier `acc_xyz1234`, then the ACH account number would be vaulted under data identifier `bank.acc_xyz1234.ach_account_number`. --- # Custom Statuses Custom statuses let you model **long-lived, domain-specific state** for users and businesses in Footprint. They provide a clean, auditable way to represent concepts like verification levels, compliance states, or business-specific classifications—without having to infer state from playbook runs or decisions. ## How Custom Statuses Work Custom statuses are **customer-defined enumerations** attached to an entity (user or business). Each custom status: * Has a **kind** (for example, `id_verification_status`) * Can hold **one value at a time** (such as `verified`, `pending`, or `rejected`), or be unset * Is **fully auditable** — Footprint maintains an append-only log of all changes * Is independent of playbooks, decisions, and signals ### Key Characteristics **State, Not Events** * Custom statuses represent the **current state** of an entity * Unlike playbook outcomes or decisions (which are event-based), statuses persist over time * Ideal for tracking verification levels, eligibility, or compliance posture **Strongly Typed & Predefined** * Statuses must be defined before use (via dashboard) * Each status has a fixed set of allowed values * Prevents typos and enforces consistency across systems **Append-Only Semantics** * Statuses can be set, updated, or cleared * Every status change is recorded with a timestamp and actor * Previous values are never overwritten in the audit log ## Example Use Cases ### Verification Layers Model verification state that aggregates multiple checks: * `identity_verification_status`: `pending`, `verified`, `rejected` * `tax_verification_status`: `pending`, `verified`, `rejected` These statuses represent the *result* of multiple underlying signals or playbooks. ### Compliance States Track regulatory or risk-related classifications: * `kyc_status`: `pending`, `verified`, `rejected` * `aml_status`: `clear`, `flagged`, `reviewed` ### Business-Specific Logic Represent application-specific concepts: * `account_tier`: `basic`, `premium`, `enterprise` * `risk_level`: `low`, `medium`, `high` ## Creating Custom Statuses Custom statuses are defined once and reused across playbooks and manual reviews. When creating a status, you define: * The **status kind** (e.g. `id_verification_status`) * The **entity type** it's applicable to (`user` or `business`) * The set of **allowed values** for the status Statuses must be created before they can be used in playbooks. You can create custom statuses in the [dashboard](https://dashboard.onefootprint.com/settings/configurations). ## Setting Statuses ### Setting a Status in a Playbook Playbooks are the primary way to set custom statuses automatically. To update a status from a playbook: 1. Add an **Action** node in the playbook builder 2. Select **Set Status** 3. Choose the status kind and value When the node executes during an onboarding, the entity's status will be updated. ### Updating a Status Manually Custom statuses can also be set or cleared on the Users/Businesses dashboard or Manual Review dashboard. This allows reviewers to override automated outcomes. ## API Access Custom statuses are returned on entity detail endpoints. View [User](/api-reference#get-users-fp-id) or [Business](/api-reference#get-businesses-fp-bid) API documentation to learn more. --- # Sandbox mode Footprint's sandbox mode allows you to test your integration without contacting live identity verification vendors and incurring cost. Sandbox mode is a testing environment that simulates creating real records without consulting live vendors. You can toggle between live mode and sandbox mode on the dashboard. Sandbox and live mode each have their own API keys, playbooks, and users. To start performing real KYC and KYB verifications, you must activate your account by contacting us. ## Sandbox IDs In live mode, users are generally unique by phone number. In sandbox mode, two users can share a phone number as long as they have different sandbox IDs. This allows you to create multiple test users with the same login credentials. And, by providing the login credentials and sandbox ID of an existing user, you can test flows where an existing user logs back into your app. You can see an existing user's sandbox ID on the user detail page of the dashboard. ## Mock outcome results When testing KYC and KYB in sandbox mode, Footprint doesn't contact live verification vendors. Instead, after onboarding onto the playbook you choose how the verification step resolves: * **Emulate an outcome** — pick a mock outcome such as **Success**, **Fail**, **Manual review**, or **Step up**. This applies the outcome directly and short-circuits your playbook's rules, which is useful when you just want to land a user in a specific state. * **Evaluate rules** — run your playbook's rules as normal against a set of mocked risk signals you select. Use this to test the rules themselves: the rules engine evaluates the mocked signals and produces the resulting outcome. For example, you might emulate a manual review to land in that state directly, or evaluate rules with a watchlist hit raised to confirm your rules route the user the way you expect. ## Fixture contact info When testing onboarding flows in sandbox mode, it may be tedious to receive one-time passcodes (OTPs) to your phone / email. So, we have provided a set of phone numbers and emails that may be used to log into sandbox user accounts. | Type | Fixture contact info | Fixture OTP | | ----- | ---------------------------------------------------------------------------------- | ----------- | | Phone | `+1 (555) 555-0100` or `+1 (555) 555-0111` | `000000` | | Email | `sandbox@onefootprint.com` or any `@example.com` address, like `piip@example.com`. | `000000` | Sandbox contact info is only valid in sandbox mode. Don't use them in live mode. --- # Introduction and Security Model ## Overview Vault Disaster Recovery (DR) is an enterprise feature that provides organizations with increased control over their data vaulted with Footprint in case of an emergency while maintaining high security and low risk. With Vault Disaster Recovery enabled, Footprint continuously backs up vaulted data to customer-owned cloud storage (like Amazon S3) in an encrypted format. During normal operation, the organization maintains low risk, as access to the vaulted data is protected and audited by the Footprint platform. However, in a catastrophic situation, if the customer wishes to "break glass" and gain access to their data without using Footprint services, Footprint or an escrow will securely disclose a payload that can be used to decrypt the cloud storage backups. ## How it Works Vault Disaster Recovery is based on the [age](https://age-encryption.org/) encryption format and a cryptographic key structure that enables targeted testing of the recovery mechanism without compromising the long-term confidentiality of the data. age is a modern file encryption format with multiple pluggable recipients, and seekable streaming encryption. No system or person has the ability to unilaterally decrypt Vault Disaster Recovery backups. In particular: * Footprint itself cannot decrypt, or even read, vaulted data written to the customer’s cloud storage. * The customer can decrypt individual Vault DR records to test recovery functionality via an audited and access-controlled API, similar to Footprint Vault’s standard decryption API. * The customer cannot decrypt Vault DR records outside the Footprint platform until Footprint or an escrow discloses a "recovery payload." These properties are made possible through a simple protocol using three different kinds of cryptographic keys. ### 1. Org Keys The customer generates one or more org-scoped key pairs during the onboarding process. These keys are stored on YubiKeys using P-256 elliptic curve cryptography. Other private keys are encrypted to (or "wrapped" by) the org public keys, which enables Footprint to securely store and transfer those private keys to the customer without maintaining unused long-term access to them. The customer maintains access to the org private keys, which can be used in cooperation with Footprint or an escrow to recover data from the cloud storage bucket. ### 2. Record Keys Records in the cloud storage bucket are each encrypted with a record-specific record key pair. Footprint encrypts the record private key to the org public key, stores the wrapped record key internally, and immediately discards the record private key. Then, Footprint encrypts the vaulted data to the record public key and writes the result to cloud storage. The customer can test decryption of individual records by using a Footprint API to securely request access to the encrypted record key for a given record. This action is audited and controlled similar to a standard Footprint Vault decryption request. Only the customer can use the org private key to unwrap the record key, which can in turn decrypt the single record. ### 3. Recovery Key Pair Using age’s multi-recipient hybrid encryption, Footprint also encrypts each file in the cloud storage bucket to the recovery public key. The X25519 recovery key pair is generated by Footprint during the onboarding process. Footprint encrypts the recovery private key to the org public key and immediately discards the recovery private key. Depending on the customer’s wishes, either 1. Footprint stores the wrapped recovery private key (known as the "recovery payload") so Footprint can work with the customer to trigger Disaster Recovery, or 2. A third-party escrow stores the recovery payload so the customer can trigger Disaster Recovery without the involvement of Footprint. In either case, the customer does not store the recovery payload themselves to avoid the risk of any single person or system having the ability to have access to both the recovery private key and data encrypted to this key. ## Security Model The table below outlines the different conditions or capabilities necessary for vault decryption in the three possible flows: Online Decrypt API Test Recovery Full Recovery Footprint API is online Required Required Footprint API key with decryption scopes Required Required Read access to backup S3 bucket Required Required Org private key Required Required Wrapped record keys(fetched via audited testing API) Required Recovery payload(held by Footprint or an escrow) Required Critically, the Test Recovery flow does not relax the security provided by [Footprint’s Enclave](https://onefootprint.com/blog/inside-the-enclave-part-1), since the decryption flow is audited in the same ways as the standard online Vault decryption APIs, and requires the same level of API key access. Prior to a Full Recovery, neither Footprint nor the customer has the required components to gain offline access to decrypted data via the Full Recovery flow. Spreading the components required for Full Recovery across multiple parties reduces the impact of an adversary gaining access to any given component. It also significantly increases the cost and complexity of an attack, as multiple organizations would need to be compromised to decrypt data offline. --- # Onboarding ## Prerequisites * You must have an AWS account and permissions to configure AWS resources. * You must have the Admin role in the Footprint dashboard. * You must have a YubiKey that supports the PIV card interface over USB. A [YubiKey 5C](https://www.yubico.com/product/yubikey-5-series/yubikey-5c/) should suffice. Amazon often has the [YubiKey 5C NFC](https://www.amazon.com/Yubico-Two-factor-authentication-security-certified/dp/B08DHL1YDL) available for fast delivery. ## 1. Install Vault Disaster Recovery tools First install the CLI tool used for enrollment and decryption. On a Mac, run the following: ```bash brew install onefootprint/tap/footprint-dr ``` For other platforms, you can download releases from [here](https://github.com/onefootprint/footprint-dr-releases/releases). ## 2. Create an API key and log in with the CLI Go to [the API Keys page](https://dashboard.onefootprint.com/api-keys) on the Footprint dashboard and use the toggle in the top right corner to select *Sandbox* or *Live* mode, depending on which data set you would like to enroll. Create a new API key with an admin scope. Run `footprint-dr login [--sandbox/--live]` and paste the API key at the prompt. ```text filename="Example Output" $ footprint-dr login --live Enter Footprint Live API key: ``` ## 3. Create a bucket for encrypted data storage Footprint needs a dedicated customer-owned Amazon S3 bucket to store encrypted data. Create a new S3 bucket similar to the one in the Terraform example below. Ensure you are creating the bucket using the us-east-1 region for optimal performance. ```terraform resource "aws_s3_bucket" "fp_vault_data" { bucket = "acme-inc-footprint-vault-data" } resource "aws_s3_bucket_public_access_block" "fp_vault_data_public_access_block" { bucket = aws_s3_bucket.fp_vault_data.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } ``` We recommend that you create this S3 Bucket in a dedicated AWS account with tight access controls. Keeping access to this bucket to a minimum helps reduce the impact of unintentional leaks of an org private key. We also recommend that you enable CloudTrail audit logs for data events on this bucket. See the following example Terraform: ```terraform resource "aws_s3_bucket" "fp_vault_data_cloudtrail" { bucket = "acme-inc-footprint-vault-data-cloudtrail" } resource "aws_s3_bucket_public_access_block" "fp_vault_data_cloudtrail_pab" { bucket = aws_s3_bucket.fp_vault_data_cloudtrail.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } locals { fp_vault_data_cloudtrail_name = "footprint-vault-data-cloudtrail" } data "aws_iam_policy_document" "fp_vault_data_cloudtrail" { statement { sid = "AWSCloudTrailAclCheck" effect = "Allow" principals { type = "Service" identifiers = ["cloudtrail.amazonaws.com"] } actions = ["s3:GetBucketAcl"] resources = [aws_s3_bucket.fp_vault_data_cloudtrail.arn] condition { test = "StringEquals" variable = "aws:SourceArn" values = ["arn:${data.aws_partition.current.partition}:cloudtrail:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:trail/${local.fp_vault_data_cloudtrail_name}"] } } statement { sid = "AWSCloudTrailWrite" effect = "Allow" principals { type = "Service" identifiers = ["cloudtrail.amazonaws.com"] } actions = ["s3:PutObject"] resources = ["${aws_s3_bucket.fp_vault_data_cloudtrail.arn}/AWSLogs/${data.aws_caller_identity.current.account_id}/*"] condition { test = "StringEquals" variable = "s3:x-amz-acl" values = ["bucket-owner-full-control"] } condition { test = "StringEquals" variable = "aws:SourceArn" values = ["arn:${data.aws_partition.current.partition}:cloudtrail:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:trail/${local.fp_vault_data_cloudtrail_name}"] } } } resource "aws_s3_bucket_policy" "fp_vault_data_cloudtrail" { bucket = aws_s3_bucket.fp_vault_data_cloudtrail.id policy = data.aws_iam_policy_document.fp_vault_data_cloudtrail.json } resource "aws_cloudtrail" "fp_vault_data_cloudtrail" { name = local.fp_vault_data_cloudtrail_name s3_bucket_name = aws_s3_bucket.fp_vault_data_cloudtrail.id include_global_service_events = false depends_on = [aws_s3_bucket_policy.fp_vault_data_cloudtrail] event_selector { read_write_type = "All" include_management_events = true data_resource { type = "AWS::S3::Object" values = ["${aws_s3_bucket.fp_vault_data.arn}/"] } } } ``` ## 4. Fetch your external ID Your [external ID](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html) will help secure the cross-account IAM access. Run `footprint-dr get-external-id [--sandbox/--live]` to fetch it. ```text filename="Example Output" $ footprint-dr get-external-id --live 42ee4f928973996f8f855aebaebf70cd ``` ## 5. Create an IAM role for bucket management Footprint needs read and write access to the bucket to manage the encrypted data. Create a IAM role to delegate access like the one in the Terraform example below, substituting your external ID. ```terraform locals { external_id = "42ee4f928973996f8f855aebaebf70cd" } resource "aws_iam_role" "fp_vault_data_management" { name = "fp-vault-data-management" assume_role_policy = data.aws_iam_policy_document.fp_vault_data_management_assume_role_policy.json } data "aws_iam_policy_document" "fp_vault_data_management_assume_role_policy" { statement { actions = ["sts:AssumeRole"] principals { type = "AWS" identifiers = ["725896863556"] } condition { test = "StringEquals" variable = "sts:ExternalId" values = [local.external_id] } } } data "aws_iam_policy_document" "fp_vault_data_management_policy" { statement { sid = "AllowPutObject" actions = [ "s3:PutObject", ] resources = [ "${aws_s3_bucket.fp_vault_data.arn}/*", ] } statement { sid = "AllowListBucket" actions = [ "s3:ListBucket", ] resources = [ aws_s3_bucket.fp_vault_data.arn, ] } statement { sid = "AllowGetBucketLocation" actions = [ "s3:GetBucketLocation", ] resources = [ aws_s3_bucket.fp_vault_data.arn, ] } } resource "aws_iam_policy" "fp_vault_data_management" { name = "fp-vault-data-management" policy = data.aws_iam_policy_document.fp_vault_data_management_policy.json } resource "aws_iam_role_policy_attachment" "fp_vault_data_management_assume_role" { role = aws_iam_role.fp_vault_data_management.name policy_arn = aws_iam_policy.fp_vault_data_management.arn } ``` ## 6. Generate your Disaster Recovery org key pair Org identities (private keys) are stored on one or more [YubiKeys](https://www.yubico.com/products/yubikey-5-overview/). Using hardware security tokens helps virtually eliminate the risk of accidentally leaking a private key, which is a sensitive component of the recovery flow. Depending on your data durability requirements, you may choose to mitigate the risk of lost or damaged hardware by registering redundant YubiKeys. The [age YubiKey plugin](https://github.com/str4d/age-plugin-yubikey) helps makes the enrollment process simple. Install [age](https://github.com/FiloSottile/age) and [age-plugin-yubikey](https://github.com/str4d/age-plugin-yubikey) on a workstation with a USB port for the YubiKey. ```bash # On a Mac: brew install age age-plugin-yubikey ykman ``` Plug in the YubiKey. Now, change the management key by running the following command. Press the enter key to use the default management key, and enter the default YubiKey pin of 123456 at the prompt. ```bash ykman piv access change-management-key -a TDES --protect ``` Then, run a command like the following to generate an age identity for your org. Adjust the name and mode (sandbox/live) for your own reference. You will be prompted to change your PIN/PUK. Choose a PIN and retain it in your password manager of choice. The PIN will be necessary to use your YubiKey to decrypt your Vault Disaster Recovery backups. ```bash age-plugin-yubikey \ --generate \ --name "Footprint Vault DR org age identity: Acme Inc. Live" \ --pin-policy once \ --touch-policy cached \ --slot 1 ``` You may choose a different slot number if slot 1 is already in use, though we advise using dedicated YubiKeys for live-mode Vault Disaster Recovery. We recommend the pin policy of `once` and the touch policy of `cached` to improve the experience of the test recovery flow. You do not need to save any of the output, but make note of the recipient (starting with `age1yubikey`) which you will paste into the next step. You can retrieve the recipient again by running the following: ```bash age-plugin-yubikey --list ``` Repeat the key generation step once for each YubiKey you wish to register for Vault Disaster recovery, collecting a list of your org’s `age1yubikey` age recipients. ## 7. Complete enrollment Run `footprint-dr enroll [--sandbox/--live]` and follow the prompts. ```text filename="Example Output" $ footprint-dr enroll --live Enrolling Acme Inc. (Live) in Vault Disaster Recovery. Enter org public key (age recipient): age1yubikey1qgceu0h4fzsv46jg32gnfz0hf5lnaaqm8wn8skxf33qm0t4v4rz427fh79x Add another org public key? [y/n] y Enter org public key (age recipient): age1yubikey1q2eggw2hftplqfr27s9h8nwuez39m45ms6qv78m9m6kfhmsyf6gacj2km7u Add another org public key? [y/n] n Enter AWS Account ID: 123456789012 Enter AWS role name: acme-inc-footprint-disaster-recovery Enter S3 bucket name: acme-inc-footprint-encrypted-data Verifying configuration... OK Enrollment complete. Store the following information to locate your encrypted data: S3 Bucket Name: acme-inc-footprint-disaster-recovery Bucket Namespace: a39evoii5rgqhdz4jansho3tten4z0oz ``` Unplug the YubiKey, clearly label it, and store it in a physically secure location (e.g. an office safe). You will need to retrieve the YubiKey to decrypt Disaster Recovery data. Store the printed `S3 Bucket Name` and `Bucket Namespace` in your records to ensure you can locate your Disaster Recovery data. --- # Operations ## Checking Backup Status Run `footprint-dr status [--sandbox/--live]` to check the current status of your Vault Disaster Recovery backups. ```text filename="Example Output" $ footprint-dr status --live Logged in to Acme Inc. (Live) Enrolled in Vault Disaster Recovery since: 2024-04-17 23:22:42.272984 UTC Organization Public Keys: age1yubikey1qgceu0h4fzsv46jg32gnfz0hf5lnaaqm8wn8skxf33qm0t4v4rz427fh79x age1yubikey1q2eggw2hftplqfr27s9h8nwuez39m45ms6qv78m9m6kfhmsyf6gacj2km7u Storage Configuration: AWS Account ID: 123456789012 AWS Role Name: acme-inc-footprint-disaster-recovery S3 Bucket Name: acme-inc-footprint-encrypted-data Bucket Namespace: a39evoii5rgqhdz4jansho3tten4z0oz Latest Backup Record Timestamp: 2024-04-29T22:27:21Z Backup Lag: 7 seconds ``` Once the initial backup is complete, the expected lag should be at most several minutes. ## Inspecting Unencrypted Metadata Using one of the [standard AWS CLI login methods](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html), log in to an AWS role that has `s3:ListBucket` and `s3:GetObject` access on the backup bucket. To inspect the list of vaults in your backup bucket, run `footprint-dr list-vaults [--sandbox/--live]`. ```text filename="Example Output" $ footprint-dr list-vaults --live fp_id_4acxG4NxFTlGaE6b2WyDIc fp_id_WFVkUyTeuWNy0GlrOPdPBO fp_id_rwSYNkPIrDFogXYjCJ1KrQ ... ``` To list all records, run `footprint-dr list-records [--sandbox/--live]`. ```text filename="Example Output" $ footprint-dr list-records --live {"fp_id": "fp_id_rwSYNkPIrDFogXYjCJ1KrQ", "version": 2, "fields": ["id.first_name", "id.last_name", "id.phone_number"]} {"fp_id": "fp_id_4acxG4NxFTlGaE6b2WyDIc", "version": 1, "fields": ["id.first_name", "id.last_name", "id.phone_number"]} {"fp_id": "fp_id_WFVkUyTeuWNy0GlrOPdPBO", "version": 4, "fields": ["id.first_name", "id.last_name", "id.phone_number"]} ... ``` For manual pagination on either command, use the `--fp-id-gt` and `--limit` flags. Pass `--sandbox` to either command to inspect the sandbox dataset. To locate the bucket and namespace where the encrypted data is stored, by default these commands use the Footprint API. To bypass this dependency on the Footprint API, provide the `--bucket` and `--namespace` flags using the values from enrollment or from `footprint-dr status`. ## Testing Recovery Flow Using one of the [standard AWS CLI login methods](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html), log in to an AWS role that has `s3:ListBucket` and `s3:GetObject` access to the backup bucket. Prepare a [line-separated JSON file](https://jsonlines.org/) `records.jsonl` that specifies which records you would like to decrypt for the test. For example: ```jsonl {"fp_id": "fp_id_rwSYNkPIrDFogXYjCJ1KrQ", "version": 2, "fields": ["id.first_name", "id.last_name", "id.phone_number"]} {"fp_id": "fp_id_4acxG4NxFTlGaE6b2WyDIc", "version": 1, "fields": ["id.first_name", "id.last_name", "id.phone_number"]} {"fp_id": "fp_id_WFVkUyTeuWNy0GlrOPdPBO", "version": 4, "fields": ["id.first_name", "id.last_name", "id.phone_number"]} ``` For testing, decrypt just enough records to gain confidence that the backup recovery mechanism is functional. The output of `footprint-dr list-records --live --sample --limit 50` will likely suffice. Using one of the [standard AWS CLI login methods](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html), log in to an AWS role that has `s3:ListBucket` and `s3:GetObject` access on the backup bucket. Ensure you are logged in to the `footprint-dr` CLI with an API key that has an appropriate `Decrypt Data` scope. Plug in your YubiKey and create an age identity file on disk that points to the YubiKey slot you used during enrollment. If you don’t remember the slot number, run `age-plugin-yubikey --list`. ```bash age-plugin-yubikey --identity --slot 1 > org-identity.txt ``` Run `footprint decrypt` to test recovery of the desired records, like the following. ```bash footprint-dr decrypt \ --live \ --records records.jsonl \ --org-identity org-identity.txt \ --output-dir /tmp/decrypt-output ``` Tap your YubiKey when prompted. This fetches the encrypted records from S3, fetches the wrapped record keys from Footprint’s testing API, unwraps each record key using your YubiKey, and decrypts the records using those record keys. ## Full Recovery Flow In the catastrophic event a full recovery is needed, Footprint or the pre-arranged escrow will transfer the recovery payload to you, a file like `acme-inc-wrapped-recovery-key.age`. Take care to prevent leakage. Prepare a [line-separated JSON file](https://jsonlines.org/) `records.jsonl` that specifies what records you would like to decrypt. For example: ```jsonl {"fp_id": "fp_id_rwSYNkPIrDFogXYjCJ1KrQ", "version": 2, "fields": ["id.first_name", "id.last_name", "id.phone_number"]} {"fp_id": "fp_id_4acxG4NxFTlGaE6b2WyDIc", "version": 1, "fields": ["id.first_name", "id.last_name", "id.phone_number"]} {"fp_id": "fp_id_WFVkUyTeuWNy0GlrOPdPBO", "version": 4, "fields": ["id.first_name", "id.last_name", "id.phone_number"]} ``` Using one of the [standard AWS CLI login methods](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html), log in to an AWS role that has `s3:ListBucket` and `s3:GetObject` access on the backup bucket. Plug in your YubiKey and create an age identity file on disk that points to the YubiKey slot you used during enrollment. If you don’t remember the slot number, run `age-plugin-yubikey --list`. Then run a `footprint-dr decrypt` command providing the `--wrapped-recovery-key` flag. For example, to decrypt all records with no dependency on the Footprint API, run a command like the following: ```bash footprint-dr decrypt \ --live \ --all \ --wrapped-recovery-key acme-inc-wrapped-recovery-key.age \ --org-identity org-identity.txt \ --bucket acme-inc-footprint-encrypted-data \ --namespace a39evoii5rgqhdz4jansho3tten4z0oz \ --output-dir /tmp/pii ``` Tap your YubiKey when prompted. This unwraps the recovery private key using your YubiKey, fetches the encrypted records from S3, and decrypts the records using the recovery private key. Rather than decrypting all records with the `--all` flag, customers with many records may prefer to batch or even parallelize the decryptions using multiple invocations with the `--records` flag using record batches from `footprint-dr list-records`. To maximize download and decryption speed, you can adjust the `--concurrency-limit` flag on `footprint-dr decrypt` . The default is `2 * number of CPUs`. Adjust according to the measured download speed in the output. Setting too high of a concurrency limit may be detrimental to performance or yield open file limit errors. --- # Footprint API Footprint offers a rich set of APIs to interact with the users that have onboarded onto your product. Our API has predictable resource-oriented URLs. It generally accepts JSON-encoded request bodies and returns JSON-encoded responses. The API uses standard HTTP response codes, authentication, and verbs. You can use the Footprint API in sandbox mode, which doesn't affect your live data, incur costs, or interact with production identity verification vendors. The API key that you use to authenticate determines whether the request is made in sandbox mode or live mode. The Footprint API is accessible at the base URL `https://api.onefootprint.com`. For more information on APIs available, see our [API reference](/api-reference). --- # Webhooks When integrating with Footprint, you may want your application to receive events as they occur in Footprint so your backend system can react accordingly. At their core, webhooks are just a POST request sent to a pre-determined endpoint that you configure. Footprint uses HTTPS to send webhook events to your app as a JSON payload. By returning an HTTP 2xx, your endpoint indicates that the webhook has been processed. To enable webhook events, you need to register webhook endpoints in the [Footprint dashboard](https://dashboard.onefootprint.com/webhooks). After you register them, Footprint can push real-time event data to your application's webhook endpoint when events happen in Footprint. # Getting started To get started, see the [guide to consuming webhooks](https://docs.svix.com/receiving/introduction). We recommend that you secure your integration by always verifying that all webhook requests are generated by Footprint. Please see our guide on [securely verifying webhook signatures](https://docs.svix.com/receiving/verifying-payloads/how). # Recommended integration When integrating with Footprint's frontend SDKs for collecting and verifying user information, you most commonly may want to subscribe to events related to a user's status. ## Onboarding completed We recommend subscribing to the footprint.onboarding.completed event, which will fire when a user's onboarding has reached a terminal status. In most cases, the [status received synchronously](/articles/guide/definitive-integration-guide#the-end-to-end-integration-step-5-handle-the-decision) at the end of Footprint's onboarding flow will be a terminal `pass`, `fail`, or `none`. In some cases where identity verifications vendors take longer to verify a user, you will synchronously receive a `pending` status in your integration with the frontend SDK. In these cases, you will receive the final decision for an onboarding through the `footprint.onboarding.completed` event. ## Manual review Employees at your company may change a user's status while manually reviewing a user in the Footprint dashboard. This will fire the footprint.user.manual\_review event. Upon receiving this webhook, we recommend consulting the GET /users/{fp_id} API for information on the user's new manual status. ## Watchlist checks If your playbook has continuous monitoring enabled, Footprint will regularly check if any of your users are found on AML watchlists. Updates on watchlist checks are sent using the footprint.watchlist\_check.completed event. # IP allowlist If your webhook endpoint is behind a firewall, you can allowlist the following IP addresses to ensure that Footprint's webhook events can reach your endpoint. All webhook requests originate from one of these addresses: ```text 44.228.126.217 50.112.21.217 52.24.126.164 54.148.139.208 2600:1f24:64:8000::/56 ``` --- # Customization ## Introduction Footprint supports visual customization, allowing you to match the design of your product with the `appearance` option. It's available in all SDKs (`javascript`, `react` and `react-native`). Do you want to see a project example? Go [here](https://github.com/onefootprint/examples/tree/master/idv/frontend-vite-vanilla). ## Variables The easiest way to create custom themes is by extending the theme variables. Under the hood, they are just CSS variables, so you can inspect the resulting DOM using the DOM explorer in your browser. Scroll down to see the full list of variables. Properties can be set using the unit that you prefer: pixels, rem, hex, rgb, etc. ```typescript import "@onefootprint/footprint-js/dist/footprint-js.css"; import { onboarding } from "@onefootprint/footprint-js"; onboarding.initialize({ onboardingSessionToken: "obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH", onComplete: (validationToken) => { console.log(validationToken); }, appearance: { variables: { borderRadius: "0px", buttonPrimaryBg: "#315E4C", buttonPrimaryHoverBg: "#46866c", buttonPrimaryColor: "#FFF", }, }, }); ``` ## Global variables In the Footprint SDK, visual token inheritance operates on a hierarchical system. A higher-level token can set default values for a series of lower-level tokens. The table below lists these higher-level tokens and the lower-level tokens they influence by default: | Higher-level Token | Lower-level Tokens Influenced | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `borderRadius` | `buttonBorderRadius`, `containerBorderRadius`, `inputBorderRadius`, `dropdownBorderRadius`, `radioSelectBorderRadius` | | `colorError` | `borderColorError`, `hintErrorColor`, `linkButtonDestructiveColor` | | `colorAccent` | `linkColor`, `linkButtonColor`, `linkButtonHoverColor`, `linkButtonActiveColor`, `radioSelectSelectedColor`, `radioSelectSelectedBorderColor`, `radioSelectComponentsIconSelectedBg` | | `borderColorError` | `inputErrorBorderColor` | You can define these tokens to easily propagate design choices across multiple lower-level tokens. However, should you need to customize a specific token, setting it individually will override the higher-level default. This allows you to maintain broad stylistic consistency across your interface while still providing granular control over specific components. ### Container Wraps the entire Footprint component. | Variable | Description | | ----------------------- | ---------------------------------------- | | `containerBg` | Sets the container body background color | | `containerElevation` | Determines the container's shadow | | `containerBorder` | Defines the container's border | | `containerBorderRadius` | Defines the container's border radius | | `containerMaxWidth` | Defines the container's max width | To customize the max width of the content for inline integrations, use the `containerMaxWidth` variable — for example, `containerMaxWidth: '480px'`. This constrains and centers the content while letting the outer container fill the available space. For modal and drawer variants, the width is predetermined. ### Link Links function like standard anchor tags. A common example is the `Terms of Service` link. To apply more properties or modify states, use custom rules. | Variable | Description | | ----------- | ---------------------- | | `linkColor` | Defines the link color | ### Label The label refers to the native HTML element that pairs with an input field. | Variable | Description | | ------------ | ------------------------- | | `labelColor` | Sets the label color | | `labelFont` | Determines the label font | ### Input This encompasses all types of input fields on the platform, including PIN, Phone, and Address Inputs. | Variable | Description | | ---------------------------- | -------------------------------------------------------------------------- | | `inputBorderRadius` | Determines the input border radius | | `inputBorderWidth` | Sets the input border width | | `inputFont` | Sets the input font | | `inputHeight` | Sets the input height | | `inputPlaceholderColor` | Defines the input placeholder color | | `inputColor` | Sets the input color | | `inputBg` | Defines the input background color | | `inputBorderColor` | Sets the input border color | | `inputHoverBg` | Determines the input background color when hovered | | `inputHoverBorderColor` | Sets the input border color when hovered | | `inputFocusBg` | Determines the input background color when focused | | `inputFocusBorderColor` | Sets the input border color when focused | | `inputFocusElevation` | Sets the input box shadow when focused | | `inputErrorBg` | Determines the input background color when an error is present | | `inputErrorBorderColor` | Sets the input border color when an error is present | | `inputErrorHoverBg` | Determines the input background color when an error is present and hovered | | `inputErrorHoverBorderColor` | Sets the input border color when an error is present and hovered | | `inputErrorFocusBg` | Determines the input background color when an error is present and focused | | `inputErrorFocusBorderColor` | Sets the input border color when an error is present and focused | | `inputErrorFocusElevation` | Sets the input box shadow when an error is present and focused | ### Hint Hints are informative texts below input fields. They can indicate an error or provide additional information. | Variable | Description | | ---------------- | ---------------------------------------------------------------- | | `hintColor` | Sets the hint color | | `hintErrorColor` | Determines the hint color when the input field is in error state | | `hintFont` | Defines the hint font | ### LinkButton A LinkButton is a variant of our standard button but without any backgrounds. | Variable | Description | | ---------------------------------- | ------------------------------------------------------------------------------ | | `linkButtonColor` | Sets the button color | | `linkButtonHoverColor` | Defines the button color when hovered | | `linkButtonActiveColor` | Sets the button color when pressed | | `linkButtonDestructiveColor` | Determines the button color when it signifies a destructive action | | `linkButtonDestructiveHoverColor` | Sets the button color when it signifies a destructive action and is hovered | | `linkButtonDestructiveActiveColor` | Determines the button color when it signifies a destructive action and pressed | ### Button | Variable | Description | | ------------------------------------ | ------------------------------------------------------------- | | `buttonBorderRadius` | Defines the button border radius | | `buttonBorderWidth` | Sets the button border width | | `buttonElevation` | Determines the button box shadow | | `buttonElevationHover` | Sets the button box shadow when hovered | | `buttonElevationActive` | Determines the button box shadow when activated | | `buttonOutlineOffset` | Defines the button outline offset | | `buttonPrimaryBg` | Sets the primary button's background color | | `buttonPrimaryColor` | Defines the primary button's color | | `buttonPrimaryBorderColor` | Determines the primary button's border color | | `buttonPrimaryHoverBg` | Sets the primary button's background color when hovered | | `buttonPrimaryHoverColor` | Defines the primary button's color when hovered | | `buttonPrimaryHoverBorderColor` | Determines the primary button's border color when hovered | | `buttonPrimaryActiveBg` | Sets the primary button's background color when activated | | `buttonPrimaryActiveColor` | Defines the primary button's color when activated | | `buttonPrimaryActiveBorderColor` | Determines the primary button's border color when activated | | `buttonPrimaryDisabledBg` | Sets the primary button's background color when disabled | | `buttonPrimaryDisabledColor` | Defines the primary button's color when disabled | | `buttonPrimaryDisabledBorderColor` | Determines the primary button's border color when disabled | | `buttonPrimaryLoadingBg` | Sets the primary button's background color when loading | | `buttonPrimaryLoadingColor` | Defines the primary button's color when loading | | `buttonSecondaryBg` | Sets the secondary button's background color | | `buttonSecondaryColor` | Defines the secondary button's color | | `buttonSecondaryBorderColor` | Determines the secondary button's border color | | `buttonSecondaryHoverBg` | Sets the secondary button's background color when hovered | | `buttonSecondaryHoverColor` | Defines the secondary button's color when hovered | | `buttonSecondaryHoverBorderColor` | Determines the secondary button's border color when hovered | | `buttonSecondaryActiveBg` | Sets the secondary button's background color when activated | | `buttonSecondaryActiveColor` | Defines the secondary button's color when activated | | `buttonSecondaryActiveBorderColor` | Determines the secondary button's border color when activated | | `buttonSecondaryDisabledBg` | Sets the secondary button's background color when disabled | | `buttonSecondaryDisabledColor` | Defines the secondary button's color when disabled | | `buttonSecondaryDisabledBorderColor` | Determines the secondary button's border color when disabled | | `buttonSecondaryLoadingBg` | Sets the secondary button's background color when loading | | `buttonSecondaryLoadingColor` | Defines the secondary button's color when loading | Please note that MDX (Markdown for the Component Era) allows you to import and use JSX components in your Markdown files. In my above example, I treated this like a regular markdown table, but you can also use JSX syntax if you want to create more complex and interactive tables. ### Dropdown Dropdowns are used with input fields. When it's a phone input, it's used to display the list of countries. When it's an address input, it's used to display the list of results. | Variable | Description | | ------------------------ | -------------------------------------------------- | | `dropdownBg` | The dropdown background color | | `dropdownHoverBg` | The dropdown background color, when it's hovered | | `dropdownBorderColor` | The dropdown background color, when it's activated | | `dropdownBorderWidth` | The dropdown border width | | `dropdownBorderRadius` | The dropdown border radius | | `dropdownElevation` | The dropdown box shadow | | `dropdownColorPrimary` | The dropdown color primary | | `dropdownColorSecondary` | The dropdown color secondary | | `dropdownFooterBg` | The dropdown footer background | ### Radio Select Radio Select is used in two important areas within the Footprint interface. Firstly, it is employed when selecting an outcome for onboarding with a sandbox key. In this case, the chosen outcome is only visible in sandbox mode and remains hidden from end users. Secondly, Radio Select is utilized during the ID document verification process, allowing users to select the type of ID document they wish to use for verification. | Variable | Description | | --------------------------------------------- | ------------------------------------------------------ | | `radioSelectBg` | The background of the radio select component | | `radioSelectColor` | The text color when is not selected | | `radioSelectHoverColor` | The hover text color when is not selected | | `radioSelectSelectedColor` | The text color when is selected | | `radioSelectSelectedHoverColor` | The hover text color when is selected | | `radioSelectBorderRadius` | The border radius of the radio select component | | `radioSelectBorderWidth` | The border width of the radio select component | | `radioSelectBorderColor` | The border color of the radio select component | | `radioSelectHoverBg` | The hover background of the radio select component | | `radioSelectHoverBorderColor` | The hover border color of the radio select component | | `radioSelectSelectedBg` | The background of the selected radio item | | `radioSelectSelectedBorderColor` | The border color of the selected radio item | | `radioSelectComponentsIconBg` | The background of the icon in the radio item | | `radioSelectComponentsIconHoverBg` | The hover background of the icon in the radio item | | `radioSelectComponentsIconSelectedBg` | The background of the selected icon in the radio item | | `radioSelectComponentsIconColor` | The color of the icon in the radio item | | `radioSelectComponentsIconHoverColor` | The hover color of the icon in the radio item | | `radioSelectComponentsIconSelectedColor` | The color of the selected icon in the radio item | | `radioSelectComponentsIconSelectedHoverColor` | The hover color of the selected icon in the radio item | ## Custom fonts Footprint supports custom fonts via [Google Fonts](https://fonts.google.com/). To use a custom font, pass a `fontSrc` URL in the `appearance` option. The SDK will automatically load the font and apply it across the component. We strongly recommend also setting `fontFamily` in `variables` to ensure the font is applied consistently. While the SDK attempts to extract the font family from the Google Fonts URL, explicitly setting `fontFamily` gives you full control over the value and fallback chain. ```typescript import "@onefootprint/footprint-js/dist/footprint-js.css"; import { onboarding } from "@onefootprint/footprint-js"; onboarding.initialize({ onboardingSessionToken: "obtok_...", onComplete: (validationToken) => { console.log(validationToken); }, appearance: { fontSrc: "https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap", variables: { fontFamily: "'Poppins', sans-serif", }, }, }); ``` We recommend using Google Fonts URLs for `fontSrc`. Other font sources may produce unexpected results. | Property | Location | Description | | ------------ | --------------------------------- | ---------------------------------------------------- | | `fontSrc` | `appearance.fontSrc` | A Google Fonts URL that loads the font stylesheet | | `fontFamily` | `appearance.variables.fontFamily` | The CSS `font-family` value applied to the component | ## Rules If you want to add styles that are not supported by these variables, you can use `rules`. With `rules`, you can use CSS to arbitrarily style the elements. ```typescript import '@onefootprint/footprint-js/dist/footprint-js.css'; import { onboarding } from '@onefootprint/footprint-js'; onboarding.initialize({ onboardingSessionToken: 'obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH', onComplete: (validationToken) => { console.log(validationToken); }, appearance: { rules: { button: { transition: 'all .2s linear', }, `button:hover`: {}, `button:focus`: {}, `button:active`: {}, `input`: {}, `input:hover`: {}, `input:focus`: {}, `input:active`: {}, `pinInput:hover`: {}, `pinInput:focus`: {}, `pinInput:active`: {}, `label`: {}, `hint`: {}, `link`: {}, `link:hover`: {}, `link:active`: {}, } }, }); ``` ### Available rules | Variable | Description | | --------------- | ------------------------------- | | `button` | The button | | `button:hover` | The button, when it's hovered | | `button:active` | The button, when it's activated | | `input` | The input | | `input:hover` | The input, when it's hovered | | `input:focus` | The input, when it's focused | | `label` | The input label | | `hint` | The hint | | `link` | The link | | `link:hover` | The link, when it's hovered | | `link:active` | The link, when it's activated | ## Localization configuration ### Setting the language & locale Footprint also supports localization settings, you can use the `l10n` (localization) option and specify the desired locale and language. For example, if you have Spanish speaking users, you can set the language to "es", and it will show the onboarding flow in Spanish. ```typescript import "@onefootprint/footprint-js/dist/footprint-js.css"; import { onboarding } from "@onefootprint/footprint-js"; const l10n = { language: "es", }; onboarding.initialize({ onboardingSessionToken: "obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH", onComplete: (validationToken) => { console.log(validationToken); }, l10n, }); ``` Similarly, if your audience is in Mexico, you can set the locale to "es-MX" (Spanish - Mexico). This locale configuration helps Footprint format dates and numbers and adapt to cultural conventions. ```typescript import "@onefootprint/footprint-js/dist/footprint-js.css"; import { onboarding } from "@onefootprint/footprint-js"; const l10n = { language: "es", locale: "es-MX", }; onboarding.initialize({ onboardingSessionToken: "obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH", onComplete: (validationToken) => { console.log(validationToken); }, l10n, }); ``` ### Available l10n properties | Property | Type | Description | | ---------- | ---------------- | ------------------------------------------- | | `locale` | `en-US`, `es-MX` | Optional. Locale for date/number formatting | | `language` | `en`, `es` | Language code for UI text localization | --- # Run a playbook The POST /onboardings API is the canonical way to run one of your playbooks on a user or business. Onboardings are created from your backend. The playbook executes as much as it can with any existing data in the entity's vault. The response carries either the decision or a `continue_onboarding` token that lets the user finish the remaining steps in a Footprint flow. This one API covers both integration shapes: * **Fully headless.** Your application already has all the data the playbook needs. Vault it, run the onboarding, and receive the decision with no Footprint UI. * **Finish in a Footprint flow.** You expect to collect information from the user or verify documents. The run returns a `continue_onboarding` token: pass it to one of our frontend SDKs, or send the user the hosted link, and they complete the rest. For a step-by-step walkthrough of the full integration, see [The Integration Guide](/articles/guide/definitive-integration-guide). ## Quick start Create the user with POST /users (or a business with POST /businesses) and write the data you've already collected. Both calls are server-side APIs authenticated with your [secret API key](https://dashboard.onefootprint.com/api-keys); never send it to the client. ```bash curl -X POST https://api.onefootprint.com/users \ -u : \ -d '{ "id.first_name": "Jane", "id.last_name": "Doe", "id.dob": "1990-01-01", "id.ssn9": "123-45-6789", "id.address_line1": "1 Main St", "id.city": "San Francisco", "id.state": "CA", "id.zip": "94105", "id.country": "US" }' ``` See [Vault fields](/articles/vault/fields) for the full set of attributes you can vault, and [Migrating user data](/articles/integrate/migrate-existing-data) to bring over data you already hold. If you expect the user to provide most of their information through the interactive flow, it's fine to vault only what you have, or nothing at all. Then run a [playbook](https://dashboard.onefootprint.com/playbooks) on the user. Set `synchronous_timeout_secs` (max 30) to wait for the decision and receive it inline: ```bash curl -X POST https://api.onefootprint.com/onboardings \ -u : \ -d '{ "fp_id": "fp_id_K0q6Eh6Rr3WOOfFBLPiHsr", "key": "", "synchronous_timeout_secs": 30 }' ``` ```json { "id": "ob_SRFT2a1mN7DAWJ0VPXkiqK", "status": "pass", "requires_manual_review": false, "continue_onboarding": null, "error_message": null, "output": null } ``` Not every run finishes inline with a decision like this one: see [Processing the response](#processing-the-response) for handling `pending` and `incomplete` statuses. ## Request options Beyond the entity and the playbook key, the request accepts these options. ### Synchronous vs. asynchronous * **Synchronous:** set `synchronous_timeout_secs` (max 30). The call waits for the run to finish and returns the decision inline. If the playbook doesn't finish before the timeout, the response is `pending`, execution continues in the background, and the decision arrives via webhook. * **Asynchronous:** omit it. The call returns immediately with `status: "pending"` and the run executes in the background; the decision arrives via the `footprint.onboarding.completed` webhook. See [Webhooks](/articles/integrate/webhooks). Asynchronous runs are useful for latent operations, like an AI agent, that may not finish within the synchronous timeout. `continue_onboarding` is only returned on synchronous runs. Run synchronously whenever the user may need to finish the flow interactively. For an asynchronous run, [fetch the onboarding](#fetching-an-onboarding-later) once it stops on the user to pick up a token. ### External IDs If the user you created above has an external ID, you can provide `external_id` instead of `fp_id`. To keep a one-to-one mapping with your own records, set one by passing your own identifier as the `x-external-id` header when creating the entity. ### Reonboarding and idempotency By default, an entity may only onboard onto a playbook one time. This protects you from incurring accidental charges for repeat onboardings: running POST /onboardings again for the same entity and playbook returns a `409` error. `onboarding_external_id` gives you control over this behavior and associates an onboarding with an event in your application. If the entity already has an onboarding with the provided `onboarding_external_id`, that onboarding's result is returned without re-running anything. If not, a new onboarding is created and the entity reonboards. For example, if you'd like your users to reonboard every time they fill out a new account application in your product, provide the application's identifier: ```bash curl -X POST https://api.onefootprint.com/onboardings \ -u : \ -d '{ "fp_id": "fp_id_K0q6Eh6Rr3WOOfFBLPiHsr", "key": "", "onboarding_external_id": "bc13ca5b-210f-49af-9aba-e98db366484a", "synchronous_timeout_secs": 30 }' ``` Or, to allow users to reonboard onto the same playbook once every month, provide an `onboarding_external_id` that is a function of the current month, like `onboarding-2026-07`. We recommend choosing a value that implies some limit on how frequently an entity can reonboard, since you are responsible for the charges each reonboard incurs. External IDs may only include alphanumeric characters, `_`, `-`, or `.` and must be between 10 and 256 characters. ### Passing onboarding data If your playbook needs additional context from your backend, like details of the transaction that triggered the onboarding, configure a prerequisite node on the playbook and pass the data as `prerequisite_data`: ```bash curl -X POST https://api.onefootprint.com/onboardings \ -u : \ -d '{ "fp_id": "fp_id_K0q6Eh6Rr3WOOfFBLPiHsr", "key": "", "prerequisite_data": { "transaction_amount": 1000 }, "synchronous_timeout_secs": 30 }' ``` The data is available in template contexts and branch nodes under the `data.` prefix, for example `data.transaction_amount`. `prerequisite_data` is only accepted when the playbook has a prerequisite node. ## Processing the response Every run returns the same fields: | Field | Description | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | The onboarding's unique identifier. Use it to [fetch this run later](#fetching-an-onboarding-later), and to fetch its details, risk signals, and documents. | | `status` | The outcome of the run; see [Statuses](#processing-the-response-statuses). | | `requires_manual_review` | Whether the entity has an open manual review after the run. | | `error_message` | Present only when the run failed due to logic configured on your playbook. | | `continue_onboarding` | Present only when the run is `incomplete`: a `token`, a `link`, and `expires_at`. | | `output` | The values declared by your playbook's output node, once the run reaches it; see [Playbook output](#processing-the-response-playbook-output). | ### Collect information in a Footprint SDK When the playbook needs something only the user can provide, like a field the playbook collects that isn't vaulted or an identity document, the run stops with `status: "incomplete"` and a `continue_onboarding` object: ```json { "id": "ob_SRFT2a1mN7DAWJ0VPXkiqK", "status": "incomplete", "requires_manual_review": false, "error_message": null, "output": null, "continue_onboarding": { "token": "obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH", "link": "https://verify.onefootprint.com/?type=user#obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH", "expires_at": "2026-07-17T12:00-07:00" } } ``` The token resumes this exact onboarding: the user picks up where the headless run stopped, and the completed run keeps the same `id`. Treat the token as a secret, and use it before `expires_at` (12 hours). There are two ways to hand it to the user: * **Hosted:** send the `link` to your user via email, SMS, or a button in your app. * **Embedded:** pass the `token` into the SDK as the `onboardingSessionToken`: ```javascript import "@onefootprint/footprint-js/dist/footprint-js.css"; import { onboarding } from "@onefootprint/footprint-js"; onboarding.initialize({ onboardingSessionToken: "obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH", // continue_onboarding.token onComplete: () => { // the user has finished the remaining steps }, }); ``` When the user finishes, the SDK's `onComplete` handler fires. Read the result by fetching the onboarding with GET /onboardings/{id}, using the `id` you received when you created it. For iOS and Android examples, see the [Integration Guide](/articles/guide/definitive-integration-guide#the-end-to-end-integration-step-5-handle-the-decision). ### Statuses The `status` field carries the outcome of the run, one of: | Status | Meaning | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pass` / `fail` / `none` | The playbook ran to completion. Conventionally these are the output of the rules you define; `none` means no rules executed. | | `incomplete` | Footprint needs something only the user can provide. Use `continue_onboarding` to let them finish. | | `pending` | The run is still executing: the onboarding was started asynchronously, or a step took longer than your synchronous timeout. You'll receive a webhook when the onboarding completes, or you can poll with GET /onboardings/{id}. | | `error` | The run failed due to logic configured on your playbook, with details in `error_message`. | ### Playbook output A playbook can declare what it produces. Add an **Output** node as the last node of the playbook and give it a set of keys, each assigned an expression over the available onboarding data: vaulted attributes, onboarding data computed by earlier nodes, outputs of external API nodes, and the results of your verification checks. When the run reaches that node, the values it evaluated come back in `output`, keyed by the names you configured: ```json { "id": "ob_SRFT2a1mN7DAWJ0VPXkiqK", "status": "pass", "requires_manual_review": false, "error_message": null, "continue_onboarding": null, "output": { "risk_tier": "low", "is_over_21": true, "normalized_state": "CA" } } ``` `output` is `null` when your playbook has no output node or when onboarding hasn't completed yet; `pending` and `incomplete` runs return `null`. [Fetch the onboarding](#fetching-an-onboarding-later) once it finishes to read the output. Because the output node runs last, its expressions can read everything computed earlier in the flow. That makes it the place to hand your backend more context on what happened during the onboarding than the status represents. ### Webhooks Runs that don't finish inline, asynchronous runs, and onboardings that returned `pending` deliver their decision via the `footprint.onboarding.completed` webhook. See [Webhooks](/articles/integrate/webhooks) to set up your endpoint. ## Fetching an onboarding later Not every run resolves inline: asynchronous runs return immediately, synchronous runs can time out, and a run can stop to wait on the user. In all of these cases, GET /onboardings/{id} returns the current state of the run, in exactly the same shape as the response you got when you created it. ```bash curl https://api.onefootprint.com/onboardings/ob_SRFT2a1mN7DAWJ0VPXkiqK \ -u : ``` Fetching an onboarding never re-runs it, so it's safe to poll. Use it to: * **Resolve a `pending` run.** Read the final `status` and `output` once the run finishes. * **Get a link for an asynchronous run.** `continue_onboarding` is never returned by an asynchronous run, because the playbook hasn't executed yet. Once the run reaches `incomplete`, fetch it to pick up the token and link. * **Reissue an expired link.** Continuation tokens last 12 hours. Fetch the onboarding again for a fresh one. * **Read the output.** [Playbook output](#processing-the-response-playbook-output) is returned here too, once the run reaches the output node. Every fetch of an `incomplete` onboarding mints a new `continue_onboarding` token and link. Tokens issued by earlier fetches keep working until they expire, so fetching again does not invalidate a link you already sent to a user. Onboardings can also be fetched by `onboarding_external_id`, using the `ext_id:` prefix: ```bash curl https://api.onefootprint.com/onboardings/ext_id:bc13ca5b-210f-49af-9aba-e98db366484a \ -u : ``` Continuing an onboarding resumes the run the user already started, and it keeps the same `id`. To run the playbook again from scratch, call POST /onboardings with a new `onboarding_external_id`. See [Reonboarding and idempotency](#request-options-reonboarding-and-idempotency). --- # Client-side vaulting Footprint's unified onboarding and vaulting platform makes it simple to vault sensitive user data directly from client-side contexts like a mobile app or a web app. ## Prerequisites Please read our [Server-side API Authentication guide](/api-reference#authentication). ## Step 1: Use an existing vault or create a new one (server-side) For each user in your system you should create a single Footprint user vault (server side). If your user doesn't have an `fp_id` yet, create a new user using the `POST /users` API. ### Example request ```bash curl https://api.onefootprint.com/users \ -X POST \ -u sk_test_CXUsbCR8j2kH6e5GeEl8eSBnQTIPCUaKpv: ``` ### Example response ```json { "id": "fp_id_K0q6Eh6Rr3WOOfFBLPiHsr" } ``` If you're migrating other sensitive data, [read our guide here](/articles/integrate/migrate-existing-data) Make sure to store the `fp_id` on your user record to access and vault data. ## Step 2: Create a client token (server-side) In order to vault data directly from client code, you need to generate a short-lived client token. Footprint's vault supports several types of structured data like identity data, card-holder data, and custom data for arbitrary key-value records. For structured data Footprint can validate that data is in the right format to ensure that you can reliably use this data in various applications and reporting/compliance needs. For unstructured data, you can store any other sensitive user data that may not fit the mold. [Learn more about how to namespace and vault identity, card, and custom data](/articles/vault/apis#payment-cards). Note that card objects can be named, i.e. `card.primary` or `card.secondary`. This lets you store arbitrarily many cards in a user vault and control the naming scheme for each card. ### Example request ```bash curl https://api.onefootprint.com/users/fp_id_K0q6Eh6Rr3WOOfFBLPiHsr/client_token \ -X POST \ -u sk_test_CXUsbCR8j2kH6e5GeEl8eSBnQTIPCUaKpv: \ -d '{ "fields": [ "card.primary.number", "card.primary.cvc", "card.primary.expiration", "card.primary.name" ], "scope": "vault", "ttl": 180 }' ``` Be sure to specify the exact list of fields that you plan to vault. ### Example response ```json { "expires_at": "2023-05-24T14:15:22Z", "token": "ctok_vJK5Ze2N5fQ1GtE5V770BH8CZtQwXHF1hxowB9Nowh0" } ``` Now that you have this client token, you can transmit it to your client code and use it to vault data directly from the client. ## Step 3: Use the client token store data in the vault (client-side) From your client-side app, vault the data directly to footprint: ### Example request ```bash curl https://api.onefootprint.com/users/vault \ -X PATCH \ -H 'x-fp-authorization: ctok_vJK5Ze2N5fQ1GtE5V770BH8CZtQwXHF1hxowB9Nowh0' \ -d '{ "card.primary.number": "4242424242424242", "card.primary.cvc": "424", "card.primary.expiration": "10/25", "card.primary.name": "Whitfield Diffie" }' ``` You will only have permissions to vault the exact fields specifically requested in Step 2. ### Example response A successful response will return status 200 and an empty object. ## Step 4 (optional): Use a client token to decrypt data Footprint's client side vaulting APIs support two ways of decrypting data: structured decryption and downloads. ### Structured Decrypt Example Structured decryption is useful when you need to decrypt several smaller fields like `ssn` or `card.*.number` at once. This API is similar to its backend counter-part. On your backend, create a client side token with a single field and the `decrypt` scope. This token is safe to pass to your frontend because it has a short expiration time and limited scope. ```bash curl https://api.onefootprint.com/users/fp_id_K0q6Eh6Rr3WOOfFBLPiHsr/client_token \ -X POST \ -u sk_test_CXUsbCR8j2kH6e5GeEl8eSBnQTIPCUaKpv: \ -d '{ "fields": [ "id.ssn9", "card.primary.number" ], "scope": "decrypt", "ttl": 180, "decrypt_reason": "test structured" }' ``` ```json { "expires_at": "2023-07-31T14:15:22Z", "token": "ctok_vJK5Ze2N5fQ1GtE5V770BH8CZtQwXHF1hxowB9Nowh0" } ``` On your client, you can make a request to the decrypt endpoint using the token generated above. You will be able to decrypt any field specified when creating the token. ```bash curl https://api.onefootprint.com/users/vault/decrypt \ -X POST \ -H 'x-fp-authorization: ctok_vJK5Ze2N5fQ1GtE5V770BH8CZtQwXHF1hxowB9Nowh0' \ -d '{ "fields": ["id.ssn9", "card.primary.number"] }' ``` ```json { "id.ssn9": "1212121212", "card.primary.number": "424242424242424" } ``` ### Download Decrypt Download decryption is most useful for decrypting larger objects like files. This is especially useful when you'd like to have a user download this file directly instead of decrypting it. On your backend, create a client side token with a single field and the `decrypt_download` scope. ```bash curl https://api.onefootprint.com/users/fp_id_K0q6Eh6Rr3WOOfFBLPiHsr/client_token \ -X POST \ -u sk_test_CXUsbCR8j2kH6e5GeEl8eSBnQTIPCUaKpv: \ -d '{ "fields": [ "custom.paystub_w2" ], "scope": "decrypt_download", "ttl": 180, "decrypt_reason": "download w2 test" }' ``` ```json { "expires_at": "2023-07-31T14:15:22Z", "token": "ctok_vJK5Ze2N5fQ1GtE5V770BH8CZtQwXHF1hxowB9Nowh0" } ``` Now, the client can decrypt and download this object with a simple GET request. The response body will entirely comprise the contents of the object specified in `fields` above. This can easily be used by your frontend to download the contents directly to the user's device. ```bash curl https://api.onefootprint.com/users/vault/decrypt/ctok_vJK5Ze2N5fQ1GtE5V770BH8CZtQwXHF1hxowB9Nowh0 ``` --- # Migrating user data Footprint's unified onboarding and vaulting platform makes it simple to migrate sensitive user data living inside your data stores to our secure, Nitro Enclave-backed vaulting infrastructure. Whether you need to decrypt, securely proxy, search, or create user data -- you can use our unified vaulting APIs. These APIs work identically for users that you've migrated and new users that are onboarding through Footprint's embedded KYC flows. Furthermore, Footprint supports progressive onboarding, watchlist checks, embedded components, and more, all on migrated user data. ## Prerequisites Please read our [API Authentication guide](/api-reference#authentication). ## Create a new user vault If your user doesn't yet have an `fp_id`, create a new user vault using POST /users. You can provide a key-value map of data you've already collected from the user to initialize the Footprint vault. ### Example request ```bash curl https://api.onefootprint.com/users \ -X POST \ -u sk_test_CXUsbCR8j2kH6e5GeEl8eSBnQTIPCUaKpv: \ -d '{ "id.first_name": "Jane", "id.middle_name": "Samantha", "id.last_name": "Doe", "id.dob": "1988-12-30", "id.ssn9": "12-121-1212", "id.address_line1": "1 Penguin Pond", "id.city": "Polar Plunge", "id.state": "NY", "id.zip": "10014", "id.country": "US", "id.phone_number": "+15555550100", "card.primary.number": "4242424242424242", "card.primary.cvc": "424", "card.primary.expiration": "12/24", "custom.account_number": "42421212312", "custom.routing_number": "12121212121" }' ``` ### Example response ```json { "id": "fp_id_K0q6Eh6Rr3WOOfFBLPiHsr" } ``` Take special note of the `fp_id` value above - this is the only identifier you'll need to store in your database for this user vault. All vault data can be referenced with the `fp_id`. We recommend storing the `fp_id` alongside your existing users in the database. Each user in your database should have a single `fp_id` in Footprint. A successful response as above indicates the request data is properly formatted, the vault is created, and the data securely stored. Otherwise, an error will return with the data field that is improperly formatted, and the vault will not be created. Footprint's vault supports several types of structured data: identity data, debit and credit card data, documents (like drivers licenses and passports), arbitrary key-value records, and more. Footprint will apply validations to structured data when it is vaulted. Check out the API reference on POST /users for details on how to use data identifiers and this API. ## Data integrity (optional) For big migration jobs, it can be useful to validate that the data you have migrated to footprint vaults actually byte-for-byte matches what you have in your database. To that end, Footprint vaults support an `integrity` endpoint for computing signed hashes (using `HMAC-SHA256`) of the underlying data so you can check that the data you've pushed matches what you expect. To compute integrity signatures, provide a HEX-encoded `signing_key` and a list data identifiers in `fields`. For each provided data identifier `key` in `fields` a resulting `hmac-sha256(signing_key, vault[key])` is computed. ### Example ```bash curl https://api.onefootprint.com/users/fp_id_K0q6Eh6Rr3WOOfFBLPiHsr/vault/integrity \ -X POST \ -u sk_test_CXUsbCR8j2kH6e5GeEl8eSBnQTIPCUaKpv: \ -d '{ "fields": ["id.first_name", "id.last_name", "id.ssn9", "card.my_alias.number"], "signing_key": "a1f928d87278290bf9dece075d0e46330a01d21b346073f4f193739078dca458" }' ``` ```json { "id.first_name": "6e9b8af84ffc8829f03911f73c997d27c62a4c2078d90320ebcb7dbbce0e39a5", "id.last_name": "55c6c9c45dc54391fdd2f98d719095479ca3022f8583d1a6442d4c66889f8bb9", "id.ssn9": "18568e3cd81f27a50e56750317d3446a3080f3aba4a726af3b848b51eb37071f", "card.my_alias.number": "4f8a7abfbf11912991b364fd429f2a59cea4c859619692e712b628752cb83ecf" } ``` ## More guides and resources Please find additional docs and guides for using Footprint vaults: 1. [API Reference](/api-reference#post-users) 2. [PII Vault docs](/articles/vault/apis) 3. [Vault Proxy Ingress](/articles/vault/proxy) ## Manual migration assistance If you need help migrating data please reach out to us at `support@onefootprint.com` with the details of your request. For any sensitive data please use the PGP key below to secure data in transit. ### Footprint PGP instructions First, use [GPG](https://gnupg.org) to [import](http://www.gnupg.org/gph/en/manual.html#AEN84) our public key below. After importing our Public Key, you can encrypt files by running: ```bash gpg --encrypt --recipient 1ED420961981B558 ``` Note the parameters represent the following. * `1ED420961981B558`: is the Footprint key ID. * ``: is the name of the plaintext file you are encrypting. * `.gpg`: is the encrypted file that will be output for you to send us. ### Footprint PGP Public Key ```text -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBGZWM3EBEADt8TjjHZ6VyDXqzq7P7cdxjaHKcyOKltM+fl+JKmaeexO3H6Gz PK8hNnQ1Z+kmMc5th7JpN+Zcbq9IHDsx5POwe8dSOznGFU70TiUFY2WnNBMYVpNN v1noa3UUMVlgo/xkriCOvwXfcSfmz4nNyp0vaSvtma7vuTF3vUKLfFZUoJjnGBTm kMD9uMcqFjt2FowyOKH0zvn0xNhAfb9pq/kXoHwsf8wt8brDMVxG2BQYasJPcfl4 z60irxQnIxc9vi5wolRx2fjzn1Y/xhXCv6/eLz9mUchxLiE010siAXTclAJUolAD YRf1qYHwMi2xW+VHDg4Myz4QsbN8tuNa6LzTbpIPgRxusPYYPBatP3dt3vPGnnKM Y8pZVV1xdsujunoEgQJGq1DZH+tDu+BwH96jqkTDX2PMvY1BWWLGmZuyiDu/9aTB gIlXlyp6Z1q0PZV96+p2B6DRCPeZuIY9bUaE2AiROF7TrtTYXow8GIy4D6oCTN90 g7to+HUdUgntMgXG81vqdX4agAGlf+19JdPUie1qYrbZB6RDIV94+bxQyoE1ebMR fgedeDr8A0ESymjIo4335ZYeGxwaQmWyZ/CcFKmuH0d7isq89B+XuAD3mE1IUuDv kAfpAmAzmh1xJ/hmege6QN2QogDXNYQyvBbTykpeB3BQdgr9pseXmeaIWQARAQAB tC1Gb290cHJpbnQgUEdQIEtleSA8c2VjdXJpdHlAb25lZm9vdHByaW50LmNvbT6J AlQEEwEIAD4WIQQvZgtWJBipI816pvQe1CCWGYG1WAUCZlYzcQIbAwUJEtfgaQUL CQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRAe1CCWGYG1WFyaEAC66BFrzxt0PqnB f3eXGCT3BMAYBauIwwlbqGTY1XUU+E68BfBuScqMCTFc9daHJR02GiS0Xth4bmq+ I5ldpUdiVatAoJl9pZZzSsfCmFkqcv71xc1TSinWGmPzfuXcVibaa6v5ypqEpgKC q8Uh314Gotwk/yt+3+4SfiUNCcOE+1vbVmZfgnEyzyB2EeBX62WusBW1jzQ2JXyG wneZ7CiAQcliJAP5goNK38/W9pcdEYXTL8IkYXxboJY1hOZj3sTTqp3/crB2vOJO C1bra83a0mLoWmaS0/X503WyC44pYp7V48vRkCyM0P69lFS7RTjK3hhcVB6V/nnz M78cRMgn7kCqHgvsTCvpk4UDcM4b6XUKYs2Pph2Ouz3/b+EPeAtVWVuGp1F55Iti u8WAVFr8/hprq8FVl6d0aWxTtIa2mr0ht7jq6Vv/yzpLEcqsBBr9nwvn623iSQaQ BYx35D/6yUbGjj62kVbkQ61ydhXFqQ32k4uyaS5DaZ5vDmK01FckvsLrIXHlOGfZ VMmbsFnzflDI1v5jckKXpbGYOUbCHksmtdoGUtxK0c457FxpaYaw8hi/+FPz6HOC IH4XqlrbPpNckBCajAJN+fc+DVVHJVyzzZN75gtR57V0LtqQ6dAUxmfbrZnYmlx3 PyafmzLPJMOxYh+cx4nXuTuIKGYu3LkCDQRmVjNxARAAoAAO/dnqICjpm7QX999c 56H2g0tvRfrmXaZso1ctr5DmAYz9Tx4kzcEJGd2jPVvv4UPDsThjjsK3KJqS5nFi Q/xsyHHj8A5VcYfqytkkC0kV9iX7RoW3yzRCY+751ic2XjEZ46tmWdncLjCVHv1q SPkpcUcru8sda5D9lwGm0w1bxbrfLFF+7LALHAA3Rz37hnO2+I29fTBN19jkYDc5 fm2/vA93yUrOAqS+KH4YC7of+aPDK7mFw+o1YuWciYAMtN3C37fbs2xO3tY7h8DH 5LRCbGDSuFCauW7LXVZULayaH5O9kBwU5lq+jCLJlpuy/9ZjHeHQGqqSCE//rL/1 rkwJ72xB/V+TP7t+NCko4LSWSDDjiScC7UhqebOOtPr5+G/VUMGWflq2gP6KAVh/ uQkk//jp05adSMBu+s4vp5ArGJuIUtnIlD1Z7YZ+x+orxbSQx/ddqqo4jYXQAg1Y SGtq0ercoExh7Gnchj1SHUtu7Y7o4KN8ikrUvYtgq56eW43w0JPsV3E0jFZEgjti NtG/HQRxQjwMgUbK3bvnaEon+lcY5bAwE09cuZ06KRriBGrO/fRb4vbzl/X0iZeR reXgHQ/Fr8NitFJkoMSxel23XsEF37KZNoRhOAyJkHUWu/RkOfZ7isdO63+v1v6N JM3aBTDKq/xsFElanawZmyEAEQEAAYkCPAQYAQgAJhYhBC9mC1YkGKkjzXqm9B7U IJYZgbVYBQJmVjNxAhsMBQkS1+BpAAoJEB7UIJYZgbVYpMsP/R6dHdiEn/OCVfpx KlCIFMaD/gry1YiVo4p4NJzUY1N9gujIvsTarHFfELpP2JBsBt6XZglstqThv+RW 7HcKApDk7z99WqYqI3kq2n+3S99C8tHv5KObY3ayjZRX+mJJbh3urlqBLApszP0Q X2Bd0e2OKHq1EkAZ2t7SXyPk3mPh5qTCXXb7oHp0sWS08CJdAz0ivD6rD65CG+QW Fsd/Hs5DuyQooXUzbhCtksF90zowyJzMNWoWE6RiwMWHR505+iJrU/C2LMEumoJb F4BXmBIlR/rnApiUxpuYsW1w+LNVuWgX++sn7UWLYMuhif28OLh6jqIdz6jsw2lr DyfQdCSQKadngZNAjvmP8R3azWaLA6Vs14EUJleo9bH2e5tHPNxTm/L3lNukZgw3 wgZMC5Z/XMzasz2Fbu3D2MGKynZL12kOQsyiHlnPXWmdXjWjNNXhTECyfkmNkWaA NZ4sENur27l1VRZerMpblFnTtasQI2dTkpt2FXdXLUhZDK7OusQExk4E8xATLiwv J6xz6VfoSZrAuJ/qLMXac+XoZdrbhqBMSYk8k/FOGnCCrCnvI39k+VPn9KoTTBdZ +dfKUURDufG8FuZs6hCb6WTzbuuNbZPtzLYootf8g8CUUU0KZFg+dw7QQsq78FVT OwC3wKkvTn6QpiPA9DDuE3PSp2ZZ =EpSl -----END PGP PUBLIC KEY BLOCK----- ``` --- # Content Security Policy Permissions Policy and Content Security Policy (CSP) can provide an additional layer of security to your applications. If you have these policies configured, you need to make sure Footprint features and domains are whitelisted. ## Required Security Headers Depending on the framework you use, there are different ways to set Permissions & Content Security Policies. However, you should ensure the following values are added to your policies: ```json { key: 'Permissions-Policy', value: 'camera=(self "https://*.onefootprint.com/*"), publickey-credentials-get=(self "https://*.onefootprint.com/*"), otp-credentials=(self "https://*.onefootprint.com/*"), clipboard-write=(self "https://*.onefootprint.com/*")' }, { key: 'Content-Security-Policy', value: 'child-src onefootprint.com; connect-src *.onefootprint.com https://fp.risk.onefootprint.com https://fpnpmcdn.net; frame-src *.onefootprint.com;' } ``` 1. Permissions Policy * Footprint identity verification flows require access to the browser's public key credentials to register and verify passkeys. * Our flows may also require access to the browser camera if your onboarding configurations collect ID documents and selfies. * We also have some functionality that copies content to clipboard, such as copying the test ID in sandbox mode. More information on Permissions Policy can be found [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy). 2. Content Security Policy * Because our integrations run in an iframe for compliance and security reasons, you will also need to make sure footprint domains are whitelisted for `child-src`, `connect-src` and `frame-src` fields. More information on Content Security Policy can be found [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP). --- # No-code integration Footprint offers a no code integration that takes minutes to set up. While our SDKs provide the best experience for users onboarding from your website, our no-code integration is a great alternative if you want to share a permanent link with your users that they can use to create Footprint vaults. ## Step 1: Create an onboarding configuration 1. Go to the Footprint [Playbooks tab](https://dashboard.onefootprint.com/playbooks) and create a new Playbook. * On the "Your Playbook" screen, click the edit button to change the fields you plan to collect. * On the "Authorized scopes" screen, select the fields you require access to decrypt. 2. Grab the permanent onboarding link * Open the dropdown menu in the Playbook you just created * Clicking "Get shareable link" will copy the onboarding config link to your clipboard. This link never expires, and you can share it with your users. ## Step 2: (Optional) Set up Webhooks for new onboardings You can set up webhooks to receive updates on this configuration. For example, you can receive Slack notifications every time a user successfully onboards using the configuration you created. More information on how to configure Webhooks can be found [here](/articles/integrate/webhooks). --- # Introduction Our `footprint-js` library lets you integrate your application with Footprint. It can be used together with any framework or library, such as Angular, React, Vue, or simply plain Javascript. Under the hood, it launch iframes from your website, and exchange secure messages between Footprint and your application. ## Browser support Footprint libraries supports all recent versions of major browsers. For the sake of security and providing the best experience to the majority of customers, we do not support less popular browsers that no longer receive security updates. | Name | Versions | | ----------------------- | --------------------- | | Chrome | Last 3 major versions | | Chrome Mobile | Last 3 major versions | | Firefox | Last 3 major versions | | Microsoft Edge | Last 3 major versions | | Safari OSX | Last 3 major versions | | Safari iOS | Last 3 major versions | | Android default browser | Last 3 major versions | Footprint products depend on certain native browser functionality. Polyfilling certain browser features (e.g Promises API) may prevent footprint.js from working normally. If footprint.js isn't functioning as expected, try removing polyfills to determine if they might be causing the issue. ## Installation 1. Install `footprint-js` version 5 or higher. ```bash npm install @onefootprint/footprint-js ``` Make sure your application's permissions and content security policies are configured correctly to allow the Footprint integration. More info can be found [here](/articles/integrate/content-security-policy). To make sure our iframe has the correct styles, you'll need to import our css file: ```javascript import "@onefootprint/footprint-js/dist/footprint-js.css"; ``` --- # Onboarding (KYC/KYB) ## Getting started This guide explains how to integrate the Footprint onboarding flow for KYC (Know Your Customer) and KYB (Know Your Business) into your JavaScript/TypeScript applications. **Note:** Make sure you have `@onefootprint/footprint-js` version 5.0.0 or higher installed for the onboarding integration to work properly. The default behavior launches the onboarding flow within a modal. For an inline integration option, see the [Inline Integration](#inline-integration) section below. 1. **Start the onboarding**: * Start the onboarding and get an onboarding token, e.g., `obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH`, from [POST /onboardings](/articles/guide/definitive-integration-guide#the-end-to-end-integration-step-4-run-the-playbook). 2. **Initialize the Footprint Flow**: * Trigger Footprint, for example, when a button is clicked. Then, pass the `onboardingSessionToken` and `onComplete` callback to the `initialize` method. ```javascript import "@onefootprint/footprint-js/dist/footprint-js.css"; import { onboarding } from "@onefootprint/footprint-js"; const App = () => { const launch = () => { onboarding.initialize({ onboardingSessionToken: "obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH", onComplete: (validationToken) => { console.log(validationToken); }, }); }; return ( ); } ``` 3. **Handle Completion:** * Once the user completes the flow, you'll receive the validationToken through the `onComplete` callback. Post this token to your backend for further processing. Click [here](https://github.com/onefootprint/examples/tree/master/idv/frontend-vite-vanilla) to check out a full example. ## Listening to events Footprint provides several events based on actions performed by the user. To listen to events, pass them from either the `initialize` (modal) or `initializeInline` method. ```typescript import { onboarding } from "@onefootprint/footprint-js"; onboarding.initialize({ onboardingSessionToken: "obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH", onComplete: (validationToken) => { console.log(validationToken); }, onError: (error) => { console.log(error); }, onAuth: (validationToken) => { console.log(validationToken); }, onCancel: () => { console.log("User canceled the flow"); }, onClose: () => { console.log("User closed the flow"); }, }); ``` ## Tracking flow progress **Note:** The `onRequirementChange` callback is supported from version 5.6.0 onwards. Pass `onRequirementChange` to track the flow's progress from your page — for example, to drive your own stepper while the flow runs inline. It fires whenever the requirement being executed changes, and again when the flow moves to a different screen within the same requirement: ```typescript import { onboarding } from "@onefootprint/footprint-js"; onboarding.initializeInline({ onboardingSessionToken: "obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH", containerId: "footprint-container", onComplete: (validationToken) => { console.log(validationToken); }, onRequirementChange: ({ kind, page }) => { console.log(kind); // e.g. "collect_business_data" console.log(page); // e.g. "business_owners", or undefined }, }); ``` Possible `kind` values are `collect_business_data`, `collect_data` (personal information), `collect_document`, `liveness` (passkey registration), `link_bank_account`, `collect_investor_profile`, `collect_card_data`, `collect_custom_data`, `collect_document_data`, `confirm_verified_prefill`, `register_auth_method` and `process`. New kinds may be added over time, so handle unknown values gracefully. When a requirement spans more than one distinct screen, the payload also carries a `page` field. Today it is set to `business_owners` while the beneficial-owners screen of `collect_business_data` is shown, so a stepper can distinguish it from the business-details screen of the same requirement. It is omitted everywhere else, and new `page` values may be added over time. ## Inline Integration **Note:** The `initializeInline` method is supported from version 5.1.0 onwards. For use cases where you prefer to embed the onboarding flow directly within your page instead of using a modal, you can use the `initializeInline` method. **Note:** The inline integration requires a container element in your DOM where the flow will be rendered. ```javascript import "@onefootprint/footprint-js/dist/footprint-js.css"; import { onboarding } from "@onefootprint/footprint-js"; const App = () => { const launchInline = () => { onboarding.initializeInline({ onboardingSessionToken: "obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH", containerId: "footprint-container", onComplete: (validationToken) => { console.log(validationToken); }, }); }; return (
); } ``` The inline integration supports the same event handlers and customization options as the modal version. Make sure to provide adequate height for the container element (recommended minimum: 600px). ## Setting a custom appearance You can customize the appearance of the onboarding flow by passing an `appearance` object to either the `initialize` (modal) or `initializeInline` method. ```typescript import { onboarding } from "@onefootprint/footprint-js"; onboarding.initialize({ onboardingSessionToken: "obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH", onComplete: (validationToken) => { console.log(validationToken); }, appearance: { variables: { borderRadius: "8px", colorSuccess: "#10b981", colorError: "#F87171", buttonPrimaryBg: "#5550e9", }, }, }); ``` For more information, including a list of available variables, check out the [customization guide](/articles/integrate/customization). ## Available Props | Variable | Description | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `onboardingSessionToken` | The onboarding session token you created. | | `onComplete` | Triggered after the user completes the onboarding flow. You'll receive a `validationToken` that your backend can exchange with Footprint to see the fp\_id, the login method used, and the KYC status. | | `onAuth` | Optional. Triggered after the user finishes logging in, before the user has finished fully onboarding. You'll receive a `validationToken` in this callback that your backend can exchange with Footprint to see the fp\_id and the login method used. | | `onError` | Optional. A function that is called when there was an unrecoverable error while initializing the onboarding flow. It takes in an error string argument with more details. | | `onCancel` | Triggered when the user abandons the flow. This can be triggered when the user clicks on the close button inside our iframe | | `onClose` | Triggered when the user closes the flow (either completed or canceled). | | `onRequirementChange` | Optional. Triggered when the requirement being executed changes. Receives `{ kind, page? }`, so your page can track the flow's progress. | | `appearance` | Optional. A `FootprintAppearance` object that customizes the look of your integration | | `l10n` | Optional. Specifies the desired localization. More information [here](/articles/integrate/customization#localization-configuration). | | `containerId` | Required for inline integration only. The ID of the DOM element where the onboarding flow will be rendered. | --- # Changelog Notable releases for `@onefootprint/footprint-js`, the core browser SDK for launching Footprint onboarding and bank linking. --- # Introduction ## Before starting Expo is a powerful open-source platform that allows developers to build, deploy, and quickly iterate on native iOS, Android, and web apps from the same JavaScript codebase. It provides a set of tools and services for building applications that target a wide range of platforms with a single codebase, making the process of app development faster and easier. Footprint's integration with Expo is designed specifically to leverage Expo's unique environment. This package - footprint-expo - allows Footprint to work seamlessly within Expo's framework. The implementation, though similar to our web-based solution, uses an in-app browser (webview) instead of iframes, providing a smooth, native-like experience for the user. **Important:** This package cannot be used in the "Expo Go" app because it requires custom native code. You'll need to rebuild your app as described in the [Adding custom native code](https://docs.expo.dev/workflow/customizing/) guide using `npx expo prebuild`. ## Minimum version requirements Before proceeding with the installation, make sure your environment meets the minimum version requirements: | Dependency | Minimum Version | | ---------- | --------------------- | | Expo | Last 3 major versions | ## Installation 1. Install `@onefootprint/footprint-expo` version 3 or higher. ```bash npm install @onefootprint/footprint-expo ``` 2. Install required peer dependencies ```bash npm install expo-linking expo-web-browser @fingerprintjs/fingerprintjs-pro-react-native ``` 3. For iOS, install pods: ```bash npx pod-install ``` 4. For Android, add the Fingerprint Pro Maven repository to the `allprojects` block in your `android/build.gradle`: ```groovy allprojects { repositories { google() mavenCentral() maven { url 'https://www.jitpack.io' } maven { url 'https://maven.fpregistry.io/releases' } } } ``` This is required because `@fingerprintjs/fingerprintjs-pro-react-native` hosts its Android SDK on a custom Maven repository. Without this, your Android build will fail with dependency resolution errors. --- # Onboarding (KYC/KYB) ## Getting started This guide explains how to integrate the Footprint onboarding flow for KYC (Know Your Customer) and KYB (Know Your Business) into your Expo applications. 1. **Start the onboarding**: * Start the onboarding and get an onboarding token, e.g., `obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH`, from [POST /onboardings](/articles/guide/definitive-integration-guide#the-end-to-end-integration-step-4-run-the-playbook). 2. **Initialize the Footprint Flow**: * Trigger Footprint, for example, when a button is clicked. Them, pass the `onboardingSessionToken` and `onComplete` callback to the `initialize` method. ```javascript import { onboarding } from "@onefootprint/footprint-expo"; import { View, Button } from "react-native"; const App = () => { const launch = () => { onboarding.initialize({ onboardingSessionToken: "obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH", onComplete: (validationToken) => { console.log(validationToken); }, }); }; return (