> ## Documentation Index
> Fetch the complete documentation index at: https://help.statisfy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Statisfy SDK API Reference

> Endpoints, authentication headers, error codes, and TypeScript types for the Statisfy SDK.

This page is the low-level reference behind the SDK components and clients. Most apps never call these endpoints directly — the SDK does — but you'll need them when minting tokens or debugging.

## Base URL & headers

All requests go to your Statisfy gateway (e.g. `https://api.statisfy.app`). Every SDK data request carries:

| Header                       | Value                     | Notes                                                                                               |
| ---------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------- |
| `Authorization`              | `Bearer <session-token>`  | The HS256 session token from `/sdk/auth`.                                                           |
| `X-Statisfy-Publishable-Key` | `pk_live_…` / `pk_test_…` | Identifies your tenant. Required by the chat routes; project routes read the tenant from the token. |

## Authentication

There are two ways a bearer token comes into existence. The SDK accepts either — it just returns whatever `getToken` gives it.

| Path                               | Who mints the token                                                                                                                                                                         | Carries `customer_id`? |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
| **Vendored** (SDK in your own app) | Your backend signs an HS256 JWT with your **JWT secret**. No Statisfy endpoint involved. Required claims: `iat`, `exp`, `email`. See [Publishable Keys & Secrets](/admin/publishable_keys). | No — chat only         |
| **Portal** (Statisfy-hosted)       | `POST /sdk/auth` exchanges the tenant's Clerk/IdP JWT                                                                                                                                       | Yes — chat + projects  |

### `POST /sdk/auth` — exchange an identity for a session token (portal path)

Exchanges a signed-in user's identity for a short-lived Statisfy session token. Not used in the vendored path.

**Request**

```http theme={null}
POST /sdk/auth
Authorization: Bearer <tenant Clerk/IdP JWT>
X-Statisfy-Publishable-Key: pk_live_…
Content-Type: application/json

{ "entity_type": "portal", "entity_id": "<uuid>" }
```

`entity_type` must be `"portal"`; `entity_id` is the portal id, validated against the resolved tenant. Any other `entity_type` is rejected with `unsupported_entity` — there is no "latest published portal" fallback.

**Response** `200`

```json theme={null}
{
  "token": "eyJhbGc…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "portal": { "portal_id": "<uuid>", "commit_sha": "<sha>", "url": "https://…" }
}
```

The minted token is an HS256 JWT signed with your tenant's shared secret, carrying `tenant_id`, `customer_id`, `portal_id`, `email`, `iss` (`statisfy-sdk`), `iat`, and `exp`. The gateway scopes every subsequent request to that `customer_id`.

**Error codes**

| Status | `code`                     | Meaning                                              |
| ------ | -------------------------- | ---------------------------------------------------- |
| 400    | `unsupported_entity`       | `entity_type` is anything other than `"portal"`.     |
| 401    | `auth_token_invalid`       | The user JWT is missing, expired, or invalid.        |
| 401    | `auth_token_missing_email` | The user JWT has no email claim.                     |
| 401    | `publishable_key_missing`  | The `X-Statisfy-Publishable-Key` header wasn't sent. |
| 401    | `publishable_key_invalid`  | Unknown or revoked publishable key.                  |
| 403    | `not_a_customer`           | The user's email isn't a customer of this tenant.    |
| 403    | `customer_unresolved`      | The matched person has no customer id.               |
| 404    | `portal_not_found`         | The `portal_id` is unknown or not published.         |
| 500    | `sdk_secret_unconfigured`  | The tenant's signing secret isn't configured.        |

## Project endpoints (`/sdk/v1`)

Authenticated by the session token's own `tenant_id` + `customer_id` claims (token-only — the publishable key is not required here). All errors return `{ "detail": { "code": "...", "message": "..." } }`.

<Warning>
  These routes scope every query by the signed `customer_id`, so they require a **portal-session** token. A token minted by your own backend without that claim is rejected with `401 not_a_portal_session`, even though it is validly signed and works for chat.
</Warning>

