Appearance
Errors
When a request fails, Papermill returns a JSON body alongside the HTTP status:
json
{
"statusCode": 402,
"message": "You have exceeded your quota for pages generated. Please contact support@papermill.io or upgrade your plan.",
"timestamp": "2026-07-10T18:00:00.000Z",
"data": { "code": "ERR_QUOTA_EXCEEDED", "cap": 1000, "usage": 1000 }
}statusCode, message, and timestamp are always present. Everything else is optional, so read defensively.
Commonly returned codes
message is written for a human reading a log and may be reworded at any time. data.code is a stable identifier for a specific issue.
data.code | Status | Meaning |
|---|---|---|
ERR_QUOTA_EXCEEDED | 402 | Production page allowance for the billing period is used up. This is only enforced on free accounts, we allow paid subscriptions to continue rendering and charge an overage based on subscription tier. |
ERR_DRAFT_QUOTA_EXCEEDED | 402 | Draft page allowance is used up; draft quota only applies to free accounts. |
ERR_RATE_LIMITED | 429 | Too many requests. Honour the Retry-After header and retry. |
ERR_RENDER_TIMEOUT | 504 | The render took too long. Reduce the document or its remote assets. |
ERR_TEMPLATE_NOT_FOUND | 404 | The template_id does not exist or you lack permissions. |
ERR_TEMPLATE_EMPTY | 400 | The template has no content yet. |
ERR_SLUG_TAKEN | 409 | Another template in your workspace already uses that slug. |
ERR_SLUG_INVALID | 400 | The slug is not a legal name. The message names the rule it broke. |
ERR_WORKSPACE_UNRESOLVED | 409 | The account is not associated with a workspace, so slugs are unavailable. |
ERR_KEY_REVOKED | 401 | The API key has been revoked. |
ERR_KEY_EXPIRED | 401 | The API key is no longer valid. |
ERR_ACCOUNT_DISABLED | 403 | The account is disabled. Contact support support@papermill.io. |
ERR_EMAIL_NOT_VERIFIED | 403 | Verify your email address before continuing. |
Additional fields
Depending on the failure, data may carry more:
| Field | Appears on | Meaning |
|---|---|---|
cap | ERR_QUOTA_EXCEEDED, ERR_DRAFT_QUOTA_EXCEEDED | The page allowance for the period. |
usage | ERR_QUOTA_EXCEEDED, ERR_DRAFT_QUOTA_EXCEEDED | Pages used so far in the period. |
A Press authoring error adds a top-level stackTrace array of source locations, pointing at the elements responsible. POST /v2/validate reports the problems as structured findings before you attempt a render, but cannot find issues like elements that do not fit.
Statuses without a code
| Status | Cause |
|---|---|
| 400 | The request or the document is malformed — for example JSON, CSV, or markdown sent without template_id, or an image src which cannot be accessed by us. |
| 401 | The API key is missing or not recognised. |
| 413 | The request body exceeded 32 MB. |
| 415 | The Content-Type is not one Papermill accepts. The message names the accepted types. |
| 500 | Contact support support@papermill.io or retry the request. |
Handling errors
Retry 429 after the delay in Retry-After, and treat 402, 401, and 403 as terminal — retrying will not help until the account or key changes. 504 means the document was too large or too slow to fetch remote assets; retrying an unchanged document is likely to time out again.
python
import os
import requests
response = requests.post(
"https://api.papermill.io/v2/pdf",
params={"template_id": "papermill-simple-report"},
headers={
"Authorization": f"Bearer {os.environ['PAPERMILL_API_KEY']}",
"Content-Type": "text/markdown",
},
data=b"# Report",
)
if not response.ok:
error = response.json()
code = error.get("data", {}).get("code")
if code == "ERR_RATE_LIMITED":
retry_after = int(response.headers.get("Retry-After", "5"))
... # wait retry_after seconds, then retry
elif code == "ERR_QUOTA_EXCEEDED":
... # allowance is gone for this period; alert rather than retry
else:
raise RuntimeError(error["message"])javascript
const response = await fetch('https://api.papermill.io/v2/pdf?template_id=papermill-simple-report', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PAPERMILL_API_KEY}`,
'Content-Type': 'text/markdown',
},
body: '# Report',
})
if (!response.ok) {
const error = await response.json()
switch (error.data?.code) {
case 'ERR_RATE_LIMITED': {
const retryAfter = Number(response.headers.get('Retry-After') ?? 5)
break // wait retryAfter seconds, then retry
}
case 'ERR_QUOTA_EXCEEDED':
break // allowance is gone for this period; alert rather than retry
default:
throw new Error(error.message)
}
}