Troubleshooting

The failures that actually happen, and what each one means.

Troubleshooting

It works locally and fails in production

The deployed origin is not on the allowlist. A pk_test_ key skips the check, which is exactly why it worked on localhost. Add the origin in Settings, scheme and port included.

Part of the capture is blank

Almost always a cross-origin iframe, a video, or a tainted canvas — see Limits. Check result.warnings, which names what could not be inlined. Switching to engine: 'display-media' captures all of it at the cost of a permission prompt.

The text renders in the wrong font

A webfont that could not be inlined, usually because it is served from a CDN without Access-Control-Allow-Origin. Self-host the font, or add the header. Rasterization happens inside an SVG foreignObject, which cannot make external requests — anything not inlined is not available at all.

Rasterization failed and the page looks fine

Rendering happens inside an SVG foreignObject, which is parsed as XML — stricter than HTML about two things HTML accepts happily. Both are handled for you, and neither changes a pixel of the result:

  • Framework binding attributes — dmx-on:click, v-on:click, @click, (click), x-on:click and the like. XML reads the part before the colon as an undeclared namespace prefix. They are behaviour rather than appearance, so they are dropped from the clone.
  • Control characters in text — a vertical tab or a stray NUL from pasted content, a CSV import or a templating engine. XML forbids every control character except tab, newline and carriage return. They are invisible on the page, so they are stripped.

If a capture still fails this way, the error names the line, the column and the offending markup — that text is the fault, and it is worth sending to us.

The capture is cut off

Check area. viewport deliberately stops at the fold. For an element taller than the window you want element, and for the full page document. If output is being scaled down instead, you have hit maxDimension.

Captures are slow

Call preload() when the user hovers your export button — most of the cost is fetching and inlining fonts and images, and that work is cacheable. Then look at result.timings to see which stage is actually expensive. Dropping scale from 3 to 2 removes over half the pixels.

The webhook never arrives

Three things, in order: the capture must have been completed (an abandoned upload never fires), the endpoint must be enabled and subscribed to the event, and your server must answer 2xx within the timeout. Every attempt and response is on the Webhooks page — start there rather than guessing.

A capture URL that worked yesterday now 404s

Captures are deleted after three days by default, and signed URLs are clamped to the same window — see Retention. Raise retention_days to as much as seven if that is too tight, but if you need it to outlive that, copy the bytes somewhere of your own when the webhook arrives. Storing the URL and fetching it later will not work.

A delivery failed and nothing happened

That is the design — we deliver once and leave the retry to you, so a handler that half-succeeded isn’t run again behind your back. Fix the cause, then press Resend on the Webhooks page; the response code and body from the failed attempt are listed there to tell you what to fix.

Some captures never produce a webhook

Check for webhook: false — per call, on autoBind, on the client config, or as data-s2a-webhook on the element. The response from complete says skipped: true when delivery was turned off, which distinguishes it from having no endpoints configured.

Signature verification always fails

You are almost certainly verifying against a parsed body. Frameworks that JSON-parse before your handler runs change the bytes; you need the raw buffer. In Express that means express.raw() on this route, and in Next.js reading await request.text() before parsing.