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

2xx success
{
  "success": true,
  "statusCode": 200,
  "message": "Success",
  "data": { ... },
  "timestamp": "2025-01-01T00:00:00.000Z"
}

successtrue 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

4xx / 5xx error
{
  "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"
}

successfalse 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.

errorHandler.ts
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.success before accessing body.data
  • Log the path and timestamp fields alongside errors for quick debugging
  • Surface the errors array 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
For a mapping of status code values to their meanings, see the Status Codes page.