| Method  | Path                                            | Body                    | Response                                    |
| ------- | ----------------------------------------------- | ----------------------- | ------------------------------------------- |
| `GET`   | `/sdk/v1/config/{module}`                       | —                       | `{ data: {...}, meta: { module } }`         |
| `GET`   | `/sdk/v1/projects/{id}`                         | —                       | `{ data: ProjectData, meta: { can_edit } }` |
| `PATCH` | `/sdk/v1/projects/{id}`                         | `{ properties: {...} }` | `{ data: ProjectData, meta }`               |
| `PATCH` | `/sdk/v1/projects/{id}/tasks/{taskId}`          | `{ properties: {...} }` | `{ data: ProjectData, meta }`               |
| `GET`   | `/sdk/v1/projects/{id}/comments`                | —                       | `{ data: ProjectComment[] }`                |
| `POST`  | `/sdk/v1/projects/{id}/comments`                | `{ value }`             | `ProjectComment`                            |
| `GET`   | `/sdk/v1/projects/{id}/tasks/{taskId}/comments` | —                       | `{ data: ProjectComment[] }`                |
| `POST`  | `/sdk/v1/projects/{id}/tasks/{taskId}/comments` | `{ value }`             | `ProjectComment`                            |
| `PATCH` | `/sdk/v1/projects/{id}/comments/{commentId}`    | `{ value }`             | `ProjectComment`                            |

Comments posted through the SDK are always customer-visible (`external`), and an external session only ever sees `external` comments. Editing is **author-only** and text-only; it has no SDK client method in v0.

For `module=portal`, the config response exposes the account's onboarding project under the `onboarding_project__c_` key.

**Error codes**

| Status | `code`                 | Meaning                                             |
| ------ | ---------------------- | --------------------------------------------------- |
| 401    | `user_token_invalid`   | Token signature invalid / claims missing.           |
| 401    | `user_token_expired`   | Token `exp` is in the past — re-mint.               |
| 401    | `not_a_portal_session` | Token has no `customer_id` (not a portal session).  |
| 404    | `module_not_found`     | Unknown config module.                              |
| 404    | `project_not_found`    | Project not found for this customer.                |
| 404    | `task_not_found`       | Task not found on this project.                     |
| 400    | `not_editable`         | A supplied field isn't editable / doesn't exist.    |
| 400    | `invalid_option`       | Value isn't an allowed option for a `select` field. |
| 400    | `invalid_type`         | Value doesn't match the field's type.               |

## Chat endpoints (`/sdk/dw/v1`)

Authenticated by the publishable key (resolves the tenant) plus the session/worker token.

| Method | Path                                            | Body / Query                                               | Response                                                                                   |
| ------ | ----------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `POST` | `/sdk/dw/v1/workers/{workerId}/messages`        | `{ content, thread_id?, structured_input?, form_prompt? }` | `{ reply, thread_id, conversation_id, agent_message_id, pending_approval?, next_action? }` |
| `POST` | `/sdk/dw/v1/workers/{workerId}/messages/stream` | same as above                                              | Server-Sent Events (see below)                                                             |
| `GET`  | `/sdk/dw/v1/workers/{workerId}/conversation`    | `?thread_id=<id>&source=in_app_sdk`                        | `{ thread_id, conversation_id, messages[], next_action? }`                                 |
| `GET`  | `/sdk/dw/v1/workers/{workerId}/conversations`   | `?source=in_app_sdk&limit=<n>`                             | `{ conversations[], total }`                                                               |

<Note>
  The chat endpoints moved under the `/sdk` namespace (from `/dw/v1/...` to `/sdk/dw/v1/...`). The old `/dw/v1/workers/...` paths still work as a deprecated alias, but new integrations should use `/sdk/dw/v1`. The official SDK already calls the new paths — upgrade to pick them up automatically.
</Note>

### Streaming frames

`/messages/stream` emits these SSE frame types:

| Frame                 | Payload                                                          | Meaning                                   |
| --------------------- | ---------------------------------------------------------------- | ----------------------------------------- |
| `text_delta`          | `{ content }`                                                    | Incremental reply text.                   |
| `tool_call_started`   | `{ tool_name, tool_input? }`                                     | A tool was invoked (e.g. `present_form`). |
| `tool_call_completed` | `{ tool_name }`                                                  | Tool succeeded.                           |
| `tool_call_error`     | `{ tool_name, tool_error? }`                                     | Tool failed.                              |
| `done`                | `{ thread_id, conversation_id, agent_message_id, next_action? }` | Turn finished.                            |
| `error`               | `{ message, code? }`                                             | Streaming error.                          |

