HTTP API
Base path /api/v1. JSON in, JSON out.
Authentication
| Credential | Header | Who holds it |
|---|---|---|
Secret API key — fd_live_…, fd_test_… | Authorization: Bearer <key> | your backend only |
| Session token — minted per session and role | Authorization: Bearer <token> or x-session-token: <token> | the actor's browser |
| Callback token — per async action call | Authorization: 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 | |
|---|---|
role | the first actor's role |
externalUserId | your id for the person |
prefill | { fieldId: value } to start with; unknown field ids → 422 |
period | which 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 /secrets | the environment's secret names and the signing secret's prefix |
PUT /secrets/:NAME { "value": "…" } | set one (UPPER_SNAKE_CASE) |
DELETE /secrets/:NAME | remove one |
POST /secrets/signing | create 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/:sid | the state, as this role may see it |
GET /sessions/:sid/events | the ledger, redacted for this role → { events, head, status } |
GET /sessions/:sid/outline | every stage with the fields this role may read |
POST /sessions/:sid/events | { events: [{ type: 'field', field, value }, …] } → { state, accepted, rejected, head } |
POST /sessions/:sid/submit | validate and submit the active stage |
POST /sessions/:sid/actions/:actionId/run | ask 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/:docId | a 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/complete | the 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/:pid | a short-lived link, for a role that may read the field |
WS /sessions/:sid/live | first 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
| Route | Auth |
|---|---|
POST /sessions/:sid/actions/:actionId/callback | callback token — { ref, result } or { ref, error } for a webhook that answered 202 |
POST /providers/stripe-identity/:environmentPid/webhook | Stripe's signature |