Skip to content

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.codeStatusMeaning
ERR_QUOTA_EXCEEDED402Production 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_EXCEEDED402Draft page allowance is used up; draft quota only applies to free accounts.
ERR_RATE_LIMITED429Too many requests. Honour the Retry-After header and retry.
ERR_RENDER_TIMEOUT504The render took too long. Reduce the document or its remote assets.
ERR_TEMPLATE_NOT_FOUND404The template_id does not exist or you lack permissions.
ERR_TEMPLATE_EMPTY400The template has no content yet.
ERR_SLUG_TAKEN409Another template in your workspace already uses that slug.
ERR_SLUG_INVALID400The slug is not a legal name. The message names the rule it broke.
ERR_WORKSPACE_UNRESOLVED409The account is not associated with a workspace, so slugs are unavailable.
ERR_KEY_REVOKED401The API key has been revoked.
ERR_KEY_EXPIRED401The API key is no longer valid.
ERR_ACCOUNT_DISABLED403The account is disabled. Contact support support@papermill.io.
ERR_EMAIL_NOT_VERIFIED403Verify your email address before continuing.

Additional fields

Depending on the failure, data may carry more:

FieldAppears onMeaning
capERR_QUOTA_EXCEEDED, ERR_DRAFT_QUOTA_EXCEEDEDThe page allowance for the period.
usageERR_QUOTA_EXCEEDED, ERR_DRAFT_QUOTA_EXCEEDEDPages 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

StatusCause
400The 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.
401The API key is missing or not recognised.
413The request body exceeded 32 MB.
415The Content-Type is not one Papermill accepts. The message names the accepted types.
500Contact 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)
  }
}