Run a playbook

9 min read

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.

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; never send it 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  }'

See Vault fields for the full set of attributes you can vault, and Migrating user 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 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: see 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. 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 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
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  }'

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
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.

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.

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
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).

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
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 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.

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
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 your playbook has no output node or when onboarding hasn't completed yet; 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 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 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
1curl https://api.onefootprint.com/onboardings/ob_SRFT2a1mN7DAWJ0VPXkiqK \
2  -u <SECRET_API_KEY>:

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