Error Handling
Every response from the Tulu Switch API — success or error — follows the same JSON envelope. Understanding the response shape lets you build a single consistent error handler for your integration.
Success Response Shape
{
"success": true,
"statusCode": 200,
"message": "Success",
"data": { ... },
"timestamp": "2025-01-01T00:00:00.000Z"
}success — true on success
statusCode — the HTTP status code (e.g. 200, 201)
message — a human-readable summary, often the controller's return message
data — the response payload; shape varies by endpoint
timestamp — ISO 8601 datetime of the response
Error Response Shape
{
"success": false,
"statusCode": 400,
"message": "Currency NGN is not supported for the STABLECOIN channel",
"errors": [
"currency must be one of: USDC, USDT"
],
"data": null,
"timestamp": "2025-01-01T00:00:00.000Z",
"path": "/v2/purse/deposit"
}success — false on all errors
statusCode — the HTTP status code (400, 401, 403, 404, 409, 503…)
message — primary error message string
errors — optional array of validation messages when multiple field errors exist
data — always null on error responses
timestamp — ISO 8601 datetime of the error
path — the request path that generated the error, useful for debugging
HTTP Status Codes
400 Bad Request- Invalid or missing required fields in the request body
- Currency not supported for the specified channel
- Zero or negative amount
- Bank transfer (payout) attempted in TEST environment
- Unsupported subscription interval for the selected provider
401 Unauthorized- Missing or malformed Authorization header
- Invalid public key (not found in database)
- Secret key does not match the stored hash
- Expired or invalid JWT (Customer API)
- Revoked API key
- Key prefix environment mismatch (e.g. using a pk_test_ key on a pk_live_ route)
403 Forbidden- Account is SUSPENDED, DEACTIVATED, or INACTIVE
- Email address not yet verified
- KYB compliance status is not APPROVED (for LIVE operations)
- Attempting to generate a LIVE key without KYB approval
- Team member lacks the required role or permission
404 Not Found- Transaction ID does not exist
- Customer ID does not exist
- Wallet not found for the specified currency and provider
- Webhook config not registered for this environment
- No provider adapter registered for the requested provider
409 Conflict- API key limit reached (5 active keys per environment)
- Wallet already exists for the currency and provider combination
- Webhook already configured for this environment
- Duplicate transaction reference
503 Service Unavailable- Seerbit not configured (missing credentials)
- Hedera not configured
- Flutterwave credentials not set
- Refund attempted with Seerbit (not supported)
- Subscription attempted with Seerbit (not supported)
How to Handle Errors
Use the success boolean as the primary gate, then branch on statusCode for specific handling. The errors array (when present) contains field-level details for validation errors.
async function apiCall(url: string, options: RequestInit) {
const res = await fetch(url, options);
const body = await res.json();
if (!body.success) {
const { statusCode, message, errors, path } = body;
switch (statusCode) {
case 400:
// Validation issue — show errors[] to the user
throw new ValidationError(message, errors);
case 401:
// Re-authenticate or rotate keys
throw new AuthError(message);
case 403:
// Account or compliance issue
throw new ForbiddenError(message);
case 409:
// Duplicate — check if the original request succeeded
throw new ConflictError(message);
case 503:
// Provider unavailable — retry with a different provider or later
throw new ServiceError(message);
default:
throw new ApiError(statusCode, message, path);
}
}
return body.data;
}Best Practices
- Always check
body.successbefore accessingbody.data - Log the
pathandtimestampfields alongside errors for quick debugging - Surface the
errorsarray to end users for 400 validation responses - Implement exponential backoff for 503 errors — the provider may be temporarily unavailable
- For 409 conflicts on transaction references, look up the original transaction rather than retrying blindly
- Never expose raw error bodies to end users in production