Skip to content

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.

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
Example error response
{
"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.

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.

The TypeScript client (src/api/errors.ts) wraps failures in a typed hierarchy so callers can branch on error kind rather than string matching:

Error class hierarchy
// Base application error
class AppError extends Error {
code: ErrorCodeType;
originalError?: unknown;
timestamp: Date;
getUserMessage(): string;
}
// API error with HTTP context
class APIError extends AppError {
statusCode?: number;
endpoint?: string;
}
// Network-level failure (no HTTP response)
class NetworkError extends AppError {}
// End-to-end encryption failure with severity
class E2EEError extends AppError {
securityLevel: 'warning' | 'critical';
}
// Media URL access denied
class MediaAccessDeniedError extends Error {
statusCode: number; // 403
url: string;
reason: 'forbidden' | 'expired' | 'removed' | 'unknown';
}

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

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
isAppError(error): error is AppError
isAPIError(error): error is APIError
isE2EEError(error): error is E2EEError
isMediaAccessDeniedError(error): error is MediaAccessDeniedError
createError(code, originalError?): AppError
createAPIError(code, statusCode?, endpoint?, originalError?): APIError
getErrorMessage(code): string
Branching on error type
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);
}
}
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.