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 whatevergetToken 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 — noAuthorization 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
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
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
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.
Contacts on several accounts — switching account
A session token is scoped to exactly one account (itscustomer_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": "..." } }.
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 & chatworkerId 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.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.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
getTokencallback — from your own backend in the vendored path, or viaPOST /auth/tokenin 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_idfrom the signed token, never from client-supplied input, so a customer can only ever read or write their own project and conversations.