# 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 <SECRET_API_KEY>: \
  -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 <SECRET_API_KEY>: \
  -d '{
    "fp_id": "fp_id_K0q6Eh6Rr3WOOfFBLPiHsr",
    "key": "<PLAYBOOK_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 <SECRET_API_KEY>: \
  -d '{
    "fp_id": "fp_id_K0q6Eh6Rr3WOOfFBLPiHsr",
    "key": "<PLAYBOOK_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 <SECRET_API_KEY>: \
  -d '{
    "fp_id": "fp_id_K0q6Eh6Rr3WOOfFBLPiHsr",
    "key": "<PLAYBOOK_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 <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](#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 <SECRET_API_KEY>:
```

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