Errors
Every error response, from every endpoint, uses the same envelope. This includes unknown routes, malformed JSON and server crashes.
{
"error": {
"code": "mau_cap_exceeded",
"message": "This workspace has reached its plan's monthly active users.",
"doc_url": "https://poolse.dev/docs/api/errors#mau_cap_exceeded",
"details": { "count": 1000, "cap": 1000 }
}
}
| Field | Type | Description |
|---|---|---|
error.code | string | Machine-readable. Switch on this. |
error.message | string | Human-readable explanation. Log it. It is not meant for end-users and its wording can change. |
error.doc_url | string | A link to this code's section on this page. |
error.details | object, optional | Extra data. For validation_failed, a map of field name to a list of messages. For quota codes, the numbers involved. |
Retrying
5xx: retry with exponential backoff and jitter.429: wait forRetry-After(seconds) ordetails.retry_after_ms, then retry.- Other
4xx: do not retry unchanged. Fix the request, the credential or the plan.
Use an Idempotency-Key so that a retried
POST does not create a second resource.
Codes by status
| Status | Codes |
|---|---|
| 400 | bad_request, invalid_parameter, invalid_cursor, invalid_token, missing_token |
| 401 | missing_authorization, malformed_authorization, invalid_api_key, revoked_api_key, invalid_user_token, token_revoked, user_blocked, unauthorized |
| 402 | mau_cap_exceeded, user_cap_exceeded, attachment_too_large, attachment_total_cap_exceeded, webhook_count_cap_exceeded, webhooks_not_in_plan, subscription_inactive, call_minutes_exhausted |
| 403 | forbidden, cross_tenant, attachments_not_in_plan |
| 404 | not_found, user_not_found, message_not_found, no_live_call |
| 406 | not_acceptable |
| 409 | message_deleted, call_full, call_not_answered, read_receipts_fixed |
| 410 | call_finished |
| 413 | payload_too_large |
| 422 | validation_failed, byte_size_too_large, member_add_failed, send_failed, edit_failed, reaction_failed, user_scope_required, upload_request_failed, create_failed, replay_failed |
| 429 | rate_limited, lazy_provision_rate_limited |
| 5xx | internal_error, storage_error, sfu_not_configured, auth_pipeline_misconfigured |
Request errors
bad_request
400. The request could not be read. Two causes:
- the body is not valid JSON;
- an id in the body or query string is not a valid UUID, for example
quoted_message_id,reply_to_id, anattachment_idsormentionsentry,message_idon mark read, the replyidonPOST /v1/notifications/reply, orcursoronGET /v1/audit_logs. The message is thenAn id in the request is not a valid UUID.
An id in the path that is not a UUID is 404 not_found instead.
not_found
404. The thing does not exist in your tenant. Also returned for an
unknown route, and for any id in the path that is not a UUID. Some
endpoints also return it when a conversation exists but you cannot see
it: the Call history conversation endpoint does this.
Most conversation and message endpoints return 403 forbidden to a
non-member instead.
not_acceptable
406. The Accept header asks for a format other than JSON.
payload_too_large
413. The request body is larger than the server accepts.
invalid_parameter
400. A query parameter has the wrong format: conversation_id not
a UUID, or since / until not an ISO 8601 time. Returned by the
Call history endpoints.
invalid_cursor
400. The cursor passed to a Call history list
is not one the server issued.
validation_failed
422. The body failed validation. details maps each field to its
problems:
{
"error": {
"code": "validation_failed",
"message": "Request failed validation.",
"doc_url": "https://poolse.dev/docs/api/errors#validation_failed",
"details": { "external_id": ["can't be blank"] }
}
}
Some endpoints return this code without details (for example
Devices and Notification actions).
The message then says which field is wrong.
byte_size_too_large
422. POST /v1/attachments/upload-url with a byte_size over the
hard limit of 100 MiB, whatever your plan.
member_add_failed
422. Adding a member failed for a reason other than validation: a
direct conversation already has 2 members, or a group has reached its
member_limit. The message names the user.
send_failed
422. A message could not be sent. The most common causes:
reply_to_id or quoted_message_id names no message, the quoted
message is in another conversation, or an attachment id is unknown,
already used on another message, or was uploaded by someone else. When
an attachment's stored file is larger than the byte_size declared for
it, the message contains attachment_size_mismatch.
edit_failed
422. A message edit failed for a reason other than validation.
reaction_failed
422. Adding or removing a reaction failed for a reason other than validation.
user_scope_required
422. The action needs an end-user (reactions, mark read). It is not returned to normal JWT callers.
upload_request_failed
422. An attachment upload URL could not be issued for a reason not covered by another code.
create_failed
422. A webhook could not be created for a reason not covered by another code.
replay_failed
422. A webhook delivery could not be replayed.
Authentication errors
missing_authorization
401. No Authorization header.
malformed_authorization
401. The Authorization header is not Bearer <credential>.
invalid_api_key
401. The API key is malformed, unknown, or its secret does not match.
revoked_api_key
401. The API key was revoked in the dashboard.
invalid_user_token
401. The end-user JWT has a bad signature, has expired, or its user no longer exists. Mint a new one.
token_revoked
401. The end-user JWT was revoked with POST /admin/v1/tokens/revoke.
user_blocked
401. The user behind the JWT or notification reply grant has
is_blocked: true. (The call grant endpoints report a blocked user as
unauthorized instead.) See
Users.
unauthorized
401. A call grant, call-control token or reply grant is missing,
expired, of the wrong kind, or for a different call. Returned by the
grant endpoints on Calls and
Notification actions. On the call grant
endpoints (answer, decline, hang up, reconnect, ring) it is also
returned, with the message User is blocked., when the user behind the
credential (ring grant, control token, or the JWT decline accepts) is
blocked, even for a grant issued before the block.
invalid_token
400. POST /admin/v1/tokens/revoke was given a token that does not
verify, has already expired, or lacks the required claims.
missing_token
400. POST /admin/v1/tokens/revoke was called without a token
field.
Permission errors
forbidden
403. You are authenticated but not allowed. The causes:
- the API key lacks the scope the endpoint needs (the message names it);
- the user is not a member of the conversation, or lacks the role the action needs;
- a message send while your subscription is inactive.
cross_tenant
403. POST /admin/v1/tokens/revoke was given a token from another
tenant.
attachments_not_in_plan
403. Your plan does not include attachments.
Plan errors
These are quotas tied to your plan. Upgrading lifts them. See Limits & quotas.
mau_cap_exceeded
402. The workspace already has its plan's number of monthly active
users this month, and this user has not been active yet this month.
Returned by any request made with an end-user JWT, and by the call-grant
and notification-action routes; the socket refuses to connect for the
same reason. Users already active this month are never
refused. details: { count, cap }. See
Limits & quotas.
user_cap_exceeded
402. Creating a user would exceed a cap on how many users a
workspace may have. No current plan has such a cap (plans are sized by
monthly active users
instead), so you will not see it today. It would come from
POST /v1/users, POST /v1/conversations or
POST /v1/conversations/:conversation_id/members creating users, with
details: { count, cap }.
attachment_too_large
402. The file is larger than your plan's per-file limit for hosted
storage (Free 1 MB, Pro and Scale 25 MB). Not returned while the workspace
has its own bucket connected. details: { bytes, cap }.
attachment_total_cap_exceeded
402. The file would push your tenant's attachments in hosted storage
over your plan's total (Free 300 MB, Pro and Scale 500 MB). Not returned
while the workspace has its own bucket connected; see
Bring your own storage.
details: { used, would_use, cap }.
webhook_count_cap_exceeded
402. You already have as many webhooks as your plan allows.
details: { count, cap }.
webhooks_not_in_plan
402. Your plan does not include webhooks.
subscription_inactive
402. Your subscription is past due or cancelled. Returned when creating webhooks and requesting attachment uploads.
State errors
user_not_found
404. POST /v1/users/:user_id/tokens with a user_id that is not a
user in your tenant. user_id is the poolse id, not your
external_id.
message_not_found
404. POST /v1/conversations/:conversation_id/read with a
message_id that is not in that conversation.
no_live_call
404. GET /v1/conversations/:conversation_id/call when nothing is
live on the conversation. 409 from
POST /v1/conversations/:conversation_id/call-token for the same
reason: start a call or room first.
call_minutes_exhausted
402. The workspace has used this month's call minutes for the
medium. Returned when placing a call, starting or joining a room, joining
a live call, ringing more people in, or fetching a media token. details: { media, used, cap },
media being audio or video. Calls already connected carry on. See
Limits & quotas.
message_deleted
409. Editing or reacting to a deleted message.
call_full
409. Joining would take the call past the workspace's people-per-call limit. Someone already in the call can always rejoin.
call_not_answered
409. POST /v1/calls/:id/ring on a call nobody has answered yet.
read_receipts_fixed
409. PATCH /v1/me with read_receipts while the workspace does
not let users choose. See Current user.
call_finished
410. The call has ended, or the server no longer knows the call.
Rate limits
These are abuse limits, the same on every plan. They reset on their own.
rate_limited
429. Too many POST /v1/users/:user_id/tokens calls for this API
key: the limit is 60 per minute. The Retry-After header gives the
seconds to wait.
lazy_provision_rate_limited
429. The end-user referenced more than 50 unknown external_ids
within an hour, through member_external_ids or external_ids. The
Retry-After header gives the seconds to wait, and
details: { retry_after_ms } the same in milliseconds.
Server errors
internal_error
500. Something failed on the server. Retry with backoff.
storage_error
502. The storage bucket did not issue a download URL. Retry. With your own bucket connected, also check that the stored key still works (Test connection on the dashboard's Storage page).
sfu_not_configured
503. This deployment has no media server, so call endpoints cannot issue media credentials.
auth_pipeline_misconfigured
500. A server-side configuration bug. Report it if you see it.