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_onboarding token: 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
1curl -X POST https://api.onefootprint.com/users \
2  -u <SECRET_API_KEY>: \
3  -d '{
4    "id.first_name": "Jane",
5    "id.last_name": "Doe",
6    "id.dob": "1990-01-01",
7    "id.ssn9": "123-45-6789",
8    "id.address_line1": "1 Main St",
9    "id.city": "San Francisco",
10    "id.state": "CA",
11    "id.zip": "94105",
12    "id.country": "US"
13  }'

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:

bash
1curl -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  }'
json
1{
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 is pending, 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 the footprint.onboarding.completed webhook; 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:

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

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

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

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

json
1{
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 link to the user by email, SMS, or a button in your app.
  • Embedded. Pass the token to the SDK as onboardingSessionToken:
javascript
1import "@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:

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

bash
1curl 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 pending run. Read the final status and output once 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 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 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:

bash
1curl https://api.onefootprint.com/onboardings/ext_id:bc13ca5b-210f-49af-9aba-e98db366484a \
2  -u <SECRET_API_KEY>: