Prerequisites
- A React 18 or 19 application.
- A publishable key for your workspace (
pk_live_…orpk_test_…) — see Publishable Keys & Secrets. - Embedding in your own app only: a JWT secret (generated alongside the publishable key) and a backend that can sign a short-lived JWT with it. The secret must never reach the browser. A Statisfy-hosted Portal needs neither —
<PortalAuthGate>owns sign-in, so there is no secret to hold and no backend to write.
The worked example below is the own-app path, where your backend mints the token. Building a Portal instead? Steps 1 and 4 are the same; skip step 3 entirely (the gate supplies the provider) and read the Portal half of step 2 in place of the own-app half.
1. Install
The SDK ships as two packages. Install the one whose surfaces you render — or both.- Digital Worker chat
- Portal shell + projects
- Both
--dw-* token layer, so importing both is idempotent.
@statisfy/sdk-core — the shared foundation both packages resolve at runtime — is a required peer dependency of each, so npm 7 and newer installs it for you and the commands above are all you need. You never import it directly either; everything it holds is re-exported from the package you installed.What matters is that it resolves to exactly one copy: two would leave an app rendering both surfaces with two <StatisfyProvider> contexts, and a chat that cannot see the provider your portal shell rendered. So if your install skips peer dependencies — legacy-peer-deps in your .npmrc or on the command line (a common React 19 workaround), or Yarn — name it yourself, at the same version as the surface package:2. Supply a session token
Every SDK request carries a short-lived HS256 bearer token. The SDK never mints it — it calls yourgetToken callback and sends whatever you return. How you produce it depends on where the SDK is running.
In your own app: your backend signs it
Sign a short-lived JWT with your JWT secret on your server, and expose an endpoint your frontend can call. Statisfy requires the standardiat / exp claims plus the end user’s email, which is how the gateway resolves who is asking.
In a Statisfy Portal: <PortalAuthGate> handles it
A hosted portal has no backend of its own, so its customers sign in passwordless: Statisfy emails a one-time code, and the exchange returns a session token plus a rotating refresh token that keeps them signed in across reloads.
You don’t implement any of that. Wrap your portal in <PortalAuthGate> — it renders the sign-in form, owns the token lifecycle, and supplies StatisfyProvider internally, so nothing below it touches getToken:
customer_id. A code is only ever sent to an address that matches; a request for any other address succeeds silently and sends nothing, so the sign-in screen can’t be used to probe your customer list.
The session token is short-lived (default 1 hour).
<PortalAuthGate> re-mints it silently before it expires using the stored refresh token, so customers stay signed in without seeing the form again.createPortalAuthClient export gives you the same token lifecycle without the React components.
3. Wrap your app in StatisfyProvider
StatisfyProvider supplies the gateway URL and authentication to every SDK component via React context. Set it up once, high in your tree. It is re-exported identically from both packages — import it from whichever you installed (@statisfy/ai-react for a chat-only app):
4. Render your first component
Now any SDK component works anywhere beneath the provider — no connection props needed.401 not_a_portal_session on a self-signed token.
5. Theming (optional)
The SDK’s styles are self-contained and ship inside@layer, so they won’t collide with your app’s CSS — and any plain, unlayered rule you write wins over them without !important. There are two override mechanisms:
--dw-*CSS variables (~31 tokens: colors, radius, elevation, focus rings, status colors, motion), set on:rootor any ancestor. Colors are space-separated RGB triplets, not hex.data-dw-partanatomy selectors to restyle exactly one element.
data-dw-theme="dark" (or "auto" to follow the visitor’s OS) on your html element. Without the attribute the widget stays light.
See Theming for the full variable tables, the guaranteed part names, and the dark palette.
Troubleshooting
401 user_token_invalid
401 user_token_invalid
The bearer token’s signature didn’t verify. Make sure
getToken returns the session token (in a portal, the one from /auth/verify — not the opaque refresh token), that it hasn’t expired, and that you minted it against the same tenant/environment your gateway points at.403 not_a_customer
403 not_a_customer
The signed-in user’s email isn’t a person on any account in your tenant. Add them as a contact, or sign in as a known customer.
Components throw 'must be rendered inside <StatisfyProvider>'
Components throw 'must be rendered inside <StatisfyProvider>'
An SDK component is mounted outside the provider. Move it beneath
<StatisfyProvider> — or, in a portal, beneath <PortalAuthGate>, which supplies the provider itself.