User Tokens
Issue the 24-hour access token an end user presents on the SX Users APIs, renew it with its 30-day refresh token, or issue tokens in bulk.
Issue tokens scoped to a single end user. The user's token reaches only that user's data on the SX Users APIs—wallets, addresses, transfers, withdrawals, trading, balances, and transactions—without ever exposing your Owner API Key.
Each issuance returns a pair:
- Access token (
token)—starts withutk_test_in sandbox (utk_live_in production) and lasts 24 hours (expires_at). Send it asAuthorization: Bearer <token>. - Refresh token (
refresh_token)—starts withrt_, lasts 30 days (refresh_expires_at), and works once. Trade it for a new pair before the access token expires, without your owner key.
Both values appear only in the response that issues them; Sovera stores just a hash. Issuing or refreshing retires the user's previous pair, so always store the newest one. An expired access token returns 401 UNAUTHORIZED.
Only VERIFIED, active users receive tokens. Activate the user first; a PENDING or SUSPENDED user returns 422 INVALID_STATE_TRANSITION.
Issue User Token
Issue a new token pair for one user with your Owner API Key and an empty body. A user token or a non-owner key gets 403 FORBIDDEN.
POST https://api.sandbox.sovera.io/sovx/v1/users/{{userID}}/tokensNo request body.
Success Response (201 Created):
{
"success": true,
"data": {
"id": "bf1f8aa6-8b55-5650-8ee7-c7a9d29d0b5e",
"object": "user_token",
"user_id": "f4057807-52cf-4083-9ecb-283ef354fb2b",
"token": "utk_test_7Qm4Lr8Tz1V2x9Kf3Bn6Wd1Yb6Jk2Hc5Np8Rf2Lm6Tq",
"token_last4": "9Vx3",
"expires_at": "2026-10-13T14:30:00Z",
"created_at": "2026-10-12T14:30:00Z",
"created_by": "65f1c2a9e4b0a1d2c3f4a5b6",
"refresh_token": "rt_9f2c4e7a1b3d5f80c6e2a4b8d1f3e5a79f2c4e7a1b3d5f80c6e2a4b8d1f3e5a7",
"refresh_expires_at": "2026-11-11T14:30:00Z"
},
"meta": {
"timestamp": "2026-10-12T14:30:00Z",
"version": "v1",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}
}curl -X POST "https://api.sandbox.sovera.io/sovx/v1/users/{{userID}}/tokens" \
-H "x-api-key: YOUR_OWNER_API_KEY"The user token object
| Field | Type | Description |
|---|---|---|
| id | string (uuid) | Token id. |
| object | string | Always user_token. |
| user_id | string (uuid) | The user the token acts for. |
| token | string | Access token, valid 24 hours. Shown only here. |
| token_last4 | string | Last four characters of the access token, for display and support. |
| expires_at | string | Access token expiry, UTC. |
| created_at | string | Issuance time, UTC. |
| created_by | string | null | Who issued the pair: your credential id, or the user id on a refresh. |
| refresh_token | string | Refresh token, valid 30 days and single use. Shown only here. |
| refresh_expires_at | string | Refresh token expiry, UTC. |
The end user then calls the SX Users APIs with the access token:
curl -X GET "https://api.sandbox.sovera.io/sovx/v1/users/{{userID}}/wallets" \
-H "Authorization: Bearer {{userToken}}"Errors: 401 UNAUTHORIZED missing or invalid key · 403 FORBIDDEN not an owner key · 404 NOT_FOUND user not found (source.parameter user_id) · 422 INVALID_STATE_TRANSITION the user isn't VERIFIED and active · 429 RATE_LIMIT_EXCEEDED.
Deprecated, removed in the next release
POST /v1/users/{user_id}/token (singular) still works as an alias with the same behavior. Move to /tokens.
Refresh User Token
Trade a valid refresh token for a new pair. Send the refresh token in the body and no owner key—the refresh token authenticates the call. The old access and refresh tokens stop working at once, and a refresh token can't be used twice. Refresh before the access token's expires_at to keep the user signed in; once the refresh token expires, issue a new pair with your owner key.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| grant_type | string | Yes | refresh_token. |
| refresh_token | string | Yes | The latest refresh token issued for this user. |
{
"grant_type": "refresh_token",
"refresh_token": "rt_9f2c4e7a1b3d5f80c6e2a4b8d1f3e5a79f2c4e7a1b3d5f80c6e2a4b8d1f3e5a7"
}Success Response (201 Created)—data is a new user_token object with a new token and refresh_token:
{
"success": true,
"data": {
"id": "0d6f2c8e-4b1a-4f3e-9c7d-2a5b8e1f4c6d",
"object": "user_token",
"user_id": "f4057807-52cf-4083-9ecb-283ef354fb2b",
"token": "utk_test_Hc5Np8Rf2Lm6Tq9Vx37Qm4Lr8Tz1V2x9Kf3Bn6Wd1Yb",
"token_last4": "6Jk2",
"expires_at": "2026-10-14T09:15:00Z",
"created_at": "2026-10-13T09:15:00Z",
"created_by": "f4057807-52cf-4083-9ecb-283ef354fb2b",
"refresh_token": "rt_4e7a1b3d5f80c6e2a4b8d1f3e5a79f2c4e7a1b3d5f80c6e2a4b8d1f3e5a79f2c",
"refresh_expires_at": "2026-11-12T09:15:00Z"
},
"meta": {
"timestamp": "2026-10-13T09:15:00Z",
"version": "v1",
"trace_id": "9c1d7e3a5b2f4e8d0a6c3b9f1e7d5a2c"
}
}Error Response (401 Unauthorized):
{
"success": false,
"errors": [
{
"code": "INVALID_REFRESH_TOKEN",
"message": "Refresh token is invalid or expired",
"source": { "pointer": "/refresh_token" }
}
],
"meta": {
"timestamp": "2026-10-13T09:15:00Z",
"version": "v1",
"trace_id": "9c1d7e3a5b2f4e8d0a6c3b9f1e7d5a2c"
}
}curl -X POST "https://api.sandbox.sovera.io/sovx/v1/users/{{userID}}/tokens" \
-H "Content-Type: application/json" \
-d '{
"grant_type": "refresh_token",
"refresh_token": "rt_9f2c4e7a1b3d5f80c6e2a4b8d1f3e5a79f2c4e7a1b3d5f80c6e2a4b8d1f3e5a7"
}'Errors: 401 INVALID_REFRESH_TOKEN unknown, expired, already used, or revoked refresh token (the same code whatever the cause) · 422 INVALID_STATE_TRANSITION the user was suspended · 422 VALIDATION_ERROR missing refresh_token · 429 RATE_LIMIT_EXCEEDED too many refreshes for your customer, or too many failed refreshes for this user (wait retry-after).
Issue User Tokens (bulk)
Issue token pairs for up to 100 users in one call with your Owner API Key. Each item succeeds or fails on its own: the response is 207 Multi-Status, with one result per item in request order. Each successful data is the same user_token object as a single issuance, and it retires that user's previous pair.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| items | array | Yes | 1 to 100 entries. |
| items[].user_id | string (uuid) | Yes | User to issue a pair for. |
Per-item result
| Field | Type | Description |
|---|---|---|
| index | integer | Position of the item in items. |
| success | boolean | Whether this item got a token pair. |
| data | object | The user_token object, when success is true. |
| errors | array | Error list, when success is false. source.pointer names the item (/items/1/user_id). |
Item error codes: NOT_FOUND (no such user in your account), INVALID_STATE_TRANSITION (the user isn't VERIFIED and active), DUPLICATE_ITEM (the same user_id appears earlier in the batch).
Deprecated, removed in the next release
user_ids (a plain array of ids) still works instead of items; its item errors point at /user_ids/{index}. Move to items.
{
"items": [
{ "user_id": "f4057807-52cf-4083-9ecb-283ef354fb2b" },
{ "user_id": "b587f614-4277-5fdc-acd4-d80033459bba" }
]
}Response (207 Multi-Status):
{
"success": true,
"data": [
{
"index": 0,
"success": true,
"data": {
"id": "bf1f8aa6-8b55-5650-8ee7-c7a9d29d0b5e",
"object": "user_token",
"user_id": "f4057807-52cf-4083-9ecb-283ef354fb2b",
"token": "utk_test_7Qm4Lr8Tz1V2x9Kf3Bn6Wd1Yb6Jk2Hc5Np8Rf2Lm6Tq",
"token_last4": "9Vx3",
"expires_at": "2026-10-13T14:30:00Z",
"created_at": "2026-10-12T14:30:00Z",
"created_by": "65f1c2a9e4b0a1d2c3f4a5b6",
"refresh_token": "rt_9f2c4e7a1b3d5f80c6e2a4b8d1f3e5a79f2c4e7a1b3d5f80c6e2a4b8d1f3e5a7",
"refresh_expires_at": "2026-11-11T14:30:00Z"
}
},
{
"index": 1,
"success": false,
"errors": [
{
"code": "NOT_FOUND",
"message": "User not found",
"source": { "pointer": "/items/1/user_id" }
}
]
}
],
"meta": {
"timestamp": "2026-10-12T14:30:00Z",
"version": "v1",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}
}curl -X POST "https://api.sandbox.sovera.io/sovx/v1/users/tokens" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_OWNER_API_KEY" \
-d '{
"items": [
{ "user_id": "f4057807-52cf-4083-9ecb-283ef354fb2b" },
{ "user_id": "b587f614-4277-5fdc-acd4-d80033459bba" }
]
}'Errors: 401 UNAUTHORIZED · 403 FORBIDDEN not an owner key · 422 VALIDATION_ERROR items missing, empty, or over 100 entries · 429 RATE_LIMIT_EXCEEDED. Per-user failures come back inside the 207 response, never as a top-level error.
Next Steps
- Create a wallet for the user with their token.
- See every credential tier in Authentication.