Error Codes Reference
Complete reference for all error codes, HTTP statuses and debugging tips for the Sovera Integration Layer API.
Reference every error code the Sovera Integration Layer API returns.
Errors are predictable. Every failure—whether a missing field, a revoked key, or an upstream outage—comes back in one envelope with the same shape. Branch your logic on the HTTP status for the broad category and on the code for the exact reason; treat the human-readable message as a hint to log or surface, not something to parse. Each code is derived from the error: the API first matches keywords in the message (for example, a "not found" about a user yields USER_NOT_FOUND), then falls back to a code based on the HTTP status.
Error Response Format
Every error returns success: false, an errors array, and a meta block. The errors array carries one or more objects, each with a stable code and a message. The meta block carries the timestamp, the API version, and a trace_id that ties the response to server-side logs—capture it for every failed request.
{
"success": false,
"errors": [
{
"code": "ERROR_CODE",
"message": "Human-readable error description"
}
],
"meta": {
"timestamp": "2026-03-12T00:00:00.000Z",
"version": "v1",
"trace_id": "abc-123-def-456"
}
}HTTP Status Codes
| Status | Meaning | When It Occurs |
|---|---|---|
| 200 | OK | Request successful |
| 201 | Created | Resource created successfully |
| 400 | Bad Request | Invalid request data or business rule violation |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Authenticated but not permitted |
| 404 | Not Found | Resource doesn't exist |
| 405 | Method Not Allowed | HTTP method not supported |
| 409 | Conflict | Resource already exists |
| 413 | Payload Too Large | Request body too large |
| 415 | Unsupported Media Type | Wrong Content-Type |
| 422 | Unprocessable Entity | Semantically invalid request |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unexpected server error |
| 502 | Bad Gateway | Upstream service error (Utila/SFOX) |
| 503 | Service Unavailable | Service temporarily unavailable |
| 504 | Gateway Timeout | Upstream service timed out |
Authentication & Authorization Errors
| Code | HTTP Status | Message | Cause |
|---|---|---|---|
UNAUTHORIZED | 401 | Varies (for example, "API key is required", "Invalid API key", "Client is inactive") | Missing, invalid, revoked, or inactive x-api-key; missing or invalid master API key |
FORBIDDEN | 403 | Varies (for example, "Access denied") | Authenticated but lacking permission for the resource |
The code for authentication failures is always UNAUTHORIZED (401) or FORBIDDEN (403); the specific reason is in the message. A 401 means the credential is missing, malformed, revoked, or expired—fix the header or mint a fresh user token. A 403 means the credential is valid but lacks reach—switch to the API key (or master key) the endpoint expects.
How to fix:
# Owner API Key (most User API endpoints)
curl -H "x-api-key: YOUR_OWNER_API_KEY" ...
# Re-generate a user token
curl -X POST "BASE_URL/users/{user_id}/token" \
-H "x-api-key: YOUR_OWNER_API_KEY"Validation Errors
| Code | HTTP Status | Message | Cause |
|---|---|---|---|
UNPROCESSABLE_ENTITY | 422 | Field-specific message | Missing required fields, wrong types, invalid format |
BAD_REQUEST | 400 | Human-readable description | Generic bad request or business-rule violation |
Field validation fails with 422 Unprocessable Entity. The errors array carries one entry per invalid field, so you can map each failure back to its input.
Example:
{
"success": false,
"errors": [
{
"code": "UNPROCESSABLE_ENTITY",
"message": "email must be a valid email address"
},
{
"code": "UNPROCESSABLE_ENTITY",
"message": "phone_number must be a valid mobile number for the country"
}
],
"meta": {
"timestamp": "2026-03-12T00:00:00.000Z",
"version": "v1",
"trace_id": "5b8f3a9d-2c7e-4a1b-9f6d-0e3c2b1a4d5f"
}
}User Errors
| Code | HTTP Status | Message | Cause |
|---|---|---|---|
USER_NOT_FOUND | 404 | User not found | Incorrect user_id or user belongs to different client |
RESOURCE_NOT_FOUND | 404 | User not found or inactive | User is deleted or inactive |
EMAIL_ALREADY_EXISTS | 409 | User with this email already exists for this customer | Duplicate email on create/update |
PHONE_ALREADY_EXISTS | 409 | User with this phone number already exists for this customer | Duplicate phone on create/update |
RESOURCE_CONFLICT | 409 | User with this user_id already exists | Duplicate user_id |
INVALID_USER_TYPE | 400 | Invalid user type | Unsupported user type value |
INVALID_ACCOUNT_TYPE | 400 | account_type must be one of: individual, corporate | Wrong enum value |
INVALID_ACCOUNT_ROLE | 400 | account_role must be one of: third | Wrong enum value |
INVALID_ACCOUNT_PURPOSE | 400 | account_purpose must be one of: trading, investing | Wrong enum value |
INVALID_AGE | 400 | User must be at least 18 years old | DOB fails age validation |
CANNOT_DELETE_ACTIVE_USER | 400 | Cannot delete an active user | Deactivate user before deleting |
CANNOT_UPDATE_DELETED_USER | 400 | Cannot update a deleted user | User is soft-deleted |
CLIENT_NO_SUPPLIER_ID | 400 | Client does not have a supplier_custody_id configured | Admin must configure vault first |
TOKEN_GENERATION_FAILED | 500 | Failed to generate user token | Internal error during token creation |
Wallet Errors
| Code | HTTP Status | Message | Cause |
|---|---|---|---|
WALLET_NOT_FOUND | 404 | Wallet not found | Incorrect wallet_id or wallet belongs to different user |
ADDRESS_NOT_FOUND | 404 | Wallet address not found | No address for given currency |
WALLET_INACTIVE | 400 | Wallet is inactive | Wallet has been deactivated |
WALLET_LOCKED | 400 | Wallet is locked | Wallet temporarily locked |
WALLET_SUSPENDED | 400 | Wallet is suspended | Contact admin |
INVALID_WALLET_ID | 400 | Invalid wallet ID | Malformed wallet ID format |
WALLET_NAME_REQUIRED | 400 | Wallet name is required | Missing name field |
WALLET_NAME_MAX_LENGTH | 400 | Wallet name exceeds maximum length (100 chars) | Name too long |
RESOURCE_CONFLICT | 409 | Wallet with name already exists for this user | Duplicate wallet name |
INVALID_CURRENCY | 400 | Invalid currency code | Unsupported or misspelled currency |
UNSUPPORTED_CURRENCY | 400 | Unsupported currency | Currency not enabled for vault |
CURRENCY_NOT_ACTIVE | 400 | Currency is not active | Currency disabled |
ADDRESS_ALREADY_EXISTS | 409 | Address already exists for this wallet | Address for this currency already created |
ADDRESS_GENERATION_FAILED | 500 | Failed to generate wallet address | Utila/SFOX error during address creation |
INSUFFICIENT_BALANCE | 400 | Insufficient balance | Available balance too low for operation |
SAME_WALLET_TRANSFER | 400 | Cannot transfer to the same wallet | from_wallet_id equals to_wallet_id |
AMOUNT_MUST_BE_POSITIVE | 400 | Amount must be positive | Zero or negative amount |
AMOUNT_TOO_SMALL | 400 | Amount must be at least 0.000001 | Below minimum transfer threshold |
TRANSFER_LIMIT_EXCEEDED | 400 | Transfer amount exceeds limit | Per-transaction cap exceeded |
DAILY_LIMIT_EXCEEDED | 400 | Daily transfer limit exceeded | Daily cap reached |
MONTHLY_LIMIT_EXCEEDED | 400 | Monthly transfer limit exceeded | Monthly cap reached |
Trading / Order Errors
| Code | HTTP Status | Message | Cause |
|---|---|---|---|
QUOTE_NOT_FOUND | 404 | Quote not found | Quote ID invalid or belongs to different client |
QUOTE_EXPIRED | 400 | Quote has expired | Quotes expire quickly — fetch a fresh one |
QUOTE_INVALID | 400 | Invalid quote | Quote already used or malformed |
INVALID_CURRENCY_PAIR | 400 | Invalid currency pair | Pair not supported |
PAIR_NOT_AVAILABLE | 400 | Currency pair not available | Pair disabled or unavailable |
INVALID_SIDE | 400 | Invalid side value | Must be buy or sell |
QUANTITY_REQUIRED | 400 | Quantity is required | Missing quantity field |
QUANTITY_MUST_BE_POSITIVE | 400 | Quantity must be positive | Zero or negative quantity |
INSUFFICIENT_BALANCE | 400 | Insufficient balance for order | Not enough funds to execute |
MINIMUM_ORDER_NOT_MET | 400 | Order amount below minimum | Below minimum order size |
MAXIMUM_ORDER_EXCEEDED | 400 | Order amount exceeds maximum | Exceeds per-order cap |
MARKET_CLOSED | 400 | Market is closed | Trading outside market hours |
LIQUIDITY_INSUFFICIENT | 400 | Insufficient liquidity | Not enough market depth |
PRICE_CHANGED | 400 | Price has changed since quote | Re-fetch quote and retry |
ORDER_FAILED | 400 | Order failed | SFOX rejected the order |
Withdrawal Errors
| Code | HTTP Status | Message | Cause |
|---|---|---|---|
WITHDRAWAL_NOT_FOUND | 404 | Withdrawal not found | Incorrect withdrawal_id |
INVALID_CRYPTO_ADDRESS | 400 | Invalid cryptocurrency address | Wrong format or wrong network |
INVALID_ADDRESS | 400 | Invalid destination address | Address validation failed |
INVALID_AMOUNT | 400 | Invalid amount | Zero, negative, or non-numeric |
AMOUNT_MUST_BE_POSITIVE | 400 | Amount must be positive | Negative or zero amount |
INSUFFICIENT_BALANCE | 400 | Insufficient balance for withdrawal | Available balance too low |
WITHDRAWAL_LIMIT_EXCEEDED | 400 | Withdrawal amount exceeds limit | Per-withdrawal cap exceeded |
DAILY_LIMIT_EXCEEDED | 400 | Daily withdrawal limit exceeded | Daily cap reached |
MONTHLY_LIMIT_EXCEEDED | 400 | Monthly withdrawal limit exceeded | Monthly cap reached |
MINIMUM_AMOUNT_NOT_MET | 400 | Withdrawal amount below minimum | Below minimum threshold |
CONFIRMATION_CODE_INVALID | 400 | Invalid confirmation code | Wrong PIN entered |
CONFIRMATION_CODE_EXPIRED | 400 | Confirmation code has expired | PIN expired — initiate a new withdrawal |
ALREADY_CONFIRMED | 400 | Withdrawal already confirmed | Do not re-submit a confirmed withdrawal |
INVALID_WEBHOOK_SIGNATURE | 401 | Invalid webhook signature | Webhook secret mismatch |
PIN_NOTIFICATION_FAILED | 400 | Failed to send PIN notification | Email delivery issue |
BANK_ACCOUNT_REQUIRED | 400 | Bank account information is required | Missing fiat withdrawal bank details |
INVALID_BANK_ACCOUNT | 400 | Invalid bank account information | Bank details failed validation |
CRYPTO_WITHDRAWAL_FAILED | 400 | Crypto withdrawal failed | Upstream error from Utila/SFOX |
FIAT_WITHDRAWAL_FAILED | 400 | Fiat withdrawal failed | Upstream fiat processing error |
System & Gateway Errors
| Code | HTTP Status | Message | Cause |
|---|---|---|---|
INTERNAL_SERVER_ERROR | 500 | Internal server error | Unexpected error — include trace_id when reporting |
BAD_GATEWAY | 502 | Bad gateway | Upstream service (Utila or SFOX) returned an error |
SERVICE_UNAVAILABLE | 503 | Service temporarily unavailable | Maintenance or overload |
GATEWAY_TIMEOUT | 504 | Gateway timeout | Upstream service did not respond in time |
NOT_IMPLEMENTED | 501 | Not implemented | Feature not yet available |
RATE_LIMIT_EXCEEDED | 429 | Too many requests | Slow down and implement backoff |
PAYLOAD_TOO_LARGE | 413 | Payload too large | Request body exceeds size limit |
UNSUPPORTED_MEDIA_TYPE | 415 | Unsupported media type | Set Content-Type: application/json |
METHOD_NOT_ALLOWED | 405 | Method not allowed | Wrong HTTP verb for this endpoint |
Debugging Tips
Trace IDs
Every response carries a trace_id in meta that correlates it with server-side logs. Log it for every failed request and include it whenever you report an issue—it's the fastest way for support to find the exact call.
{
"meta": {
"trace_id": "5b8f3a9d-2c7e-4a1b-9f6d-0e3c2b1a4d5f"
}
}Or pass your own x-trace-id to stitch a request to your application logs end-to-end. The API echoes it back instead of generating one:
curl -H "x-trace-id: my-custom-trace-123" \
-H "x-api-key: YOUR_API_KEY" \
BASE_URL/usersCommon Troubleshooting
- 401 Unauthorized — Check
x-api-keyheader is present and lowercase, key is not revoked - 403 Forbidden — Endpoint requires Owner API Key or higher permission level
- 404 Not Found — Verify the resource ID and that it belongs to your client account
- 409 Conflict — A resource with that email, phone, or name already exists
- 400 Bad Request — Read the
messagefield — it will point to the specific field or rule that failed - 429 Too Many Requests — Implement exponential backoff:
async function retryWithBackoff(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (error.response?.status === 429) {
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
} else {
throw error;
}
}
}
throw new Error('Max retries exceeded');
}- 502 / 504 — External service (Utila or SFOX) issue. Retry after a short delay.
Error Handling Example
import axios from 'axios';
async function handleApiCall() {
try {
const response = await axios.get('BASE_URL/users', {
headers: { 'x-api-key': 'YOUR_API_KEY' }
});
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
const apiError = error.response?.data;
const code = apiError?.errors?.[0]?.code;
const message = apiError?.errors?.[0]?.message;
const traceId = apiError?.meta?.trace_id;
console.error(`API Error [${code}]: ${message} (trace: ${traceId})`);
switch (code) {
case 'UNAUTHORIZED':
// Re-authenticate
break;
case 'RATE_LIMIT_EXCEEDED':
// Back off and retry
break;
case 'QUOTE_EXPIRED':
// Fetch 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]