Troubleshooting
Error Reference
Every error code, what it means, and how to handle it. All errors follow a consistent JSON format with a requestId for tracing.
Error Format
All API errors return a consistent ApiError shape. The requestId is unique per request and can be used for debugging with the admin audit log.
JSON
{ "statusCode": 404, "error": "CASE_NOT_FOUND", "message": "Case cas_abc123 not found", "requestId": "req_xyz789"}Error Codes
| Code | Status | Retry | Description | Action |
|---|---|---|---|---|
| UNAUTHORIZED | 401 | No | Invalid or expired JWT token | Refresh the JWT using onTokenRefresh callback |
| FORBIDDEN | 403 | No | Insufficient permissions for this action | Check user roles match required permissions |
| CASE_NOT_FOUND | 404 | No | Support case does not exist | Verify the case ID is correct |
| VALIDATION_ERROR | 400 | No | Request body failed schema validation | Check the request body against the API reference |
| RATE_LIMITED | 429 | Yes | Too many requests | Back off exponentially and retry |
| INTERNAL_ERROR | 500 | Yes | Unexpected server error | Retry with backoff. If persistent, contact support |
Rate Limits
| Endpoint | Limit | Scope |
|---|---|---|
| POST /api/cases | 10 per minute | Per user |
| POST /api/cases/:id/messages | 30 per minute | Per user |
| All other endpoints | 60 per minute | Per user |
Retry Pattern
For 429 and 500 errors, use exponential backoff. The widget handles this automatically, but here's the pattern for custom integrations:
JavaScript
async function callWithRetry(fn, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (err) { if (err.statusCode === 429) { // Exponential backoff: 1s, 2s, 4s const delay = Math.pow(2, attempt - 1) * 1000; await new Promise(r => setTimeout(r, delay)); continue; } throw err; // Non-retryable error } } throw new Error('Max retries exceeded');}