Users and Events
Manage your end users' lifecycle—create, list, read, update, activate, deactivate, and delete them—and read the webhook events sent to your account.
Manage the full lifecycle of your end users: create them, read and update their profile, activate or deactivate access, and delete them. A user must exist and be VERIFIED before you issue a token, open wallets, or transact on their behalf. The last section lists the webhook events your account produced.
Authentication: every endpoint on this page requires your Owner API Key. A user token gets 403 FORBIDDEN.
x-api-key: YOUR_OWNER_API_KEYEvery response uses the standard envelope and error format. POST and PATCH calls accept an x-idempotency-key header, so retry them safely.
The user object
| Field | Type | Description |
|---|---|---|
| id | string (uuid) | User id. Use it in every /users/{user_id} path. |
| user_id | string (uuid) | Same value as id. Deprecated alias—read id. |
| object | string | Always user. |
| external_id | string | null | Your own reference for the user, unique within your customer account. |
| customer_id | string | Your customer account id. |
| account_type | string | INDIVIDUAL or CORPORATE. |
| account_role | string | THIRD. |
| account_purpose | string | TRADING or INVESTING. |
| first_name | string | First name. |
| last_name | string | Last name. |
| string | Email, unique within your customer account. | |
| phone_country_code | string | ISO 3166-1 alpha-2 country of the phone number. |
| phone_number | string | Phone number, unique within your customer account. |
| status | string | PENDING, VERIFIED, or SUSPENDED. See User statuses. |
| is_active | boolean | false once the user is deactivated. |
| individual | object | null | KYC data. See The individual object. |
| created_at | string | Creation time, UTC. |
| updated_at | string | Last change, UTC. |
| created_by | string | null | Id of the credential that created the user. |
| updated_by | string | null | Who last changed the user. |
The individual object
| Field | Type | Description |
|---|---|---|
| birth_date | string | Date of birth, YYYY-MM-DD. The user must be 18 or older. |
| residential_country_code | string | Country of residence (ISO 3166-1 alpha-2). |
| residential_address | string | Street address of residence. |
| residential_city | string | City of residence. |
| residential_state | string | State or province of residence. |
| residential_postal_code | string | Postal code of residence. |
| id_type | string | SSN or PASSPORT. |
| id_number_masked | string | Identification number with all but the last four characters masked (*****6789). The full number is write-only. |
| id_country_code | string | Country that issued the ID (ISO 3166-1 alpha-2). |
User statuses
PENDING—every new user starts here. A pending user can't receive a token.VERIFIED—set by Activate User. Only verified, active users receive tokens.SUSPENDED—set by Deactivate User. The user's token stops working until you activate them again.
You can't write status, account_type, or account_role directly; use the activate and deactivate actions.
Create User
Register a new end user under your customer account. The user starts as PENDING. Email, phone number, and external_id must each be unique within your customer account. The response is 201 Created, and the location header points at the new user (/v1/users/{id}).
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| user_id | string (uuid v4) | No | Your own UUID for the user. Generated when omitted. Must be unique. |
| external_id | string | No | Your own reference for the user (max 255), unique within your customer account. |
| account_type | string | Yes | INDIVIDUAL or CORPORATE. |
| account_role | string | Yes | THIRD. |
| account_purpose | string | Yes | TRADING or INVESTING. |
| first_name | string | Yes | First name (max 100). |
| last_name | string | Yes | Last name (max 100). |
| string | Yes | Valid email address. | |
| phone_country_code | string | Yes | ISO 3166-1 alpha-2 country of the phone number (e.g. US). |
| phone_number | string | Yes | Phone number valid for phone_country_code (max 20). |
| individual | object | Yes | KYC data, all fields below required. |
| individual.birth_date | string | Yes | Date of birth, YYYY-MM-DD. The user must be 18 or older. |
| individual.residential_country_code | string | Yes | Country of residence (ISO 3166-1 alpha-2). |
| individual.residential_address | string | Yes | Street address. |
| individual.residential_city | string | Yes | City. |
| individual.residential_state | string | Yes | State or province. |
| individual.residential_postal_code | string | Yes | Postal code. |
| individual.id_type | string | Yes | SSN or PASSPORT. |
| individual.id_number | string | Yes | Identification number. It's returned masked. |
| individual.id_country_code | string | Yes | Country that issued the ID (ISO 3166-1 alpha-2). |
Deprecated input, removed in the next release
Lowercase enum values (individual, third, investing, ssn) and individual.dob (use individual.birth_date) still work during this release.
{
"external_id": "crm-000123",
"account_type": "INDIVIDUAL",
"account_role": "THIRD",
"account_purpose": "INVESTING",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"phone_country_code": "US",
"phone_number": "2252542523",
"individual": {
"birth_date": "1990-10-15",
"residential_country_code": "US",
"residential_address": "123 Sovera Lane",
"residential_city": "Cheyenne",
"residential_state": "WY",
"residential_postal_code": "82001",
"id_type": "SSN",
"id_number": "123456789",
"id_country_code": "US"
}
}Success Response (201 Created):
{
"success": true,
"data": {
"id": "f4057807-52cf-4083-9ecb-283ef354fb2b",
"user_id": "f4057807-52cf-4083-9ecb-283ef354fb2b",
"object": "user",
"external_id": "crm-000123",
"customer_id": "corp-redundant-firewall",
"account_type": "INDIVIDUAL",
"account_role": "THIRD",
"account_purpose": "INVESTING",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"phone_country_code": "US",
"phone_number": "2252542523",
"status": "PENDING",
"is_active": true,
"individual": {
"birth_date": "1990-10-15",
"residential_country_code": "US",
"residential_address": "123 Sovera Lane",
"residential_city": "Cheyenne",
"residential_state": "WY",
"residential_postal_code": "82001",
"id_type": "SSN",
"id_number_masked": "*****6789",
"id_country_code": "US"
},
"created_at": "2026-10-12T14:30:00Z",
"updated_at": "2026-10-12T14:30:00Z",
"created_by": "65f1c2a9e4b0a1d2c3f4a5b6",
"updated_by": "65f1c2a9e4b0a1d2c3f4a5b6"
},
"meta": {
"timestamp": "2026-10-12T14:30:00Z",
"version": "v1",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}
}Error Response (409 Conflict):
{
"success": false,
"errors": [
{
"code": "EMAIL_ALREADY_IN_USE",
"message": "User with this email already exists for this customer",
"source": { "pointer": "/email" }
}
],
"meta": {
"timestamp": "2026-10-12T14:30:00Z",
"version": "v1",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}
}curl -X POST "https://api.sandbox.sovera.io/sovx/v1/users" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_OWNER_API_KEY" \
-H "x-idempotency-key: 9a7c3e1f-5b2d-4c8e-a6f0-1d3b5e7f9a2c" \
-d '{
"external_id": "crm-000123",
"account_type": "INDIVIDUAL",
"account_role": "THIRD",
"account_purpose": "INVESTING",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"phone_country_code": "US",
"phone_number": "2252542523",
"individual": {
"birth_date": "1990-10-15",
"residential_country_code": "US",
"residential_address": "123 Sovera Lane",
"residential_city": "Cheyenne",
"residential_state": "WY",
"residential_postal_code": "82001",
"id_type": "SSN",
"id_number": "123456789",
"id_country_code": "US"
}
}'Errors: 401 UNAUTHORIZED missing or invalid key · 403 FORBIDDEN not an owner key · 409 EMAIL_ALREADY_IN_USE / PHONE_ALREADY_IN_USE / EXTERNAL_ID_ALREADY_IN_USE / USER_ID_ALREADY_IN_USE (with source.pointer on the field) · 422 VALIDATION_ERROR one entry per invalid field · 429 RATE_LIMIT_EXCEEDED.
List Users
List your customer account's users, newest first. Deleted users never appear. The list uses cursor pagination and the filter grammar; an empty result is 200 with data: [].
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| filter[limit] | integer | No | Page size, 1–100 (default 25). |
| cursor | string | No | Opaque cursor from meta.pagination.navigation.next or previous. Echo it as is. |
| filter[order] | string | No | created_at or updated_at, then ASC or DESC (default created_at DESC). |
| filter[where][status][eq|inq] | string | No | PENDING, VERIFIED, SUSPENDED. |
| filter[where][account_type][eq|inq] | string | No | INDIVIDUAL, CORPORATE. |
| filter[where][email][eq] | string | No | Exact email (case-insensitive). |
| filter[where][external_id][eq] | string | No | Exact external_id. |
| filter[where][created_at][gte|lte|between] | string | No | UTC date-time ending in Z. |
Up to six filter conditions per request. Any other field, operator, filter[skip], offset, or page returns 422 VALIDATION_ERROR naming the parameter in source.parameter.
GET https://api.sandbox.sovera.io/sovx/v1/users?filter[limit]=25&filter[where][status][eq]=VERIFIEDSuccess Response (200 OK):
{
"success": true,
"data": [
{
"id": "f4057807-52cf-4083-9ecb-283ef354fb2b",
"user_id": "f4057807-52cf-4083-9ecb-283ef354fb2b",
"object": "user",
"external_id": "crm-000123",
"customer_id": "corp-redundant-firewall",
"account_type": "INDIVIDUAL",
"account_role": "THIRD",
"account_purpose": "INVESTING",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"phone_country_code": "US",
"phone_number": "2252542523",
"status": "VERIFIED",
"is_active": true,
"individual": {
"birth_date": "1990-10-15",
"residential_country_code": "US",
"residential_address": "123 Sovera Lane",
"residential_city": "Cheyenne",
"residential_state": "WY",
"residential_postal_code": "82001",
"id_type": "SSN",
"id_number_masked": "*****6789",
"id_country_code": "US"
},
"created_at": "2026-10-12T14:30:00Z",
"updated_at": "2026-10-12T14:32:10Z",
"created_by": "65f1c2a9e4b0a1d2c3f4a5b6",
"updated_by": "65f1c2a9e4b0a1d2c3f4a5b6"
}
],
"meta": {
"timestamp": "2026-10-12T14:35:00Z",
"version": "v1",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"pagination": {
"records": { "limit": 25, "has_next": true, "has_previous": false },
"navigation": {
"next": "/v1/users?filter[limit]=25&filter[where][status][eq]=VERIFIED&cursor=3-gHUuzU-jSdf1bDLZKrZ0pN",
"previous": null
}
}
}
}curl -g -X GET "https://api.sandbox.sovera.io/sovx/v1/users?filter[limit]=25&filter[where][status][eq]=VERIFIED" \
-H "x-api-key: YOUR_OWNER_API_KEY"Errors: 401 UNAUTHORIZED · 403 FORBIDDEN not an owner key · 422 VALIDATION_ERROR invalid filter, order, limit, or cursor · 429 RATE_LIMIT_EXCEEDED.
Get User
Return one user by id. A user from another customer account, or a deleted one, returns 404.
GET https://api.sandbox.sovera.io/sovx/v1/users/{{userID}}Success Response (200 OK)—data is the user object, as in Create User.
Error Response (404 Not Found):
{
"success": false,
"errors": [
{
"code": "NOT_FOUND",
"message": "User not found",
"source": { "parameter": "user_id" }
}
],
"meta": {
"timestamp": "2026-10-12T14:35:00Z",
"version": "v1",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}
}curl -X GET "https://api.sandbox.sovera.io/sovx/v1/users/{{userID}}" \
-H "x-api-key: YOUR_OWNER_API_KEY"Errors: 401 UNAUTHORIZED · 403 FORBIDDEN · 404 NOT_FOUND · 429 RATE_LIMIT_EXCEEDED.
Update User
Update a user's profile with merge semantics: only the fields you send change, and everything else keeps its value. KYC fields go under individual, and you can send just the ones that change. A new phone number is checked against the effective country (the one you send, or the stored one).
Request Body
All fields are optional.
| Field | Type | Description |
|---|---|---|
| first_name | string | First name (max 100). |
| last_name | string | Last name (max 100). |
| string | Email, unique within your customer account. | |
| phone_country_code | string | ISO 3166-1 alpha-2. |
| phone_number | string | Phone number (max 20), unique within your customer account. |
| account_purpose | string | TRADING or INVESTING. |
| individual | object | Any subset of birth_date, residential_country_code, residential_address, residential_city, residential_state, residential_postal_code, id_type, id_number, id_country_code. |
Sending status, account_type, or account_role returns 422 VALIDATION_ERROR.
Deprecated, removed in the next release
PUT /v1/users/{user_id} still works as an alias of PATCH with the same merge behavior. The flat KYC names at the top level (dob, birth_date, residential_*, id_type, id_number, id_country_code) still map into individual; a nested value wins over its flat alias. Move to PATCH and the nested individual object.
{
"phone_number": "3075550142",
"individual": {
"residential_city": "Laramie",
"residential_postal_code": "82070"
}
}Success Response (200 OK)—data is the updated user object:
{
"success": true,
"data": {
"id": "f4057807-52cf-4083-9ecb-283ef354fb2b",
"user_id": "f4057807-52cf-4083-9ecb-283ef354fb2b",
"object": "user",
"external_id": "crm-000123",
"customer_id": "corp-redundant-firewall",
"account_type": "INDIVIDUAL",
"account_role": "THIRD",
"account_purpose": "INVESTING",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"phone_country_code": "US",
"phone_number": "3075550142",
"status": "VERIFIED",
"is_active": true,
"individual": {
"birth_date": "1990-10-15",
"residential_country_code": "US",
"residential_address": "123 Sovera Lane",
"residential_city": "Laramie",
"residential_state": "WY",
"residential_postal_code": "82070",
"id_type": "SSN",
"id_number_masked": "*****6789",
"id_country_code": "US"
},
"created_at": "2026-10-12T14:30:00Z",
"updated_at": "2026-10-12T15:02:44Z",
"created_by": "65f1c2a9e4b0a1d2c3f4a5b6",
"updated_by": "65f1c2a9e4b0a1d2c3f4a5b6"
},
"meta": {
"timestamp": "2026-10-12T15:02:44Z",
"version": "v1",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}
}curl -X PATCH "https://api.sandbox.sovera.io/sovx/v1/users/{{userID}}" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_OWNER_API_KEY" \
-d '{
"phone_number": "3075550142",
"individual": {
"residential_city": "Laramie",
"residential_postal_code": "82070"
}
}'Errors: 401 UNAUTHORIZED · 403 FORBIDDEN · 404 NOT_FOUND · 409 EMAIL_ALREADY_IN_USE / PHONE_ALREADY_IN_USE · 422 VALIDATION_ERROR (invalid field, phone not valid for the country, or a read-only field) · 429 RATE_LIMIT_EXCEEDED.
Delete User
Soft-delete a user. The call revokes the user's access and refresh tokens at once, and later reads of the user return 404.
DELETE https://api.sandbox.sovera.io/sovx/v1/users/{{userID}}Success Response (204 No Content)—empty body.
curl -X DELETE "https://api.sandbox.sovera.io/sovx/v1/users/{{userID}}" \
-H "x-api-key: YOUR_OWNER_API_KEY"Errors: 401 UNAUTHORIZED · 403 FORBIDDEN · 404 NOT_FOUND · 429 RATE_LIMIT_EXCEEDED.
Activate User
Move a user to VERIFIED and set is_active to true. Activate a new user before you issue their first token, and use this call to restore a suspended user. Send no body.
POST https://api.sandbox.sovera.io/sovx/v1/users/{{userID}}/activateSuccess Response (200 OK)—data is the user object with "status": "VERIFIED" and "is_active": true.
curl -X POST "https://api.sandbox.sovera.io/sovx/v1/users/{{userID}}/activate" \
-H "x-api-key: YOUR_OWNER_API_KEY"Errors: 401 UNAUTHORIZED · 403 FORBIDDEN · 404 NOT_FOUND · 429 RATE_LIMIT_EXCEEDED.
Deactivate User
Move a user to SUSPENDED and set is_active to false. The user's token stops working until you activate them again. Send no body.
POST https://api.sandbox.sovera.io/sovx/v1/users/{{userID}}/deactivateSuccess Response (200 OK)—data is the user object with "status": "SUSPENDED" and "is_active": false.
curl -X POST "https://api.sandbox.sovera.io/sovx/v1/users/{{userID}}/deactivate" \
-H "x-api-key: YOUR_OWNER_API_KEY"Errors: 401 UNAUTHORIZED · 403 FORBIDDEN · 404 NOT_FOUND · 429 RATE_LIMIT_EXCEEDED.
Events
Every webhook event Sovera produces for your customer account is stored, including events produced while no webhook endpoint was configured (those stay PENDING with status_reason: null and aren't retried). Read them here to reconcile missed deliveries or audit what was sent. The data of each event type is described in the event catalogue.
List Events
List your customer account's webhook events, newest first, with cursor pagination.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| filter[limit] | integer | No | Page size, 1–100 (default 25). |
| cursor | string | No | Opaque cursor from meta.pagination.navigation. |
| filter[order] | string | No | created_at ASC or created_at DESC (default). |
| filter[where][type][eq|inq] | string | No | Event type, e.g. user.deposit.received. |
| filter[where][delivery_status][eq|inq] | string | No | PENDING, DELIVERED, SUSPENDED. |
| filter[where][resource_type][eq] | string | No | Resource type, e.g. DEPOSIT. |
| filter[where][resource_id][eq] | string | No | Id of the resource the event is about. |
| filter[where][created_at][gte|lte|between] | string | No | UTC date-time ending in Z. |
The event object
| Field | Type | Description |
|---|---|---|
| id | string (uuid) | Event id. |
| event_id | string (uuid) | Same value as id. |
| object | string | Always event. |
| type | string | Dotted event type, e.g. user.deposit.received. |
| resource_type | string | null | Type of the resource the event is about, UPPER_SNAKE. |
| resource_id | string | null | Id of that resource. |
| sequence_number | integer | null | Position of the event among the events about the same resource (resource_type + resource_id), starting at 1. |
| data | object | null | The event payload, as delivered to your webhook, except that verification codes (data.code) are never returned. |
| delivery_status | string | PENDING (also while retries are pending), DELIVERED, or SUSPENDED. |
| status_reason | object | null | {code, message} when SUSPENDED: HTTP_<status> (your endpoint answered that status), TIMEOUT, or DELIVERY_FAILED. |
| attempt_count | integer | Delivery attempts so far. |
| occurred_at | string | null | When the underlying change happened, UTC. |
| delivered_at | string | null | When your endpoint accepted the event, UTC. |
| created_at | string | null | When the event was recorded, UTC. |
GET https://api.sandbox.sovera.io/sovx/v1/events?filter[where][delivery_status][eq]=SUSPENDEDSuccess Response (200 OK):
{
"success": true,
"data": [
{
"id": "3c9e1f7a-2b4d-4e6f-8a1c-5d7e9f0b2a4c",
"event_id": "3c9e1f7a-2b4d-4e6f-8a1c-5d7e9f0b2a4c",
"object": "event",
"type": "user.deposit.received",
"resource_type": "DEPOSIT",
"resource_id": "96abb315-99d8-59e7-a333-1007dba37ad5",
"sequence_number": 3,
"data": {
"wallet_id": "fd956820-0446-5e40-a0fe-8fff794b7fd6",
"deposit_id": "96abb315-99d8-59e7-a333-1007dba37ad5",
"amount": "0.05000000",
"currency": "btc",
"source_address": "bc1q9h7garjqzd4ps7pkkx6hf0rsk2xw6d6yvgxvq5",
"tx_hash": "a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90"
},
"delivery_status": "SUSPENDED",
"status_reason": {
"code": "HTTP_500",
"message": "The webhook endpoint answered HTTP 500"
},
"attempt_count": 3,
"occurred_at": "2026-10-12T14:28:03Z",
"delivered_at": null,
"created_at": "2026-10-12T14:28:03Z"
}
],
"meta": {
"timestamp": "2026-10-12T14:30:00Z",
"version": "v1",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"pagination": {
"records": { "limit": 25, "has_next": false, "has_previous": false },
"navigation": { "next": null, "previous": null }
}
}
}curl -g -X GET "https://api.sandbox.sovera.io/sovx/v1/events?filter[where][delivery_status][eq]=SUSPENDED" \
-H "x-api-key: YOUR_OWNER_API_KEY"Errors: 401 UNAUTHORIZED · 403 FORBIDDEN not an owner key · 422 VALIDATION_ERROR invalid filter, order, limit, or cursor · 429 RATE_LIMIT_EXCEEDED.
Next Steps
- Issue a user token so the user can act on their own behalf.
- Walk the full user onboarding flow.