Error handling
All Watsi API surfaces — Fastify services and Next.js API routes — return a consistent error envelope. This page documents the standard shape, every error code, and recommended retry strategies.
Standard error response
Every error response has the same envelope. The top-level error field is always an object with code and message:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "email is required",
"details": {} // optional — extra context when available
}
}| Field | Type | Description |
|---|
error.code | string | Machine-readable error code (e.g. UNAUTHORIZED) |
error.message | string | Human-readable description of what went wrong |
error.details | object? | Optional object with field-level errors or extra context |
Error codes
| Status | Code | Description |
|---|
| 400 | VALIDATION_ERROR | The request body or query parameters failed validation. Check the message for details. |
| 400 | BAD_REQUEST | The request is malformed or cannot be processed in its current state. |
| 401 | UNAUTHORIZED | Missing or invalid authentication credentials. Include a valid API key or session. |
| 403 | FORBIDDEN | The authenticated user does not have permission to perform this action. |
| 404 | NOT_FOUND | The requested resource does not exist or is not accessible. |
| 405 | METHOD_NOT_ALLOWED | The HTTP method is not supported for this endpoint. |
| 409 | CONFLICT | The request conflicts with the current state (e.g. duplicate resource). |
| 429 | RATE_LIMITED | Too many requests. Back off and retry after the Retry-After interval. |
| 500 | INTERNAL_ERROR | An unexpected server error occurred. Retry with exponential backoff. |
| 502 | EXTERNAL_API_ERROR | An upstream service (WhatsApp, AI provider) returned an error. |
| 503 | SERVICE_UNAVAILABLE | The service is temporarily unavailable. Retry after a short delay. |
Example responses
Validation error (400)
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "email is required"
}
}Unauthorized (401)
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": {
"code": "UNAUTHORIZED",
"message": "Unauthorized"
}
}Not found (404)
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"error": {
"code": "NOT_FOUND",
"message": "Chat not found"
}
}Rate limited (429)
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json
{
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded. Retry after 30 seconds."
}
}Internal error (500)
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
"error": {
"code": "INTERNAL_ERROR",
"message": "Internal server error"
}
}Retry strategies
| Code | Retryable? | Strategy |
|---|
VALIDATION_ERROR | No | Fix the request parameters before retrying. |
UNAUTHORIZED | No | Check your API key or re-authenticate. |
FORBIDDEN | No | Verify you have the required permissions (admin role, etc.). |
NOT_FOUND | No | Verify the resource ID exists. |
CONFLICT | No | The resource already exists. Fetch the existing resource or use a different identifier. |
RATE_LIMITED | Yes | Wait for the duration in the Retry-After header, then retry. Default limit: 100 req/min per API key. |
INTERNAL_ERROR | Yes | Retry with exponential backoff: 1s, 2s, 4s, up to 3 attempts. |
EXTERNAL_API_ERROR | Yes | An upstream provider failed. Retry with exponential backoff. |
SERVICE_UNAVAILABLE | Yes | The service is temporarily down. Retry after 5–30 seconds. |
Handling errors in code
const res = await fetch('https://api.watsi.ai/api/v1/conversations', {
headers: { Authorization: `Bearer ${apiKey}` },
})
if (!res.ok) {
const body = await res.json()
const { code, message } = body.error
switch (code) {
case 'RATE_LIMITED':
const retryAfter = res.headers.get('Retry-After') ?? '30'
await sleep(Number(retryAfter) * 1000)
return retry()
case 'UNAUTHORIZED':
throw new Error('Invalid API key')
default:
throw new Error(`API error [${code}]: ${message}`)
}
}