Skip to main content
This guide takes you from an empty React app to a working Digital Worker chat embedded in your product.

Prerequisites

  • A React 18 or 19 application.
  • A publishable key for your workspace (pk_live_… or pk_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.
Import the stylesheets once, near your app’s entry point. Both sheets carry the same --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 your getToken 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 standard iat / exp claims plus the end user’s email, which is how the gateway resolves who is asking.
Never sign tokens in the browser. Only the publishable key is safe to ship client-side — the JWT secret is a server credential.

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:
Every portal template already ships with this wiring, so in practice you only edit what’s inside the gate. Because the gate provides the provider, skip step 3 in a portal. The email the customer receives — sender, subject, body, and how long the code stays valid — is configured per portal on the Auth tab in Portal Studio. See Customizing the Login Email. The address a visitor enters must belong to a customer (a person on an account) of your tenant — that’s how the session is bound to a 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.
Only the portal sign-in produces a customer_id claim, and the project endpoints require one. With a token your own backend signed, the Digital Worker chat works but OnboardingProject / ProjectModule / useProject return 401 not_a_portal_session.
If you need the raw endpoints — you’re not using React, or you’re building your own sign-in UI — see Portal sign-in. The 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):
Pass a stable getToken (wrap it in useCallback). The provider memoizes its context value on getToken identity, so an inline arrow function would re-render every consumer on each render.

4. Render your first component

Now any SDK component works anywhere beneath the provider — no connection props needed.
That’s a fully working chat: streaming replies, conversation history, unread tracking, and inline forms. See Digital Worker Chat for every prop and the headless client. To show a customer’s onboarding project instead, render the Project Module:
This needs a portal session — see the warning in step 2 about 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 :root or any ancestor. Colors are space-separated RGB triplets, not hex.
  • data-dw-part anatomy selectors to restyle exactly one element.
Dark mode is opt-in: set 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

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.
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.
An SDK component is mounted outside the provider. Move it beneath <StatisfyProvider> — or, in a portal, beneath <PortalAuthGate>, which supplies the provider itself.