Skip to content

HTTP API

Base path /api/v1. JSON in, JSON out.

Authentication

CredentialHeaderWho holds it
Secret API keyfd_live_…, fd_test_…Authorization: Bearer <key>your backend only
Session token — minted per session and roleAuthorization: Bearer <token> or x-session-token: <token>the actor's browser
Callback token — per async action callAuthorization: Bearer <token>the provider that answers a webhook

An API key belongs to one environment and sees only its sessions. A session token is good for one session as one role; the engine takes the actor from it, never from the request body.

Your backend (API key)

POST /workflows/deploy

json
{ "workflowId": "expense_claim_v1", "version": "1.0.0", "manifest": {  }, "draft": false }

Creates or updates the workflow. Every distinct manifest is kept as a revision; running sessions stay on theirs. clipless deploy calls this. 422 with problems if a role's conditions read answers it may not see.

POST /workflows/:workflowId/sessions

Start a session.

Body
rolethe first actor's role
externalUserIdyour id for the person
prefill{ fieldId: value } to start with; unknown field ids → 422
periodwhich run of a recurring workflow this is — see Recurring. Needs externalUserId
prefillFrom"previous" or a session id: start from that user's earlier run
json
{
  "session": "Xk2…",
  "token": "…",
  "created": true,
  "carried": { "from": "…", "fields": [  ], "dropped": [ { "field": "attest", "reason": "not carried" } ] },
  "state": {  }
}

With period, asking again for the same user and period returns the existing session with "created": false and a fresh token. carried is present when prefillFrom found a run.

POST /sessions/:sid/tokens

Mint a token for another role — the handoff.

json
{ "role": "role:manager", "externalUserId": "mgr_7" }  →  { "token": "…" }

GET /workflows/:workflowId/sessions

What is in flight, newest first. Filters: ?status=, ?externalUserId=, ?period=, ?limit= (max 200).

json
{ "workflow": "expense_claim_v1", "sessions": [
  { "session": "Xk2…", "status": "in_progress", "externalUserId": "user_42", "period": null,
    "currentStageId": "review", "events": 14, "version": "1.0.0", "startedAt": "…", "updatedAt": "…" }
] }

GET /workflows/:workflowId/sessions/:sid

One session in full: replayed state (vault decrypted), the whole ledger, and sensitive — the ids of pii and vault fields, for tools that mask them.

GET /workflows/:workflowId/sessions/:sid/documents/:docId

A document an action produced (PDF).

GET /workflows/:workflowId/sessions/:sid/uploads/:pid

A stored file: { pid, name, type, size, sha256, url, expiresIn }, url a presigned GET.

Secrets

GET /secretsthe environment's secret names and the signing secret's prefix
PUT /secrets/:NAME { "value": "…" }set one (UPPER_SNAKE_CASE)
DELETE /secrets/:NAMEremove one
POST /secrets/signingcreate or rotate the webhook signing secret; returned once

STORAGE_BUCKET, STORAGE_ACCESS_KEY_ID, STORAGE_SECRET_ACCESS_KEY (and optionally STORAGE_ENDPOINT, STORAGE_REGION) connect the environment's bucket for file uploads.

Secret API keys only. Values are never returned.

The actor's browser (session token)

<Workflow runtimeToken> uses these; you rarely call them yourself.

Route
GET /sessions/:sidthe state, as this role may see it
GET /sessions/:sid/eventsthe ledger, redacted for this role → { events, head, status }
GET /sessions/:sid/outlineevery stage with the fields this role may read
POST /sessions/:sid/events{ events: [{ type: 'field', field, value }, …] }{ state, accepted, rejected, head }
POST /sessions/:sid/submitvalidate and submit the active stage
POST /sessions/:sid/actions/:actionId/runask for a field's action → { ok, cached, waitingOn, emitted, state, head }; cached when it already ran on this value; { "retry": true } reruns a failure; 409 if this role may not
GET /sessions/:sid/documents/:docIda generated document
POST /sessions/:sid/uploads{ field, name, type, size }{ upload, url, method, headers, expiresIn }: a presigned PUT to your bucket. 409 without a bucket, 422 if the file does not fit the field
POST /sessions/:sid/uploads/:pid/completethe file is in the bucket: the engine hashes it and records it → { ok, file, emitted, state, head }; 409 if nothing arrived
GET /sessions/:sid/uploads/:pida short-lived link, for a role that may read the field
WS /sessions/:sid/livefirst message { "type": "auth", "token": "…" }; then { head, status } whenever the session moves

Submit answers 200 with { ok, completed, waitingOn, emitted, state, head }emitted is what the engine did (actions, routing), waitingOn an action the session is now parked on. 422 with errors: [{ fieldId, message }] when the stage does not validate, or with blockedBy and reason when a blocking action refused. 403 when this role may not submit the stage.

Events other than answers and navigation are refused: stage and action events are the engine's. A file field accepts only a value with files removed — files are added by upload.

Providers

RouteAuth
POST /sessions/:sid/actions/:actionId/callbackcallback token — { ref, result } or { ref, error } for a webhook that answered 202
POST /providers/stripe-identity/:environmentPid/webhookStripe's signature