SX Connect

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 with utk_test_ in sandbox (utk_live_ in production) and lasts 24 hours (expires_at). Send it as Authorization: Bearer <token>.
  • Refresh token (refresh_token)—starts with rt_, 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

POSThttps://api.sandbox.sovera.io/sovx/v1/users/:user_id/tokens

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}}/tokens

No 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

FieldTypeDescription
idstring (uuid)Token id.
objectstringAlways user_token.
user_idstring (uuid)The user the token acts for.
tokenstringAccess token, valid 24 hours. Shown only here.
token_last4stringLast four characters of the access token, for display and support.
expires_atstringAccess token expiry, UTC.
created_atstringIssuance time, UTC.
created_bystring | nullWho issued the pair: your credential id, or the user id on a refresh.
refresh_tokenstringRefresh token, valid 30 days and single use. Shown only here.
refresh_expires_atstringRefresh 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

POSThttps://api.sandbox.sovera.io/sovx/v1/users/:user_id/tokens

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

FieldTypeRequiredDescription
grant_typestringYesrefresh_token.
refresh_tokenstringYesThe 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)

POSThttps://api.sandbox.sovera.io/sovx/v1/users/tokens

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

FieldTypeRequiredDescription
itemsarrayYes1 to 100 entries.
items[].user_idstring (uuid)YesUser to issue a pair for.

Per-item result

FieldTypeDescription
indexintegerPosition of the item in items.
successbooleanWhether this item got a token pair.
dataobjectThe user_token object, when success is true.
errorsarrayError 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

On this page