Troubleshooting
Common problems, what causes them, and the fix. Every error code is also listed on Errors.
Tokens and auth
Minting a token returns 403 forbidden
The message says Missing required scope: users:manage. Your API key
was created without the users:manage scope. Scopes can't be added to an
existing key.
Fix: create a new key with users:manage ticked (the dashboard ticks
it by default), deploy it, and revoke the old one.
See API keys.
Minting a token returns 404
POST /v1/users/:user_id/tokens takes poolse's user id (the UUID in
the id field of POST /v1/users), not your external_id. Anything
that isn't a UUID, or a user that doesn't exist in this workspace,
returns 404.
Fix: call POST /v1/users with the external_id first (it is an
upsert, safe to repeat) and mint with the id it returns.
Minting returns 429 rate_limited
More than 60 mints per minute on one API key. Your token route is probably called far more often than needed, for example on every render.
Fix: let the SDK cache the token; it only calls getToken when the
token is about to expire or a request returns 401. Wait for the
Retry-After seconds before retrying.
The SDK throws AuthError
A request got 401. The SDK then asks your getToken for a new token
and retries once; AuthError means the retry got 401 too. Look at the
error's code:
code | Cause |
|---|---|
invalid_user_token | The token is expired or invalid, or its user no longer exists. |
token_revoked | The token was revoked with POST /admin/v1/tokens/revoke. |
user_blocked | The user is blocked (is_blocked: true). |
missing_authorization | No Authorization header was sent. |
malformed_authorization | The header isn't Bearer <token>, for example because getToken returned an empty value. |
Fix: make your token route mint a fresh token on every call instead of returning a cached one. Check it:
const res = await fetch('/api/chat-token', { method: 'POST' });
const { token } = await res.json();
const { exp, sub } = JSON.parse(atob(token.split('.')[1]));
console.log('expires', new Date(exp * 1000), 'user', sub); // expect ~1 hour from now
If exp is in the past, your route returns a stale token. If the clock
on your server is wrong, tokens can look expired; check it with date -u.
WebSocket
useRealtimeStatus() stays on connecting or reconnecting
The socket handshake is being refused, and the SDK keeps retrying. The
causes are the same as for AuthError: an expired, revoked or empty
token, or a blocked user. Fix the token route first.
Other causes:
- Mixed content. The page is served over HTTPS but
wsUrlisws://…. LeavewsUrlunset: the SDK deriveswss://from anhttps://apiUrl. - A network that blocks WebSockets. poolse has no long-polling fallback. REST keeps working, but nothing updates live.
Pass onSocketError in the SDK config to log socket errors.
Messages sent while the app was offline don't appear
Events sent while the socket was down are not replayed. From 3.1,
useMessages and useConversations reload from REST when the socket
reconnects. If you use @poolse/sdk directly, refetch when the status
returns to connected. On React Native, mount useAppStateLifecycle()
so the socket is paused and resumed cleanly. See
Realtime.
400 bad_request or 404 for an id that exists
Every id is a UUID. A value that isn't one gets 404 when it is in the
path, and 400 bad_request ("An id in the request is not a valid UUID.")
when it is in the body or query string, for example quoted_message_id
or attachment_ids. Check you are sending poolse's id, not your own.
Next.js: "two React copies" / useContext returns null
You added @poolse/* to transpilePackages in next.config.mjs. Remove
it and restart next dev. See
Integration.
- transpilePackages: ['@poolse/sdk', '@poolse/react', '@poolse/react-ui'],
Plan limits
402 mau_cap_exceeded
The workspace has its plan's number of monthly active users this month
(Free 1,000, Pro 10,000, Scale 50,000), and this user has not been active
yet this month. Users already active keep working; the count resets on
the 1st (UTC). details has count and cap.
Fix: request a bigger plan on Plan & usage. See Limits & quotas.
402 attachment_too_large or 402 attachment_total_cap_exceeded
Your hosted storage limits are reached. On Free, one file can be at most
1 MB and all attachments together 300 MB; on Pro and Scale, 25 MB per
file and 500 MB in total. Files over 100 MB are refused on every plan with
422 byte_size_too_large.
Fix: delete attachments you no longer need, request a bigger plan, or connect your own bucket on Storage. With your own bucket there is no storage limit from poolse and files can be up to 100 MB. See Bring your own storage.
Webhooks stopped arriving
Check, in this order:
- The webhook is Active on its dashboard page.
- Its events. A webhook with some events ticked only gets those; one with none ticked gets every event.
- The delivery quota. On Free, after 1,000 deliveries in 24 hours new events are not delivered and not queued, and no error is shown anywhere. Look at Webhook deliveries, last 24 hours on Plan & usage.
- The deliveries table on the webhook's page. Retrying or Failed rows show the status code your server returned; hover the code for the error. Redirects count as failures, and so does taking longer than 10 seconds to answer.
402 webhook_count_cap_exceeded
Free allows 5 webhooks. Delete one, or request Pro. A webhook with no events ticked receives every event, so you may need fewer webhooks than you think.
Webhook signatures don't match
- You signed re-encoded JSON instead of the raw body.
- You left
t.out of the signed string. - You used another webhook's secret.
Code that works: Webhook verification.
Attachment upload fails
- The upload URL expired. It is valid for 15 minutes. Request a new one.
Sending a message with an attachment returns 422 send_failed
One cause: the file you stored is bigger than the byte_size you declared when you requested the upload URL. The
upload URL doesn't check the size, so poolse checks it when the
attachment is sent. Request a new upload URL with the real size.
See Attachment uploads.
Sending fails with 403 forbidden for everyone
If every send in the workspace fails with 403 while reads work, check
for a Workspace suspended banner in the dashboard. A suspended or
canceled workspace can't send messages, request upload URLs or create
webhooks. See Billing.
Phones don't ring or get notifications
- Check the pills on the dashboard's Push page. Waiting for devices means no device has registered; Incomplete on iOS means the app registered its VoIP token but not its notification token.
- Use Send a test there to see what Apple or Google answer for each device.
- On iOS, the Bundle ID must match your app's bundle identifier exactly.
- No chat notifications at all? Send chat notifications may be off; it is off for workspaces created before poolse sent them.
Full walkthrough: Push setup and Ringing phones end to end.
Dashboard
Locked out of two-step sign-in
Choose another method on the sign-in screen: a recovery code, or an email code if you turned those on. If none works, email [email protected].
I deleted my workspace and want it back
You can't. Deletion removes the workspace and its encryption key, and poolse can't restore it. You can sign up again with the same email, but you get a new, empty workspace. See Danger zone.
Still stuck?
Email [email protected] with:
- Your Workspace ID (dashboard: Account → Workspace, or
tenant_idfromGET /v1/whoami). - The failing request: method, path, status and the error
code. - What you expected and what happened.