Errors
Every 4xx and 5xx response from the Lisoloo API uses the same JSON
shape. Branch on error_code, never on message — messages are
localised and may change for clarity; codes are part of the public API
contract.
The envelope
Section titled “The envelope”{ "success": false, "status_code": 400, "error_code": "1101", "message": "Invalid amount or recipient list", "details": [ { "field": "to[0]", "message": "Must match E.164 format", "code": "INVALID_PHONE_NUMBER" } ], "timestamp": "2026-05-27T10:15:00Z", "request_id": "req_8f3a4c2e9b1d7a6f", "retryable": false, "retry_after": null}| Field | Type | Notes |
|---|---|---|
success | bool | Always false on error. Easy guard: if (!body.success) { … }. |
status_code | int | Mirrors the HTTP status. |
error_code | string | Four-digit stable code. Branch on this. |
message | string | Human-readable. Localised by Accept-Language. Show to ops, not end users. |
details | array | null | Field-level breakdown for validation errors (1110). |
timestamp | ISO 8601 | Server time the error was generated. |
request_id | string | Quote when contacting support. |
retryable | bool | true for transient failures. Never auto-retry on false. |
retry_after | int | null | Seconds to wait when retryable: true (and 429s). |
Code groups at a glance
Section titled “Code groups at a glance”The leading digit groups the cause:
| Range | Group | Example codes |
|---|---|---|
1001–1005 | Authentication | 1001 INVALID_APP_KEY, 1004 APP_SUSPENDED |
1101–1110 | Request validation | 1101 INVALID_RECIPIENTS, 1104 INVALID_PHONE_NUMBER, 1109 MISSING_REQUIRED_FIELD |
1201–1205 | Business logic | 1202 INSUFFICIENT_BALANCE, 1203 RECIPIENT_LIMIT_EXCEEDED |
1301–1305 | Security & rate-limit | 1301 RATE_LIMIT_EXCEEDED |
1401–1405 | Server / system | 1401 INTERNAL_SERVER_ERROR, 1402 SERVICE_UNAVAILABLE |
1501–1505 | Carrier / SMS processor | 1502 SMS_REJECTED_BY_CARRIER, 1503 SMS_TIMEOUT |
The full catalogue with remediation is at Errors → catalogue.
Branching on error_code
Section titled “Branching on error_code”function handleLisolooError(body: { error_code: string; message: string; retryable: boolean; retry_after: number | null;}) { switch (body.error_code) { case "1001": // INVALID_APP_KEY case "1004": // APP_SUSPENDED // Stop. Don't retry. Surface to operations. throw new AuthenticationFailed(body.message);
case "1101": // INVALID_RECIPIENTS case "1104": // INVALID_PHONE_NUMBER case "1109": // MISSING_REQUIRED_FIELD // Bad input on our side. Fix and don't retry the same body. throw new ValidationError(body.message);
case "1202": // INSUFFICIENT_BALANCE // Surface to dashboard, alert ops, do not retry. throw new BalanceExhausted(body.message);
case "1301": // RATE_LIMIT_EXCEEDED // Honour retry_after. return scheduleRetry(body.retry_after ?? 60);
case "1402": // SERVICE_UNAVAILABLE case "1503": // SMS_TIMEOUT if (body.retryable) { return scheduleRetry(body.retry_after ?? 30); } throw new ProcessorFailure(body.message);
default: throw new UnknownError(body.error_code, body.message); }}HTTP status mapping
Section titled “HTTP status mapping”Some codes map onto multiple HTTP statuses depending on context (e.g.
1004 APP_SUSPENDED is 403, while 1001 INVALID_APP_KEY is 401).
The HTTP status is the broad category; error_code is the specific
cause. Use the HTTP status for routing through middleware (auth failures
to one handler, validation failures to another); use error_code for
the actual logic.
| HTTP | Meaning |
|---|---|
400 | Validation failure. Don’t retry. |
401 | Auth header missing or invalid. Don’t retry. |
402 | Payment required (balance). Don’t retry. |
403 | Authenticated but forbidden (suspended key). Don’t retry. |
404 | Resource not found (e.g. unknown message_id). |
429 | Rate-limited. Honour Retry-After. |
500 | Server error. Safe to retry once with exponential backoff. |
502 / 503 | Upstream / carrier issue. Safe to retry. |
details on validation failures
Section titled “details on validation failures”When the gateway returns 1110 INVALID_FIELD_FORMAT (or 1109 MISSING_REQUIRED_FIELD), the details array carries one entry per failed
field:
{ "error_code": "1110", "message": "Field validation failed", "details": [ { "field": "to[2]", "message": "Must match E.164 format", "code": "INVALID_PHONE_NUMBER" }, { "field": "message", "message": "Must be 1-1600 characters", "code": "INVALID_LENGTH" }, { "field": "scheduled_dates[0].time", "message": "Must be HH:mm", "code": "INVALID_TIME_FORMAT" } ]}The field path uses JSONPointer-style dot notation with [n] for
array indices. Iterate details to render per-field errors in your UI.
See also
Section titled “See also”- Error catalogue — every code with remediation
- Authentication — the auth failure modes
- Rate limits — the
Retry-Aftercontract