Errors
The ZapAPI error envelope — deterministic HTTP status codes, machine-readable error codes and field-level validation messages.
ZapAPI uses a single, deterministic error format across every endpoint. You can write one error handler and use it everywhere.
The error envelope
Every non-2xx response uses the same envelope as successful responses, with
status: "error" and an errors array of field-level messages:
{
"status": "error",
"statusCode": 422,
"message": "Validation failed",
"errors": ["amount must be a positive integer"],
"timestamp": "2026-10-25T00:00:00.000Z",
"path": "/api/v1/invoices"
}For machine-readable branching, many errors also carry a code:
{
"status": "error",
"statusCode": 403,
"code": "missing_scope",
"message": "Missing required scope: invoices:write",
"timestamp": "2026-10-25T00:00:00.000Z",
"path": "/api/v1/invoices"
}HTTP status codes
ZapAPI uses conventional HTTP status codes:
| Code | Meaning | When you see it |
|---|---|---|
400 | Bad Request | Malformed JSON, invalid enum value, missing required field. |
401 | Unauthorized | Missing, invalid, expired or revoked API key. |
403 | Forbidden | Valid key, but missing a required scope. |
404 | Not Found | The resource does not exist (or belongs to another project). |
409 | Conflict | Idempotency-key replay with a different body; state conflict. |
422 | Unprocessable Entity | Semantic validation failed (e.g., amount out of range). |
429 | Too Many Requests | Rate limit exceeded — see Retry-After header. |
500 | Internal Server Error | Something broke on our side — retried automatically when safe. |
502 / 503 | Bad Gateway / Service Unavailable | Upstream (LNbits, node) issue — retry with backoff. |
Machine-readable codes
Common code values you can branch on:
| Code | HTTP | Meaning |
|---|---|---|
invalid_api_key | 401 | Key is malformed, revoked or expired. |
key_expired | 401 | Key passed its expiresAt. |
missing_scope | 403 | Key lacks the required scope. |
resource_not_found | 404 | Resource does not exist. |
idempotency_conflict | 409 | Same key, different body. |
validation_failed | 422 | One or more fields failed validation. |
rate_limited | 429 | Rate limit exceeded. |
upstream_unavailable | 503 | LNbits / Lightning node unavailable. |
The set of codes is additive — new codes may be introduced, but existing codes will not change meaning. Branch on the codes you care about and treat unknown codes as generic errors.
Handling errors in the SDK
The TypeScript SDK throws a typed ZapAPIError with helpers for
the common cases:
import { ZapAPI, ZapAPIError } from '@zapapi/sdk';
try {
await zap.invoices.create(params);
} catch (err) {
if (err instanceof ZapAPIError) {
err.status; // 400, 401, 403, 409, 429, …
err.code; // 'invalid_api_key', 'missing_scope', …
err.payload; // raw API error body
err.isUnauthorized; // 401
err.isForbidden; // 403
err.isNotFound; // 404
err.isConflict; // 409 — idempotency conflict
err.isRateLimited; // 429
}
}Retrying safely
- Idempotent retries — for any request that used an
Idempotency-Key, retrying with the same key is always safe. See Idempotency. - 429s — respect the
Retry-Afterheader and back off exponentially. - 5xxs — retry with exponential backoff; if you used an idempotency key, the retry is safe.
- 409s — do not retry blindly; a conflict means the original request succeeded with a different body.
Validation errors
422 responses include a field-level errors array. Each entry is a
human-readable message naming the field and the problem:
{
"statusCode": 422,
"message": "Validation failed",
"errors": [
"amount must be a positive integer",
"currency must be one of: sat, USD, EUR, GBP, INR, BRL, JPY, CAD, AUD"
]
}Surface these to your users directly — they are written to be end-user friendly.