Error handling
Every Zentalk API error returns an appropriate HTTP status code and a consistent JSON envelope. The official TypeScript client maps these onto a small hierarchy of error classes with machine-readable codes.
Error response format
Section titled “Error response format”All error responses use the ErrorResponse envelope:
| Field | Type | Presence | Description |
|---|---|---|---|
success |
boolean | always | Always false for errors |
error |
string | always | Human-readable error message, safe to display |
code |
string | optional | Machine-readable error code (e.g. RATE_LIMIT_EXCEEDED, NOT_FOUND) |
request_id |
string | optional | Request tracking ID; include it when reporting issues |
{ "success": false, "error": "Group not found or access denied", "code": "NOT_FOUND", "request_id": "1c4f2a6e-9b0d-4e21-8f3a-7d5c1e0b9a42"}code and request_id are omitted when not set, so the minimal shape is:
{ "success": false, "error": "Authorization header required"}Handle errors by inspecting the HTTP status first and the code field second; never parse the error message text, which may change.
HTTP status codes
Section titled “HTTP status codes”These are the statuses the API actually returns, as declared in the OpenAPI specification:
| Status | Name | When it is used |
|---|---|---|
| 400 | Bad Request | Missing or invalid parameters, validation failure |
| 401 | Unauthorized | Missing, invalid, expired, or malformed token — see Authentication |
| 403 | Forbidden | Authenticated but insufficient permissions (e.g. not group owner or channel admin) |
| 404 | Not Found | Resource does not exist (group, channel, message, user) |
| 409 | Conflict | Request conflicts with current state (e.g. username taken, duplicate resource) |
| 410 | Gone | Retired API version — see API versioning and lifecycle |
| 429 | Too Many Requests | Rate limit exceeded — see Rate limits |
| 500 | Internal Server Error | Unexpected server-side error |
| 503 | Service Unavailable | Server temporarily unavailable (maintenance, overload) |
Per-endpoint status behavior is documented in the REST API reference.
Client-side error classes
Section titled “Client-side error classes”The TypeScript client (src/api/errors.ts) wraps failures in a typed hierarchy so callers can branch on error kind rather than string matching:
// Base application errorclass AppError extends Error { code: ErrorCodeType; originalError?: unknown; timestamp: Date; getUserMessage(): string;}
// API error with HTTP contextclass APIError extends AppError { statusCode?: number; endpoint?: string;}
// Network-level failure (no HTTP response)class NetworkError extends AppError {}
// End-to-end encryption failure with severityclass E2EEError extends AppError { securityLevel: 'warning' | 'critical';}
// Media URL access deniedclass MediaAccessDeniedError extends Error { statusCode: number; // 403 url: string; reason: 'forbidden' | 'expired' | 'removed' | 'unknown';}Error codes
Section titled “Error codes”The ErrorCode enum identifies failures across all client modules. Representative codes by category:
| Category | Codes |
|---|---|
| Authentication | AUTH_FAILED, AUTH_EXPIRED, AUTH_INVALID_TOKEN, AUTH_NOT_INITIALIZED, AUTH_NO_WALLET, AUTH_SIGNATURE_FAILED |
| Network | NETWORK_ERROR, TIMEOUT, RATE_LIMITED, SERVICE_UNAVAILABLE |
| E2EE | ENCRYPTION_FAILED, DECRYPTION_FAILED, KEY_NOT_FOUND, KEY_BUNDLE_FAILED, MITM_DETECTED, EXPIRED_PREKEY, INVALID_PREKEY, KEY_DERIVATION_FAILED |
| WebSocket | WS_CONNECTION_FAILED, WS_DISCONNECTED, WS_SEND_FAILED |
| Calls | CALL_FAILED, CALL_REJECTED, CALL_SIGNALING_FAILED, PEER_CONNECTION_FAILED |
E2EE security levels
Section titled “E2EE security levels”E2EEError carries a securityLevel that dictates how the client must react:
| Level | Example codes | Required action |
|---|---|---|
critical |
MITM_DETECTED, INVALID_PREKEY |
Warn the user; require manual key verification |
warning |
KEY_NOT_FOUND, DECRYPTION_FAILED, EXPIRED_PREKEY |
Often recoverable via key rotation or session reset |
Type guards and helpers
Section titled “Type guards and helpers”isAppError(error): error is AppErrorisAPIError(error): error is APIErrorisE2EEError(error): error is E2EEErrorisMediaAccessDeniedError(error): error is MediaAccessDeniedError
createError(code, originalError?): AppErrorcreateAPIError(code, statusCode?, endpoint?, originalError?): APIErrorgetErrorMessage(code): stringHandling pattern
Section titled “Handling pattern”import { isAPIError, isE2EEError } from '@/api/errors';
try { await sendMessage(to, content, getAuthHeaders);} catch (error) { if (isAPIError(error) && error.statusCode === 401) { await refreshSession(); // re-authenticate, then retry once } else if (isAPIError(error) && error.statusCode === 429) { scheduleRetry(error); // honor Retry-After, see Rate limits } else if (isE2EEError(error) && error.securityLevel === 'critical') { showSecurityWarning(error.getUserMessage()); } else { console.error('API error:', error); }}Retry guidance
Section titled “Retry guidance”| Status | Retry? | How |
|---|---|---|
| 400, 403, 404, 409 | No | Fix the request; retrying will fail identically |
| 401 | After re-auth | Refresh the token, then retry once |
| 410 | No | Migrate to the successor API version |
| 429 | Yes | Wait Retry-After seconds, then retry with backoff |
| 500 | Idempotent requests only | Exponential backoff with jitter, bounded attempts |
| 503 | Yes | Exponential backoff with jitter, bounded attempts |
| Network errors / timeouts | Idempotent requests only | Backoff; the request may have been processed |
Realtime delivery failures over the WebSocket surface as WS_* codes; the connection layer handles reconnection and replay — see WebSocket connection.
