Run a playbook
10 min read
The POST /onboardings API runs one of your playbooks on a user or business. You call it from your backend. The playbook executes as far as it can with the data already in the entity's vault, and the response carries either the decision or a continue_onboarding token that lets the user finish the remaining steps in a Footprint flow.
The same call covers both integration shapes:
- Fully headless. Your app 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_onboardingtoken: pass it to one of the Footprint frontend SDKs, or send the user the hosted link, and they complete the rest.
The Integration Guide walks through the full integration step by step.
Quick start
Create the user with POST /users (or a business with POST /businesses) and vault the data you have already collected. Both are server-side APIs authenticated with your secret API key; never send the key to the client.
bash
Vault fields lists every attribute you can vault, and Migrating user data covers bringing over data you already hold. If you expect the user to provide most of their information in the interactive flow, vault only what you have, or nothing at all.
Then run a playbook on the user. Set synchronous_timeout_secs (max 30) to wait for the decision and receive it inline:
bash1curl -X POST https://api.onefootprint.com/onboardings \ 2 -u <SECRET_API_KEY>: \ 3 -d '{ 4 "fp_id": "fp_id_K0q6Eh6Rr3WOOfFBLPiHsr", 5 "key": "<PLAYBOOK_KEY>", 6 "synchronous_timeout_secs": 30 7 }'
json1{ 2 "id": "ob_SRFT2a1mN7DAWJ0VPXkiqK", 3 "status": "pass", 4 "requires_manual_review": false, 5 "continue_onboarding": null, 6 "error_message": null, 7 "output": null 8}
Not every run finishes inline with a decision like this one. Processing the response covers the 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 does not finish before the timeout, the response ispending, execution continues in the background, and the decision arrives by webhook. - Asynchronous. Omit it. The call returns immediately with
status: "pending", the run executes in the background, and the decision arrives through thefootprint.onboarding.completedwebhook; see Webhooks. Asynchronous runs suit slow steps, such as an agent, that may not finish within the synchronous timeout.
Only synchronous runs return continue_onboarding. Run synchronously whenever the user may need to finish the flow interactively. For an asynchronous run, fetch the onboarding once it stops to wait on the user, and pick up the token there.
External ID
If the user you created above has an external ID, pass external_id instead of fp_id. To keep a one-to-one mapping with your own records, set one by sending your identifier in the x-external-id header when you create the entity.
Reonboarding and idempotency
By default, an entity onboards onto a playbook once. This protects you from accidental charges for repeat onboardings: a second POST /onboardings 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, to have your users reonboard every time they fill out a new account application in your product, provide the application's identifier:
bash1curl -X POST https://api.onefootprint.com/onboardings \ 2 -u <SECRET_API_KEY>: \ 3 -d '{ 4 "fp_id": "fp_id_K0q6Eh6Rr3WOOfFBLPiHsr", 5 "key": "<PLAYBOOK_KEY>", 6 "onboarding_external_id": "bc13ca5b-210f-49af-9aba-e98db366484a", 7 "synchronous_timeout_secs": 30 8 }'
To let users reonboard onto the same playbook once a month, derive onboarding_external_id from the current month, such as 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 the playbook needs context from your backend, such as details of the transaction that triggered the onboarding, add a prerequisite node to the playbook and pass the data as prerequisite_data:
bash1curl -X POST https://api.onefootprint.com/onboardings \ 2 -u <SECRET_API_KEY>: \ 3 -d '{ 4 "fp_id": "fp_id_K0q6Eh6Rr3WOOfFBLPiHsr", 5 "key": "<PLAYBOOK_KEY>", 6 "prerequisite_data": { "transaction_amount": 1000 }, 7 "synchronous_timeout_secs": 30 8 }'
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.
Passing documents as onboarding data
You can also hand a playbook a document: upload it to the user's vault under a document vault field (a data identifier in the API), then pass that field's name to the playbook as onboarding data. The playbook needs a prerequisite node that accepts a document onboarding data type, and during the onboarding the document field name is stored under the data. prefix.
For example, to verify an identity document you have already collected and skip the document collection step in your playbook, first upload each side of the document under a document vault field, for the same fp_id you pass to POST /onboardings:
bash1curl -X POST https://api.onefootprint.com/users/fp_id_K0q6Eh6Rr3WOOfFBLPiHsr/vault/document.drivers_license.front.image/upload \ 2 -u <SECRET_API_KEY>: \ 3 -H "Content-Type: image/jpeg" \ 4 --data-binary @drivers-license-front.jpg 5 6curl -X POST https://api.onefootprint.com/users/fp_id_K0q6Eh6Rr3WOOfFBLPiHsr/vault/document.drivers_license.back.image/upload \ 7 -u <SECRET_API_KEY>: \ 8 -H "Content-Type: image/jpeg" \ 9 --data-binary @drivers-license-back.jpg
The upload API is POST /users/{fp_id}/vault/{identifier}/upload.
When you run the playbook, pass the field names you uploaded to as prerequisite_data:
bash1curl -X POST https://api.onefootprint.com/onboardings \ 2 -u <SECRET_API_KEY>: \ 3 -d '{ 4 "fp_id": "fp_id_K0q6Eh6Rr3WOOfFBLPiHsr", 5 "key": "<PLAYBOOK_KEY>", 6 "prerequisite_data": { 7 "id_front": "document.custom.id_front", 8 "id_back": "document.custom.id_back" 9 }, 10 "synchronous_timeout_secs": 30 11 }'
Processing the response
Every run returns the same fields:
| Field | Description |
|---|---|
id | The onboarding's unique identifier. Use it to fetch this run later, and to fetch its details, risk signals, and documents. |
status | The outcome of the run; see 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. |
Finish in a Footprint flow
When the playbook needs something only the user can provide, such as a collected field that is not vaulted or an identity document, the run stops with status: "incomplete" and a continue_onboarding object:
json1{ 2 "id": "ob_SRFT2a1mN7DAWJ0VPXkiqK", 3 "status": "incomplete", 4 "requires_manual_review": false, 5 "error_message": null, 6 "output": null, 7 "continue_onboarding": { 8 "token": "obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH", 9 "link": "https://verify.onefootprint.com/?type=user#obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH", 10 "expires_at": "2026-07-17T12:00-07:00" 11 } 12}
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 the user in one of two ways:
- Hosted. Send the
linkto the user by email, SMS, or a button in your app. - Embedded. Pass the
tokento the SDK asonboardingSessionToken:
javascript1import "@onefootprint/footprint-js/dist/footprint-js.css";
2import { onboarding } from "@onefootprint/footprint-js";
3
4onboarding.initialize({
5 onboardingSessionToken: "obtok_UxM6Vbvk2Rcy1gzcSuXgk3sj3L9I0pAnNH", // continue_onboarding.token
6 onComplete: () => {
7 // the user has finished the remaining steps
8 },
9});
When the user finishes, the SDK's onComplete handler fires. Read the result with GET /onboardings/{id}, using the id from the response that created the run. The Integration Guide has iOS and Android examples.
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. Footprint sends a webhook when the onboarding completes, and 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:
json1{ 2 "id": "ob_SRFT2a1mN7DAWJ0VPXkiqK", 3 "status": "pass", 4 "requires_manual_review": false, 5 "error_message": null, 6 "continue_onboarding": null, 7 "output": { 8 "risk_tier": "low", 9 "is_over_21": true, 10 "normalized_state": "CA" 11 } 12}
output is null when the playbook has no output node or the onboarding has not completed yet, so pending and incomplete runs return null. Fetch the onboarding 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 give your backend more context on what happened during the onboarding than the status alone carries.
Webhooks
Runs that do not finish inline, asynchronous runs, and onboardings that returned pending deliver their decision through the footprint.onboarding.completed webhook. Webhooks covers setting 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 every case, GET /onboardings/{id} returns the current state of the run, in the same shape as the response that created it.
bash1curl https://api.onefootprint.com/onboardings/ob_SRFT2a1mN7DAWJ0VPXkiqK \ 2 -u <SECRET_API_KEY>:
Fetching an onboarding never re-runs it, so it is safe to poll. Use it to:
- Resolve a
pendingrun. Read the finalstatusandoutputonce the run finishes. - Get a link for an asynchronous run. An asynchronous run never returns
continue_onboarding, because the playbook has not executed yet. Once the run reachesincomplete, 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 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.
You can also fetch an onboarding by onboarding_external_id, with the ext_id: prefix:
bash1curl https://api.onefootprint.com/onboardings/ext_id:bc13ca5b-210f-49af-9aba-e98db366484a \ 2 -u <SECRET_API_KEY>:
id. To run the playbook again from scratch, call POST /onboardings with a new onboarding_external_id. See Reonboarding and idempotency.