Skip to content
CastPDF
Documentation menu
API

Errors and rate limits

Every CastPDF error code with its HTTP status, what it means and what to do, plus the rate limits per plan and Retry-After.

On this page

Error format

Every error response has the same JSON body. code is stable and meant for your code; message is for people and may change. docs_url links to the code's row below. details is present only for some codes: template errors include the Liquid line and column when they are known.

JSON
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit of 60 requests per minute exceeded. Retry in 42 seconds.",
    "docs_url": "https://castpdf.com/docs/errors#rate_limited"
  }
}

Every response, errors included, has an x-request-id header. Include it when you contact support.

Error codes

These are the codes the /v1 API returns, with their HTTP status:

CodeHTTPMeaningWhat to do
invalid_request400The request failed validation: invalid JSON, a missing, unknown or out-of-range field, both or neither of template_id and html, css together with template_id, metadata over 8 KB, a malformed Idempotency-Key, or page options the renderer cannot use. A body that is not application/json gets this code with HTTP 415.Fix the request; the message names the problem. Retrying the same request fails again.
invalid_api_key401The Authorization header is missing, is not "Bearer <key>", or the key is unknown or revoked.Send Authorization: Bearer with an active key from the dashboard.
plan_limit_reached402A live key used all documents included in this period, and overage is not available: the Free plan, a scheduled cancellation, an overdue payment, no active subscription, or this period’s overage was already billed. Also returned when creating a template would go over the plan’s template limit (Free 5, Starter 25, Growth 100, Pro 2,000 and Scale 2,000); then details.limit is "templates" and details.max is the number. Deleted templates are kept for 30 days, and all templates together are limited to that number plus 100; over it, details.limit is "templates_including_deleted".Upgrade, resolve the payment, or wait for the next period. Test keys are not affected. For templates, delete one you no longer need, or upgrade.
spending_cap_reached402The next overage document would take this period’s overage cost above your spending cap.Raise the spending cap in the dashboard or upgrade. Renders resume immediately.
forbidden403The team is suspended, a signed download link has an invalid signature, or a test key tried to create, change, pin or delete a template (test keys can only read and preview templates).For a suspended team, contact support. For a download, use the url returned by the API unchanged. To change templates, use a live key or the dashboard.
not_found404No route matches the method and path.Check the method and path against the API reference.
template_not_found404The template does not exist, was deleted, belongs to another workspace, or the requested version does not exist.Check template_id and template_version (GET /v1/templates/:id/versions lists the versions that exist).
document_not_found404No document with this id exists for your workspace. Test keys only see test documents.Check the id; read live documents with a live key.
idempotency_conflict409A request with the same Idempotency-Key is still being processed.Wait for the first request to finish, then retry with the same key to receive its result.
file_expired410The PDF is no longer stored: its retention period ended, the signed link expired, or an Idempotency-Key refers to a document whose file is gone.Generate the document again (with a new Idempotency-Key if you replayed one).
document_too_large413The rendered HTML is larger than 5 MB, the PDF would have more than 50 pages, or the PDF would be larger than 40 MB. The message says which.Split the document or reduce its content. For a PDF over 40 MB, use smaller or fewer images.
payload_too_large413The request body is larger than 5 MB, or, rarely, the rendered document is too large to render.Send less data, or move large assets to public URLs the renderer can fetch.
template_render_error422The Liquid template could not be parsed or rendered: a syntax error, an unknown filter, bad filter input, template HTML over 500 KB (a stored template, or the html of POST /v1/pdf when data is sent), or a render over the 1 second or 64 MB limit. details.line and details.column point at the problem when known.Fix the template or the data. Retrying unchanged fails again.
render_failed422The document crashed the renderer, for example by exhausting its memory.Simplify the document (very large images, heavy scripts). Retrying unchanged is likely to fail again.
content_lost422In print mode, part of the document could not fit on any page and would have been cut off, so no PDF was made. Known causes: a table inside a table cell that is taller than a page, and a cell with rowspan that is taller than a page. Print mode cannot split either.Put a long inner table after the outer table instead of inside it, or split the rowspan cell into one row per line. Or render the document in fast mode, which splits both. Retrying unchanged fails again.
idempotency_mismatch422This Idempotency-Key was already used with a different request body.Use a new key for a different request.
rate_limited429Too many requests in the current one-minute window for your workspace and key mode, or from your IP address. Also returned when your team already has as many PDFs rendering at once as its plan allows (Free 1, Starter 2, Growth 3, Pro 4 and Scale 4); then details.reason is "concurrency" and details.limit is the number. Template changes are also limited to 60 a minute per team.Wait for the number of seconds in the Retry-After header, then retry.
storage_limit_reached429The team already stores as many PDFs as its plan allows (Free 500 MB, Starter 5 GB, Growth 20 GB, Pro 60 GB and Scale 200 GB), or 200 MB of test-key PDFs. details.scope is "plan" or "test".Request the PDF directly ("response": "binary", no Idempotency-Key) meanwhile. Older files expire on their own and free the space.
internal_error500Something went wrong on our side. The message may include a reference; the x-request-id header always identifies the request.Retry once. If it persists, contact support with the x-request-id.
service_busy503Too many documents are being prepared at once, or the rendering service is busy or restarting. Also returned when storing PDFs is paused because the server is low on disk space; PDFs returned directly ("response": "binary") still work.Retry after a few seconds, with backoff. If storing is paused, request the PDF directly until it resumes.
render_timeout504Rendering took longer than 30 seconds (loading resources, pagination and printing included).Reduce the document’s size or slow external resources. Retrying unchanged is likely to time out again.