The headless client (`createDigitalWorkerClient`) maps these frames to the `streamMessage` handler callbacks — see [Digital Worker Chat](/sdk/v0/digital_worker_chat#headless-client).

## TypeScript types

The package is fully typed. Public exports:

**Provider & chat**

```ts theme={null}
type GetToken = () => Promise<string | null>
interface StatisfyConfig { baseUrl: string; getToken: GetToken; publishableKey?: string }
interface StatisfyProviderProps extends StatisfyConfig { children: ReactNode }
interface DigitalWorkerChatProps { workerId?: string; configId?: string; workerName?: string; greeting?: string; /* …see Chat page */ }
type OnboardingModuleProps = Omit<DigitalWorkerChatProps, 'initialThreadId' | 'autoLoadConversation' | 'history' | 'newChat' | 'scope'>
interface DigitalWorkerClientConfig { baseUrl: string; workerId: string; getToken: GetToken; publishableKey?: string }
class DigitalWorkerError extends Error { code?: string; status?: number }

// Hook: read the provider's config (throws a DigitalWorkerError outside the provider).
function useStatisfyConfig(): StatisfyConfig
```

<Note>
  `workerId` is **optional** in the type because a portal component in [config mode](/sdk/v0/digital_worker_chat#config-mode-inside-a-statisfy-portal) takes its worker from `portal.runtime.json` instead. Supply `workerId` for non-portal hosts and `configId` inside a portal — when both are set, `configId` wins and `workerId` is ignored with a dev-mode warning.
</Note>

**Forms & messages**

```ts theme={null}
interface FormField { key: string; label: string; type: string; options?: unknown[]; placeholder?: string; mandatory?: boolean }
interface FormSpec { prompt: string; fields: FormField[] }
type FormRenderer = (spec: FormSpec, ctx: FormRendererContext) => ReactNode
interface FormRendererContext { onSubmit: (values: Record<string, unknown>) => void; disabled: boolean }
interface ChatMessage { id: string; role: 'user' | 'assistant' | 'system'; content: string; /* …toolCalls, formSpec, senderKind, senderName, … */ }
interface ToolCall { toolName: string; status: 'running' | 'completed' | 'error'; error?: string }
interface Attachment { id: string; filename: string; mime_type: string; size_bytes: number }
interface NextAction { type: 'form' | 'approval' | 'complete' | 'handoff'; form_spec?: FormSpec | null; approval_payload?: HumanApprovalPayload | null; message?: string | null }
interface ToolCallEntry { tool_name: string; args: Record<string, unknown>; tool_schema: { fields: FormField[] }; item_id?: string | null; status?: string }
interface HumanApprovalPayload { tools: ToolCallEntry[]; display_type: string }
interface ToolSubmission { tool_name: string; args: Record<string, unknown>; item_id?: string | null }
```

**Portal runtime config** — see [Portal Runtime Config](/sdk/v0/portal_config)

```ts theme={null}
interface PortalFieldRef { $field: string }
type PortalConfigValue = string | PortalFieldRef
interface ResolvedPortalFields { field_values: Record<string, string | null>; locked_values: Record<string, string | null> }
interface UseResolvedFieldsResult { fields: Record<string, string | null>; locked: Record<string, string | null>; resolve: (value: PortalConfigValue | null | undefined) => string | null; loading: boolean; error: Error | null; refetch: () => void }
```

**Project**

```ts theme={null}
type ProjectFieldType = 'text' | 'date' | 'number' | 'boolean' | 'select' | (string & {})
type ProjectStepStatus = 'done' | 'current' | 'upcoming' | (string & {})
interface ProjectSelectOption { value: string; label: string; color?: string }
interface ProjectFieldSchema { key: string; label: string; type: ProjectFieldType; editable: boolean; options?: ProjectSelectOption[] }
interface ProjectTask { id: string; parent_id: string | null; is_milestone: boolean; status: string | null; step_status: ProjectStepStatus; properties: Record<string, unknown>; display: Record<string, string> }
interface ProjectProgress { percent: number | null; completed: number; total: number }   // exported as ProjectProgressData
interface ProjectData { id: string; properties: Record<string, unknown>; display: Record<string, string>; schema: ProjectFieldSchema[]; progress: ProjectProgress | null; tasks: ProjectTask[]; tasks_schema: ProjectFieldSchema[] }
interface ProjectResponse { data: ProjectData; meta: { can_edit?: boolean } & Record<string, unknown> }
interface ProjectConfigResponse { data: Record<string, unknown>; meta: Record<string, unknown> }
interface ProjectComment { id: number; value: string; author_id: string; author_name: string; created_at: string | null; updated_at: string | null; visibility: string; can_edit: boolean }
interface ProjectCommentListResponse { data: ProjectComment[] }
```

The hook result types — `UseProjectResult` and `UseCommentsResult` — are documented field-by-field on the [Project Module](/sdk/v0/projects) page.

## Security notes

* The **publishable key** is safe in client-side code; it only identifies the tenant and can't authenticate on its own.
* The **session token** is short-lived (default 1 hour) and scoped to a single customer. Re-mint it via `/sdk/auth` before expiry inside your `getToken` callback.
* Cross-origin requests are scoped per publishable key — the gateway only accepts SDK requests from origins configured for your key.
* The gateway derives `customer_id` from the signed token, never from client-supplied input, so a customer can only ever read or write their own project and conversations.
