Skip to main content
This is the auth model for every Courier client SDK. One sign-in covers Inbox, Toasts, the preference center, and mobile push tokens.
Courier’s client SDKs authenticate with a JWT (JSON Web Token). Your backend mints a short-lived, scoped token with your Courier API key, and the SDK signs the user in with it. Inbox and Toasts read the same scopes. The preference center and mobile push each add their own. covers embedding the components first.

How the JWT flow works

The private API key that signs a JWT stays on your server, so your backend mints every token, never client code:
1

Your app calls your backend

When a user signs in, your app requests a token from your backend (for example, GET /api/courier-jwt).
2

Your backend calls Courier

Your backend uses your to call the with the scopes the user needs. Keys are per . Use the one matching the environment you’re issuing tokens for.
3

Your backend returns the JWT to the client

The client passes the token to the SDK through signIn({ userId, jwt }).

1. Issue a token

Call from your backend with your API key and a scope string:

Scopes

scope is one space-separated string. Copy the line for what you are building, and swap in your user’s id.
The user_id is yours, not Courier’s.
Use whatever your app already calls this user, such as a database id or your auth provider’s sub. Nothing has to exist in Courier first, since signing in registers the user. The same id goes in two places, user_id: in the scope and the userId you pass to signIn, and they have to match.
Inbox and Toasts
Add the preference center
Add mobile push
Everything
Every string starts with user_id: and the two inbox scopes. What each one grants: For every scope value, see the .

2. Sign in on the client

Signing in tells the SDK which user it is fetching messages for. Until it runs, the components render empty. Pass the JWT to signIn when your app starts or the user logs in:

3. Read the signed-in user

Every platform exposes the current user and a listener that fires on sign-in and sign-out. Use the listener to clear app state on sign-out, or to re-render once credentials are restored on launch.
On mobile, credentials persist across app sessions. The SDK restores the last signed-in user on launch, so no cold start needs signIn. Call it again only when the user changes or the token expires.

4. Sign out

Call signOut when the user logs out of your app. It clears stored credentials, disconnects the real-time socket, and on mobile stops associating push tokens with that user.

5. Refresh tokens

Set expires_in to match your session length. A short-lived token can expire while a tab stays open, which silently empties the inbox. The SDKs do not refresh tokens. Before the current token expires, mint a new JWT on your backend and call signIn again with it.
expires_in is optional, and omitting it mints a token that never expires. Always set it. A leaked token with no exp grants that user’s inbox forever. The only fix is rotating the API key that signed it.

EU-hosted workspaces

Skip this section unless your workspace is on the . Every client SDK defaults to Courier’s US hosts. Pass regional URLs through apiUrls on signIn:
The web packages re-export three helpers from @trycourier/courier-js:
Mint the JWT against the same region as the client. For EU, your backend must call https://api.eu.courier.com/auth/issue-token. A US-issued token will not authenticate against EU hosts.
For the CourierApiUrls type shape, see .

Troubleshooting

The inbox loads but shows no messages. The SDK connects and throws nothing, so check these four causes in order. Decode the JWT at jwt.io for the first three:
  1. exp is in the future. A token that expired between page loads signs in without error and returns nothing.
  2. scope includes inbox:read:messages and inbox:write:events. Without them the connection succeeds and the feed is empty.
  3. The user_id: in the scope matches the userId you pass to signIn. A mismatch reads an empty inbox belonging to whoever the token names.
  4. The tenantId matches the send. A tenantId filters the feed, and the unread count, to that tenant. A message sent without a tenant, or in a different one, will not appear. Signing in without a tenantId applies no filter. See .
Preferences editor is empty or read-only. Add read:preferences write:preferences to the scope string. Push tokens never appear on the user. Add write:user-tokens to the scope string. covers the registration that follows. Everything 401s on an EU workspace. The token and the client must be in the same region. Confirm your backend mints against https://api.eu.courier.com/auth/issue-token and the client passes EU apiUrls.

FAQ

The token signed in but lacks read access. Confirm the scope string includes inbox:read:messages and inbox:write:events, and that exp has not passed.
They read the same feed, so one signIn with the same JWT and scopes covers both.
Older SDK versions used a different authentication method. To move to JWT, see the .