Users
An end-user is one person in your app. poolse knows them by two ids:
id: poolse's UUID for the user. Token minting, mentions, call history and membership removal use it.external_id: your own id for the user, any string up to 255 characters, unique within your tenant. Conversation and member creation use it.
Everything on this page is called from your backend with an API key.
There is no API to list, fetch or delete users with an API key. A user
can read their own record with GET /v1/me.
Provision or update a user
/v1/usersCreates the user if external_id is new in your tenant. Otherwise
updates the existing user. Any valid API key works; no scope is needed.
On an existing user, only the fields you send change. A field you leave
out keeps its stored value. So {"external_id": "u_42"} is a safe
"make sure this user exists" call: it never clears a profile or unblocks
a blocked user.
You do not have to call this before a user appears in a conversation.
Unknown external_ids passed to POST /v1/conversations
(member_external_ids) or POST /v1/conversations/:conversation_id/members
(external_ids) are created automatically, with only external_id
set. Call this endpoint when you want to set a profile, block someone,
or get the poolse id to mint a token.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
external_id | string | yes | Your id for the user. 1–255 characters. |
display_name | string | null | no | Up to 255 characters. poolse puts it in call summaries and call media tokens. |
avatar_url | string | null | no | Up to 2048 characters. |
custom_data | object | no | Free-form JSON. Replaces the stored object as a whole. It is not merged. |
is_blocked | boolean | no | true blocks the user: requests with their JWTs get 401 user_blocked, the call grant endpoints refuse their ring grants and control tokens (401 unauthorized, even grants issued earlier), and their open realtime sockets are closed. Reply grants already sent in notifications are refused too (401 user_blocked on /v1/notifications/reply and /read). false unblocks them. |
Response
200, for both a new and an existing user:
{
"id": "8a7e2f9c-1234-4d5e-8f90-a1b2c3d4e5f6",
"tenant_id": "65787825-3152-4c1e-9b8a-0d6f4e2a7c11",
"external_id": "u_42",
"display_name": "Ada",
"avatar_url": null,
"custom_data": {},
"last_seen_at": null,
"is_blocked": false,
"inserted_at": "2026-06-02T08:14:00.123456Z",
"updated_at": "2026-06-02T08:14:00.123456Z"
}
curl -X POST https://api.poolse.dev/v1/users \
-H "Authorization: Bearer $POOLSE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"external_id":"u_42","display_name":"Ada"}'
Errors
| Code | Status | When |
|---|---|---|
validation_failed | 422 | external_id missing or empty, or a field too long. details names the field. |
Plus the API-key errors listed on Authentication.
Mint a user token
/v1/users/:user_id/tokensMints a 1-hour JWT for one user. Your app sends it as
Authorization: Bearer <token> to the end-user endpoints and the
realtime socket. Each call returns a new token. Earlier tokens keep
working until they expire.
Path parameters
| Name | Type | Description |
|---|---|---|
user_id | uuid | The user's poolse id from POST /v1/users. Not your external_id. |
No request body.
Response
200:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.…",
"token_type": "bearer",
"expires_in": 3600,
"expires_at": "2026-06-02T09:14:00Z"
}
| Field | Type | Description |
|---|---|---|
token | string | The JWT. Treat it as a credential. |
token_type | string | Always "bearer". |
expires_in | integer | Seconds until it expires. |
expires_at | string | ISO 8601 expiry time. |
curl -X POST https://api.poolse.dev/v1/users/$POOLSE_USER_ID/tokens \
-H "Authorization: Bearer $POOLSE_API_KEY"
Errors
| Code | Status | When |
|---|---|---|
forbidden | 403 | The API key lacks users:manage. |
user_not_found | 404 | No user with this id in your tenant. |
not_found | 404 | user_id is not a UUID, for example when you pass an external_id by mistake. |
rate_limited | 429 | More than 60 mints in a minute with this API key. Wait for Retry-After seconds. Requests refused for a missing scope count too. |
A token minted for a blocked user is refused on use with
401 user_blocked, and with 401 unauthorized on the call grant endpoints (answer, decline, hang up, reconnect, ring).
Revoke a user token
/admin/v1/tokens/revokeRevokes one JWT before it expires. Every later request with it gets
401 token_revoked. The user's other tokens keep working.
Revoking also closes all of the user's open realtime sockets, because a
socket does not record which token opened it. Clients that hold another
valid token reconnect with it. To cut off a
user completely, set is_blocked: true with POST /v1/users instead.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
token | string | yes | The JWT to revoke. It must belong to your tenant and must not have expired. |
Response
200:
{
"jti": "2tq8f0r9b6m1k3p5s7u9w1y3",
"expires_at": "2026-06-02T09:14:00Z"
}
| Field | Type | Description |
|---|---|---|
jti | string | The token's id, now on the revocation list. |
expires_at | string | The token's own expiry. The revocation lasts until then. |
curl -X POST https://api.poolse.dev/admin/v1/tokens/revoke \
-H "Authorization: Bearer $POOLSE_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"token\":\"$USER_JWT\"}"
Errors
| Code | Status | When |
|---|---|---|
missing_token | 400 | No token in the body. |
invalid_token | 400 | The token does not verify, has already expired, or lacks a jti. |
forbidden | 403 | The API key lacks tokens:admin. |
cross_tenant | 403 | The token belongs to another tenant. |