Rate limits

Requests are limited per minute, per team and per key mode: all live keys of a team share one budget, and all test keys share another. A live key's budget depends on the plan:

KeyRequests per minute
Live key, Free plan30
Live key, Starter plan60
Live key, Growth plan120
Live key, Pro plan300
Live key, Scale plan600
Test key (any plan)20
  • Previews (POST /v1/templates/:id/preview) also have a limit of 30 per minute per team and key mode.
  • At the same time, a team can have this many PDFs rendering: Free 1, Starter 2, Growth 3, Pro 4 and Scale 4, across live keys, test keys, previews and the dashboard. One more gets 429 rate_limited with details: { "reason": "concurrency", "limit": N } and Retry-After: 1.
  • Per IP address, all requests together (including file downloads and requests with an invalid key) are limited to 600 per minute.

Responses to authenticated requests carry the budget of the limit that applies:

HeaderMeaning
x-ratelimit-limitRequests allowed per minute
x-ratelimit-remainingRequests left in the current window
x-ratelimit-resetSeconds until the window resets
Retry-AfterOn a 429 only: seconds to wait before retrying

A request over the limit gets 429 with the code rate_limited. Wait for Retry-After seconds, then retry.

Retrying safely

  • 429 rate_limited: retry after the Retry-After delay.
  • 503 service_busy: retry after a few seconds, with exponential backoff. If the message says storing PDFs is paused, request the PDF directly ("response": "binary") meanwhile.
  • 429 storage_limit_reached: the team's stored PDFs are at the plan's limit. Retrying soon gives the same answer; request the PDF directly, or wait for older files to expire (stored PDF limits).
  • 500 internal_error: retry once; if it persists, contact support with the x-request-id.
  • 409 idempotency_conflict: the first request with this key is still running; retry later with the same key.
  • Network errors and timeouts: retry POST /v1/pdf with the same Idempotency-Key. If the first attempt succeeded, you get its document back and nothing is counted twice (idempotency).
  • 4xx codes other than 409 and 429 describe a problem with the request, the template or your plan: retrying the same request gives the same answer.
Node.js 20+
async function createPdf(body, idempotencyKey) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch('https://api.castpdf.com/v1/pdf', {
      method: 'POST',
      headers: {
        Authorization: 'Bearer YOUR_API_KEY',
        'Content-Type': 'application/json',
        'Idempotency-Key': idempotencyKey,
      },
      body: JSON.stringify(body),
    });
    const retryable = res.status === 429 || res.status === 503;
    if (!retryable || attempt === 5) return res;
    const seconds = Number(res.headers.get('retry-after')) || 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
  }
}
Python (requests)
import time
import requests

def create_pdf(body, idempotency_key):
    for attempt in range(1, 6):
        res = requests.post(
            "https://api.castpdf.com/v1/pdf",
            headers={"Authorization": "Bearer YOUR_API_KEY", "Idempotency-Key": idempotency_key},
            json=body,
            timeout=60,
        )
        if res.status_code not in (429, 503) or attempt == 5:
            return res
        time.sleep(int(res.headers.get("Retry-After", 2 ** attempt)))

Other codes

The dashboard uses the same error format. These codes come only from the dashboard (sign-in, API keys, billing and the Generate PDF button), never from /v1:

CodeHTTPMeaningWhat to do
unauthenticated401The dashboard session is missing or expired.Sign in again.
invalid_credentials401The email or password is incorrect.Check them, or reset the password.
invalid_token400An email verification or password reset link is invalid or has expired.Request a new link.
payment_failed402A payment for a billing change (for example an upgrade) failed at the payment provider.Check the payment method and try again.
email_not_verified403The account email is not verified yet. Until it is, you cannot use the dashboard’s Generate PDF button, create live API keys, subscribe to a paid plan or make other billing changes. A checkout also needs the team owner’s email to be verified.Verify the email address. The dashboard can send a new link from its banner, from next to Generate PDF and from the verify page.
email_taken409An account with this email already exists.Sign in instead.
subscription_exists409The team already has a subscription.Change the plan of the existing subscription instead.
no_subscription409The billing action needs an active subscription, and the team has none.Choose a plan to subscribe.
no_customer409The team has no billing account yet.Subscribe to a plan first.
subscription_locked409The subscription cannot change right now: a renewal or plan change is in progress, a payment is overdue, or a cancellation is scheduled.Follow the message (update the payment method, resume the subscription, or try again after renewal).
checkout_in_progress409Another checkout for the team is in progress.Try again in a moment.
billing_provider_error502The billing provider rejected the request or returned an unexpected response.Try again later.