Error reference
Every error response is JSON of the form:
{
"error": "stable_code",
"reason": "Human-readable message",
"details": null
}
Stable error codes are safe to switch on in client code. Reasons are human-readable and can change between releases.
HTTP status codes
| HTTP |
error code |
When | Retry? |
|---|---|---|---|
| 400 |
validation |
Request body failed JSON Schema validation against the template's variables_schema | No (fix payload) |
| 401 |
unauthorized |
Missing or invalid Bearer token | No |
| 402 |
quota_exceeded |
Free plan monthly cap reached. Paid plans bill overage instead | Next billing cycle, or upgrade |
| 404 |
not_found |
Template slug missing, archived, or render id unknown | No |
| 422 |
validation |
Same as 400 — schema validation. We use 400 for "bad envelope", 422 for "bad data" | No (fix payload) |
| 429 |
rate_limited |
Per-minute rate limit on the API key |
After Retry-After seconds |
| 500 |
render_failed |
Render service crashed mid-render. Almost always transient | Yes (with backoff) |
| 502 |
no_render_nodes / all_nodes_unhealthy |
No healthy render node available | Yes (backoff) |
| 504 |
render_timeout |
Render didn't return within 25s |
Yes (try async: true for large jobs) |
Validation errors
When error is validation, the details array contains field paths:
{
"error": "validation",
"reason": "data does not match template variables_schema",
"details": [
{ "field": "customer.name", "message": "is required" },
{ "field": "items.0.amount", "message": "must be a number" }
]
}
Rate limits
Each API key has a per-minute limit set by the workspace's plan:
| Plan | Rate limit |
|---|---|
| Free | 10 / min |
| Starter | 30 / min |
| Growth | 60 / min |
| Business | 120 / min |
| Scale | 300 / min |
| Enterprise | Custom |
Exceeding the limit returns 429 with a Retry-After header (seconds). All official SDKs map this to QuotaError / AirpdfQuotaException and expose retryAfter.
Quota enforcement
Different from rate limits: quota is the monthly PDF count.
-
Free plan: hard cap. Returns
402 quota_exceededonce exhausted. - Paid plans: soft cap. Renders continue, overage billed at the per-PDF rate.
Reset on the first day of the customer's billing cycle.
Idempotency
Airpdf does not currently honor Idempotency-Key headers. Plan to add in a future release. For async renders, the id returned can be used to deduplicate — polling the same id twice is always safe.
Network errors
Outside HTTP — DNS failure, TLS handshake failure, socket timeout — are surfaced by SDKs as ServerError / AirpdfServerException. Retry with exponential backoff (e.g. 1s, 2s, 4s, 8s, max 30s).
Stable codes contract
The error string is a stable identifier. If we add new codes, we'll list them here and announce them in the changelog before they appear in production traffic.
| Code | Stable since |
|---|---|
validation |
v1.0 |
unauthorized |
v1.0 |
quota_exceeded |
v1.0 |
not_found |
v1.0 |
rate_limited |
v1.0 |
render_failed |
v1.0 |
render_timeout |
v1.0 |
no_render_nodes |
v1.0 |
all_nodes_unhealthy |
v1.0 |
render_server_error |
v1.0 |
Debugging tips
-
Always log the full response body, not just the status code. The
reasonfield usually pinpoints the issue. -
For 422/400, log the
detailsarray — it tells you exactly whichdatafield failed schema validation. -
Use
airpdf_test_*keys during development — they don't count against quota. - Watch the status page before assuming a 500/502 is on your side.