Skip to content
CastPDF
Errors

Troubleshooting

Fixes for missing images and fonts, blank pages, cut off content, page numbers, template errors, 401, 402 and 429 errors, webhooks, emails and expired links.

On this page

This page lists the problems people meet most often, each with its cause and the fix. Most of them show up as a warning or an error code in the response, so start there: a binary response reports problems in the x-warnings and x-blocked-resources headers, a "response": "url" response in warnings and blocked_resources, and an error in error.code. Every error code is explained on the errors page.

An image is blank or missing

Cause. The renderer fetches images itself, and it only fetches http and https URLs on ports 80 and 443 that resolve to public internet addresses. An image on localhost, on a private network (such as 10.x.x.x or 192.168.x.x), behind a login, or on a local file path (file://) cannot be loaded. A relative path such as images/logo.png has no web server behind it either: your HTML is rendered on its own, not from your site. A refused or failed request does not fail the document; the image is just missing.

Fix.

  1. Check x-blocked-resources (the number) or blocked_resources (up to 10 URLs with their reasons), for example "http://10.0.0.5/logo.png (address 10.0.0.5:80)".
  2. Put the image on a public https URL, or embed it as a data: URL (<img src="data:image/png;base64,...">), which needs no network request at all.
  3. For your logo, upload it once in the dashboard and print brand.logo_url in the template. CastPDF passes the logo to the renderer directly, so it does not depend on the network (your brand).

An image URL that answers 404 (a typo, a deleted file) is not listed in blocked_resources: the request worked, the server just had no image. Open the URL in a browser to check it. The full rules are in images and fonts and the print CSS guide.

A font does not load, or the wrong font is used

Cause. A font file is fetched like an image, under the same network rules, so a font on a private address is blocked and the browser falls back to the next font in your font-family list. Other causes: the name in font-family does not match the name in @font-face exactly, or the weight you use (such as 700) was never loaded, so the browser makes a synthetic bold.

Fix.

  • Look for the font URL in blocked_resources. Load fonts from a public https URL or a public font service, or embed them as data: URLs.
  • Use the built-in fonts, which need no loading: Plus Jakarta Sans, Inter, Noto Sans, Noto Sans Arabic, Noto Sans Hebrew and Noto Sans SC, plus the installed Noto, Liberation and DejaVu families (fonts).
  • A font you uploaded in the dashboard is added to a document only when the CSS names its family in a font-family or font declaration. Write the family name in your CSS, for example font-family: "{{ brand.font }}", sans-serif;.
  • Load every weight and style you use, and list a fallback such as sans-serif last.

There is an extra blank page at the end

Cause. Something asks for one more page. The usual causes are a break-after: page (or page-break-after: always) on the last element, a wrapper with height: 100vh or a fixed height that, together with margins or padding, is a little taller than the printable area, or a last block that does not quite fit and is pushed onto a new page by break-inside: avoid.

Fix. Remove the page break from the last element:

CSS
.page { break-after: page; }
.page:last-child { break-after: auto; }

Replace page-height wrappers with normal flow, or make them a few millimetres shorter than the page minus its margins. Remember that the printable height is the page height minus the top and bottom margins (20mm each unless you set your own).

Content is cut off on the right

Cause. Something is wider than the page's content area: a table with many columns, an image with a fixed width, a white-space: nowrap block, or a long word or URL in a table cell. In print mode CastPDF reports this as an overflow warning that names the element (its tag plus up to two classes) and the page, for example {"code": "overflow", "selector": "td.desc", "page": 2}. The PDF is still made.

Fix. Give tables a fixed layout at the full width, let long values wrap in their cells, keep images inside the page, and avoid fixed widths in pixels for the main layout:

CSS
table { width: 100%; table-layout: fixed; }
td, th { overflow-wrap: anywhere; }
img { max-width: 100%; }

A landscape page ("orientation": "landscape") helps for wide tables. See warnings.

A table header does not repeat on the next page

Cause. In print mode, CastPDF repeats a table's header row on every page the table continues onto, but only when that row is inside a <thead>. A header row in <tbody>, or a header built from <div> elements, is not a table header. CSS that gives the thead display: table-row-group turns the repetition off on purpose (the dashboard's switch does the same).

Fix. Put the header row in <thead>, render in print mode (the default for templates), and remove any display: table-row-group rule on thead if you want it repeated. See repeating table headers.

Page numbers are missing

Cause. Page numbers in the page margins (@bottom-center with counter(page) and counter(pages)) are drawn by the print mode pagination, and that is the mode CastPDF tests them in. Raw html requests render in fast mode by default, where the browser's own print engine handles @page. In fast mode, running headers (string-set) and page numbers in a table of contents (target-counter) have no effect. The second common cause is a bottom margin too small for the margin content.

Fix. Send "mode": "print" (or set print mode in the template's page settings), and keep a bottom margin large enough for the numbers (the dashboard's "Page X of Y" switch needs about 15 mm). See fast and print mode.

The request fails with a content lost error

Cause. In print mode, part of the document could not fit on any page and would have been cut off, so CastPDF refuses to return an incomplete PDF (422 content_lost). Print mode splits tall rows and blocks across pages and scales tall images down, but it cannot split two layouts: a table inside a table cell that is taller than a page, and a cell with rowspan that is taller than a page.

Fix. Put the long inner table after the outer table instead of inside a cell, or give each line of a tall rowspan cell its own row. If you need none of the print-only features, fast mode splits both. See keeping rows together.

Spots are empty and there are missing variable warnings

