Skip to main content
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.com). Every SDK data request carries:

Authentication

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

Portal sign-in

A Statisfy-hosted portal has no backend of its own, so its customers sign in passwordless: Statisfy emails a one-time code, the portal exchanges that code for a session token, and a rotating refresh token keeps the customer signed in afterwards. All three routes take a JSON body only — no Authorization header and no publishable key — and live on the same base URL as the rest of the API.
You rarely call these yourself. <PortalAuthGate> from @statisfy/portal-react runs the whole flow — it renders the sign-in form, stores the refresh token, silently re-mints the access token before it expires, and supplies StatisfyProvider internally. Every portal template ships with it already wired up.
entity_type must be "portal" and entity_id is the portal’s id; together they are how the service resolves your tenant. Any other entity_type is rejected with unsupported_entity — there is no “latest published portal” fallback.

POST /auth/otp — email a one-time code

Response 200 — an empty object, {}. The response is deliberately empty and identical for every address: it never reveals whether the email belongs to one of your customers. A code is created and sent only when the email matches a person on one of your accounts. A malformed or unknown entity_id is the one exception — that’s a misconfigured portal, not an enumeration signal, so it returns 404. The code is a 6-digit number and expires after the window set in the portal’s Auth tab (1–60 minutes, default 10) — see Customizing the Login Email.

POST /auth/verify — exchange the code for a session

Response 200
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. refresh_token is opaque — store it, don’t parse it.

POST /auth/token — refresh the session

No entity_type / entity_id is needed — the refresh token carries its own context. The response is the same shape as /auth/verify, including a new refresh_token.
Refresh tokens rotate on every use. Replace the stored token each time, and never present one that has already been exchanged — reuse revokes the whole token family and signs the customer out. A refresh token is valid for at most 30 days.

Contacts on several accounts — switching account

A session token is scoped to exactly one account (its customer_id claim), so a contact associated with more than one account switches by re-minting, not by passing an account id per request: customer_id is validated against that contact’s own accounts on every call, so it can only ever narrow to something they already had; an account they’ve since been removed from quietly resolves back to their primary, and the response’s customer_id reports the account actually bound. The SDK wraps this: listAccounts() and switchAccount(customerId) on the portal auth client, with getActiveCustomerId() for the current one. accounts is a single entry for most viewers — render a switcher only when it holds two or more. Error codes

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": "..." } }.
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.
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 does not yet have an SDK client method. For module=portal, the config response exposes the account’s onboarding project under the onboarding_project__c_ key — the Onboarding Project lookup field on the Account, resolved for the account the token is scoped to. null means that account’s field is unset. See Portal SDK: Project API. Error codes

Chat endpoints (/sdk/dw/v1)

Authenticated by the publishable key (resolves the tenant) plus the session/worker token.
The chat endpoints live under the /sdk namespace (/sdk/dw/v1/...). The old /dw/v1/workers/... paths were a deprecated alias and have been removed — they now 404. The official SDK calls the /sdk paths; upgrade if you are pinned to an older build that still calls /dw/v1.

Streaming frames

/messages/stream emits these SSE frame types: The headless client (createDigitalWorkerClient) maps these frames to the streamMessage handler callbacks — see Digital Worker Chat.

TypeScript types

The package is fully typed. Public exports: Provider & chat
workerId is optional in the type because a portal component in config mode 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.
Forms & messages
Portal runtime config — see Portal Runtime Config
Project
OnboardingProject’s configId is required — there’s no embed-mode fallback, so a portal must set it for the component to ever resolve. ProjectModule’s configId and projectId are both optional: supply projectId outside a portal and configId inside one — when both are set, configId wins and projectId is ignored with a dev-mode warning. A ProjectModule pinned via configId resolves only for the account that owns the pinned project; every other signed-in account sees the empty state.
The hook result types — UseProjectResult and UseCommentsResult — are documented field-by-field on the Project Module 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 before expiry inside your getToken callback — from your own backend in the vendored path, or via POST /auth/token in a portal. <PortalAuthGate> already does this for you.
  • 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.