Error Codes Reference
Every error code the Crypto as a Service API returns, the HTTP status behind it, and how to debug a failed call.
Errors are predictable. Every failure—a missing field, a revoked key, an upstream outage—comes back in one envelope with the same shape. Branch on the HTTP status for the category and on code for the exact reason; treat message as a hint to log or show, never something to parse.
Error Response Format
Every error returns success: false, an errors array, and meta. Each error carries a stable code, a message, and, when it points at your input, a source with a pointer (body field), parameter (path or query parameter), or header. See API conventions for the full rules.
{
"success": false,
"errors": [
{
"code": "VALIDATION_ERROR",
"message": "address should not be empty",
"source": { "pointer": "/address" }
},
{
"code": "VALIDATION_ERROR",
"message": "amount must be a positive decimal string, e.g. \"0.01000000\"",
"source": { "pointer": "/amount" }
}
],
"meta": {
"timestamp": "2026-10-12T14:03:11Z",
"version": "v1",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}
}HTTP Status Codes
| Status | Meaning | When it occurs |
|---|---|---|
| 200 | OK | Request succeeded |
| 201 | Created | Resource created |
| 204 | No Content | Succeeded with no body (deprecated DELETE aliases) |
| 207 | Multi-Status | Bulk request with per-item results (bulk user tokens) |
| 400 | Bad Request | A business rule rejected the request |
| 401 | Unauthorized | Missing, invalid, revoked, or expired credential |
| 403 | Forbidden | Valid credential that can't perform this action |
| 404 | Not Found | Resource doesn't exist or doesn't belong to you |
| 409 | Conflict | Duplicate resource, or the same idempotency key still running |
| 413 | Payload Too Large | Request body too large |
| 415 | Unsupported Media Type | POST or PUT without Content-Type: application/json |
| 422 | Unprocessable Entity | Validation failed, illegal state change, or idempotency key reused |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unexpected server error |
| 502 | Bad Gateway | An upstream service returned an error |
| 503 | Service Unavailable | Service temporarily unavailable |
| 504 | Gateway Timeout | An upstream service didn't answer in time |
Generic Codes
Any endpoint can return these. When an error has no more specific code, the code follows the HTTP status.
| Code | HTTP Status | Cause |
|---|---|---|
BAD_REQUEST | 400 | A business rule rejected the request; read message |
UNAUTHORIZED | 401 | Missing, invalid, revoked, or expired x-api-key or Bearer token |
FORBIDDEN | 403 | Valid credential without access to this action |
NOT_FOUND | 404 | Resource not found or not yours |
CONFLICT | 409 | The resource already exists |
VALIDATION_ERROR | 422 | A field, parameter, or header failed validation; source points at it |
INVALID_STATE_TRANSITION | 422 | The action isn't allowed in the resource's current status |
RATE_LIMIT_EXCEEDED | 429 | Too many requests; wait retry-after seconds |
PAYLOAD_TOO_LARGE | 413 | Request body exceeds the size limit |
UNSUPPORTED_MEDIA_TYPE | 415 | Set Content-Type: application/json |
INTERNAL_SERVER_ERROR | 500 | Unexpected error—include trace_id when reporting |
BAD_GATEWAY | 502 | An upstream service returned an error |
SERVICE_UNAVAILABLE | 503 | Service temporarily unavailable |
GATEWAY_TIMEOUT | 504 | An upstream service didn't answer in time |
METHOD_NOT_ALLOWED | 405 | The route doesn't accept this HTTP method |
NOT_ACCEPTABLE | 406 | The requested response format isn't available |
REQUEST_TIMEOUT | 408 | The request took too long to arrive |
NOT_IMPLEMENTED | 501 | The operation isn't available |
UNKNOWN_ERROR | other 4xx | Any other client error; read message |
Transitional codes
For one release, some 404 and 409 errors without a specific code still derive it from the message: USER_NOT_FOUND, DEPOSIT_NOT_FOUND, CLIENT_NOT_FOUND, EMAIL_ALREADY_EXISTS, and PHONE_ALREADY_EXISTS. Don't build new logic on them; they'll be replaced by the specific codes below.
Idempotency Errors
| Code | HTTP Status | Cause |
|---|---|---|
VALIDATION_ERROR | 422 | x-idempotency-key isn't a UUID (source.header) |
IDEMPOTENCY_IN_PROGRESS | 409 | A request with this key is still running; retry shortly |
IDEMPOTENCY_KEY_REUSED | 422 | The key was already used with a different body; send a new key |
See Idempotency.
Authentication Errors
| Code | HTTP Status | Cause |
|---|---|---|
UNAUTHORIZED | 401 | Missing, invalid, revoked, or expired credential, including a user token past its 24-hour lifetime; on x-api-key and user-token routes source.header names the header (SX Customers session errors carry no source) |
INVALID_CREDENTIALS | 401 | Login rejected: wrong email or password |
INVALID_REFRESH_TOKEN | 401 | The refresh token is invalid, expired, or already used |
FORBIDDEN | 403 | The credential can't perform this action |
A 401 means fix the header or get a fresh token. A 403 means the credential is valid but lacks reach—use the credential the endpoint expects. See Authentication.
User Errors
| Code | HTTP Status | Cause |
|---|---|---|
NOT_FOUND | 404 | No user with that user_id under your customer (source.parameter: user_id) |
USER_NOT_FOUND | 404 | The user referenced by a wallet or account call doesn't exist |
EMAIL_ALREADY_IN_USE | 409 | Another user under your customer has this email (source.pointer: /email) |
PHONE_ALREADY_IN_USE | 409 | Another user under your customer has this phone number |
EXTERNAL_ID_ALREADY_IN_USE | 409 | Another user under your customer has this external_id |
USER_ID_ALREADY_IN_USE | 409 | A user with this user_id already exists |
INVALID_STATE_TRANSITION | 422 | The user can't get a token in its current status (only verified, active users can) |
DUPLICATE_ITEM | 207 (per item) | Bulk tokens: the same user_id appears earlier in the batch |
Wallet and Address Errors
| Code | HTTP Status | Cause |
|---|---|---|
WALLET_NOT_FOUND | 404 | No such wallet under your customer, or (with a user token) under that user |
ADDRESS_NOT_FOUND | 404 | The wallet has no address for that currency |
WALLET_NAME_ALREADY_EXISTS | 400 | The user already has a wallet with this name |
Transfer Errors
| Code | HTTP Status | Cause |
|---|---|---|
TRANSFER_NOT_FOUND | 404 | No such transfer on this wallet |
SAME_WALLET_TRANSFER | 400 | Source and destination wallet are the same |
CROSS_CUSTOMER_TRANSFER | 400 | Both wallets must belong to the same customer |
INSUFFICIENT_BALANCE | 400 | The source wallet's available balance is too low |
INVALID_VERIFICATION_CODE | 400 | Wrong verification code |
VERIFICATION_CODE_EXPIRED | 400 | The verification code expired |
VERIFICATION_BLOCKED | 403 | Too many wrong codes; the transfer is BLOCKED |
ACCOUNT_FROZEN | 403 | The account is frozen |
INVALID_STATE_TRANSITION | 422 | The transfer's status doesn't allow this action |
Withdrawal Errors
| Code | HTTP Status | Cause |
|---|---|---|
WITHDRAWAL_NOT_FOUND | 404 | No such withdrawal for this user under your customer |
VALIDATION_ERROR | 422 | Invalid field, for example a network that doesn't match the currency (source.pointer: /network) |
INSUFFICIENT_BALANCE | 400 | The user's available balance is too low |
INVALID_VERIFICATION_CODE | 400 | Wrong verification code |
VERIFICATION_CODE_EXPIRED | 400 | The verification code expired |
VERIFICATION_BLOCKED | 403 | Too many wrong codes; the withdrawal is BLOCKED |
INVALID_STATE_TRANSITION | 422 | The withdrawal's status doesn't allow this action |
SERVICE_UNAVAILABLE | 503 | Crypto withdrawals aren't available for your account yet |
Trading Errors
| Code | HTTP Status | Cause |
|---|---|---|
WALLET_NOT_FOUND | 404 | The wallet in the path isn't yours |
QUOTE_NOT_FOUND | 404 | No such quote for this wallet |
QUOTE_EXPIRED | 400 | The quote expired—request a new one |
ORDER_NOT_FOUND | 404 | No such order for this wallet |
INVALID_STATE_TRANSITION | 422 | The quote was already executed |
BAD_GATEWAY | 502 | The trading venue couldn't price or execute the request |
QUOTE_PERSISTENCE_FAILED | 502 | The quote couldn't be recorded—request a new one |
A quote the venue rejects at execution doesn't fail the call: the order comes back 201 with status: "FAILED" and status_reason.code: "QUOTE_REJECTED". If the venue's answer is lost, the order comes back 201 with status: "PENDING" and status_reason.code: "EXECUTION_OUTCOME_UNKNOWN": the quote may have executed, so don't retry it; follow the order until it settles.
Customer Errors
| Code | HTTP Status | Cause |
|---|---|---|
CUSTOMER_NOT_FOUND | 404 | No such customer for your session, or it was deleted |
ACCOUNT_OWNER_REQUIRED | 403 | Only the account holder's session can delete the customer or issue or revoke credentials; a team member's session can't |
CUSTOMER_ALREADY_EXISTS | 409 | The customer is already registered (source.pointer: /customer_id) |
CREDENTIAL_NOT_FOUND | 404 | No such credential on this customer |
OWNER_CREDENTIAL_NOT_FOUND | 404 | The customer has no owner credential yet |
CREDENTIAL_ISSUANCE_FAILED | 502 | The credential couldn't be issued upstream; retry |
EMAIL_POLLER_NOT_FOUND | 404 | The customer has no email poller |
WEBHOOK_NOT_FOUND | 404 | The customer has no webhook configured |
INVALID_STATE_TRANSITION | 422 | The poller is already in the state you asked for |
VAULT_NOT_CONFIGURED | 404 | The customer's vault isn't configured yet |
TREASURY_WALLET_NOT_CONFIGURED | 404 | The customer has no treasury wallet yet |
Debugging Tips
Trace IDs
Every response carries a trace_id in meta and in the x-trace-id response header. Log it for every failed request and include it when you report an issue—it's the fastest way for support to find the exact call.
Pass your own x-trace-id (32 lowercase hex characters) to stitch a request to your application logs. The API keeps it and echoes it back; a hyphenated UUID is compacted, and any other value is replaced with a fresh id instead of failing the request.
curl -H "x-trace-id: 4bf92f3577b34da6a3ce929d0e0e4736" \
-H "x-api-key: YOUR_API_KEY" \
"https://api.sandbox.sovera.io/sovx/v1/users"Common Troubleshooting
- 401 Unauthorized—Check the
x-api-keyorAuthorization: Bearerheader, and mint a fresh user token if it's older than 24 hours. - 403 Forbidden—Use the credential the endpoint expects.
- 404 Not Found—Verify the id and that it belongs to your customer (and, with a user token, to that user).
- 409 Conflict—A resource with that email, phone, or id already exists, or the same idempotency key is still running.
- 422 Unprocessable Entity—Read
sourceto find the field, parameter, or header that failed, or check the resource's status forINVALID_STATE_TRANSITION. - 429 Too Many Requests—Wait the
retry-afterseconds, then back off exponentially:
async function retryWithBackoff(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (error.response?.status === 429) {
const retryAfter = Number(error.response.headers['retry-after'] ?? 2 ** i);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
} else {
throw error;
}
}
}
throw new Error('Max retries exceeded');
}- 502 / 504—An upstream service had an issue. Retry after a short delay with the same
x-idempotency-key.
Error Handling Example
import axios from 'axios';
async function handleApiCall() {
try {
const response = await axios.get('https://api.sandbox.sovera.io/sovx/v1/users', {
headers: { 'x-api-key': 'YOUR_API_KEY' }
});
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
const apiError = error.response?.data;
const { code, message, source } = apiError?.errors?.[0] ?? {};
const traceId = apiError?.meta?.trace_id;
console.error(`API Error [${code}]: ${message} ${JSON.stringify(source ?? {})} (trace: ${traceId})`);
switch (code) {
case 'UNAUTHORIZED':
// Re-authenticate or mint a fresh user token
break;
case 'RATE_LIMIT_EXCEEDED':
// Wait retry-after seconds and retry
break;
case 'QUOTE_EXPIRED':
// Request a new quote and retry
break;
default:
throw error;
}
}
throw error;
}
}Support
Hit persistent errors? Contact support with:
- Trace ID from the error response
- Timestamp of the request
- Endpoint and HTTP method called
- Request body / parameters (redact sensitive values)
- Email: [email protected]
Webhooks
What webhooks are for, how Sovera delivers them, how to verify and process them safely, and every event your endpoint can receive.
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.