Cause. The template prints a variable that your data does not have, so that spot is empty. CastPDF reports each one as a missing_variable warning with its path, such as {"code": "missing_variable", "path": "customer.name"}. Variables inside a loop are reported against their list, such as items[].discount. The usual reasons are a typo, a different key name in your data (customerName against customer.name), or a value that is only sometimes present.

Fix. Compare the paths in the warnings with your data and rename one side. For values that are optional, give a fallback in the template, such as {{ customer.vat_id | default: "" }}, or wrap the block in {% if customer.vat_id %}. A warning never fails the request, so read the warnings in your logs or tests. See warnings.

The request fails with a template render error

Cause. 422 template_render_error means the Liquid template could not be parsed or rendered: a syntax error such as an unclosed {% if %}, an unknown filter, a filter given the wrong kind of value (such as money on text), a template over 500 KB, or a render that took longer than 1 second or used more than 64 MB.

Fix. Read error.message, and look at error.details.line and error.details.column, which point at the tag or output that failed in the template's HTML, counted from 1. Syntax errors and unknown filters are caught when you save a template; errors that depend on the data appear only at render time, so test with realistic data. See errors and line numbers.

The request fails with a render timeout or a document too large error

Cause. 504 render_timeout means rendering took longer than 30 seconds, including loading external resources, pagination and printing. Slow image or font servers (each request may take up to 10 seconds), heavy scripts and very long documents are the usual causes. 413 document_too_large means the HTML after Liquid is larger than 5 MB, the PDF would have more than 50 pages, or it would be larger than 40 MB. The message says which.

Fix. For timeouts, host images and fonts on fast servers or embed small ones as data: URLs, remove scripts you do not need, and try fast mode for long documents that need no print-only features. For size, split the document, compress or resize large images, and avoid embedding huge data: URLs. Retrying the same request unchanged usually fails again.

401 invalid API key, or 404 for a document that exists

Cause. 401 invalid_api_key means the Authorization header is missing, is not Bearer <key>, or the key is unknown or revoked. A key copied with a missing character, a line break or quotes does not match. A 404 document_not_found (or batch_not_found) for an id you know exists usually means a test key asked for a live document: test keys see only test documents.

Fix. Send Authorization: Bearer cpdf_live_... with a key that is active in the dashboard; call GET /v1/me to check which key and mode the server sees. Read live documents with a live key. See authentication and test and live mode.

402 plan limit reached

Cause. 402 plan_limit_reached means a 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 already billed. The same code comes back when creating a template would go over your plan's template limit (details.limit is templates). 402 spending_cap_reached is the related case on paid plans: the next overage document would go over your spending cap.

Fix. Upgrade, resolve the payment, raise the spending cap in the dashboard, or wait for the next period. Test keys keep working in every case, so development is never blocked. See usage and billing.

429 rate limited

Cause. 429 rate_limited means too many requests in the current minute for your team and key mode, too many PDFs rendering at once (details.reason is concurrency), or one of the background limits (details.limit is open_jobs or test_jobs_per_day).

Fix. Wait for the Retry-After seconds, then retry with the same Idempotency-Key. Limit how many requests you send in parallel to the number of PDFs your plan renders at once. Every limit and a backoff strategy are on the rate limits page.

A webhook does not arrive

Cause. CastPDF only sends webhooks to https URLs on public addresses (ports 443, 80 or 8443), so an endpoint on localhost or a private network is refused when you create it. Other causes: the endpoint is for the other mode (test endpoints receive only test PDFs, live endpoints only live PDFs), it does not subscribe to the event, it was disabled after failing for 24 hours, your plan does not include webhooks (Starter, Growth, Pro and Scale do), or your server answers with something other than a 2xx within 10 seconds. Redirects are not followed. If the request arrives but your code rejects it, the signature check is failing.

Fix.

  1. Open the endpoint's delivery log in the dashboard or with GET /v1/webhooks/:id/deliveries: it shows each attempt with the status code and the start of your answer.
  2. Send a test event (POST /v1/webhooks/:id/test) to check the path end to end.
  3. For local development, use a tunnel that gives your machine a public https URL.
  4. Verify the signature over the raw request body, before any JSON parsing, with the endpoint's current secret, and check that your server's clock is right: signatures older than 5 minutes are rejected. See verifying the signature.
  5. Answer 2xx at once and do the slow work afterwards.

An email is not delivered

Cause. An emailed PDF (deliver.email) is queued and sent in the background, so the request succeeds before the email goes out. Its state is in delivery.email of the document (GET /v1/pdf/:id): queued, sent, failed or suppressed. A failed email has a reason: over_limit (it would go over a daily email limit and was not sent), not_stored (there was no stored PDF to send) or send_failed (all retries failed). suppressed means the address bounced or complained before, so CastPDF no longer sends to it.

Fix. For queued, wait: failed sends are retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. For sent, the email left CastPDF: ask the recipient to check their spam folder, and look for the sender name "CastPDF". For over_limit, check the limits on the email delivery page. For suppressed, ask for another address. In test mode, and on the Free plan, PDFs can be emailed only to verified email addresses of your own team.

Cause. A signed download link works until the document's expires_at, which is the end of its file retention (your plan's retention, or 1 day for test PDFs). After that the file is deleted and the link returns 410 file_expired. A link that was changed (for example with & turned into &amp; by an HTML template or an email tool) returns 403 forbidden instead.

Fix. While the PDF is still stored, fetch a fresh link from GET /v1/pdf/:id; every link of a document expires at the same time. After the retention ends, generate the document again (with a new Idempotency-Key). If you need PDFs for longer, download them when they are made and keep them in your own storage. See file retention and signed URLs.

Still stuck

Every response has an x-request-id header. Send it to support together with the error code and what you expected, and we can look at that exact request.

Updated Something unclear? [email protected], a real person replies.