How to avoid duplicate PDFs (and double-billing) with idempotency keys
An idempotency key is a string you generate per logical document and attach to your PDF request; if Docweave sees the same key again, it returns the original stored PDF instead of rendering (and billing) a new one. This matters because network timeouts, retry logic, and webhook re-deliveries all cause the exact same request to hit an API more than once. Without an idempotency key, each duplicate call produces a second PDF and a second usage event on your account.
The double-generate problem
PDF generation is a write operation dressed up as a read. You POST a source, wait a few hundred milliseconds to a few seconds while Chromium renders it, and get bytes back. That waiting window is exactly where duplicate requests come from:
- A client-side timeout fires before the response arrives, and your retry logic re-sends the same request.
- A mobile client loses connectivity mid-render and the user taps "Generate invoice" again.
- A webhook provider (Stripe, an order system, a form backend) re-delivers the same event because your handler was slow to return a 200.
- A queue worker crashes after sending the render request but before marking the job done, so it retries on restart.
In every case, the underlying intent — "generate this one invoice" — happened once, but the API call happened twice. If you're billed per document (as most PDF APIs are, including this one — see pricing), that's a silent double-charge. If the duplicate PDF gets emailed or attached somewhere, it's also a customer-facing bug: two copies of the same receipt, two versions of a contract with slightly different timestamps in the footer.
How an idempotency key fixes it
You generate a key that uniquely identifies the *document*, not the *HTTP request*, and pass it alongside your source. Docweave stores the result keyed to your account plus that string. If the same key shows up again — from a retry, a duplicate webhook, or a user double-click — the API returns the original PDF instead of rendering again, and does not record a second usage_event or charge.
curl -X POST https://docweave.dev/api/v1/pdf \
-H "Authorization: Bearer dw_live_your_key" \
-H "Content-Type: application/json" \
-H "Accept: application/pdf" \
-d '{
"source": {
"type": "html",
"html": "<h1>Invoice #4471</h1><p>Total: $240.00</p>"
},
"idempotencyKey": "invoice-4471-2026-07-14"
}' \
--output invoice-4471.pdfRun that command twice in a row and you get the same PDF bytes back both times, with only one entry in your usage history. This is the same pattern Stripe and most payment APIs use for Idempotency-Key headers, applied to document generation — see the HTML-to-PDF API glossary entry if you want the broader category context.
Doing the same thing from Node
const res = await fetch("https://docweave.dev/api/v1/pdf", {
method: "POST",
headers: {
Authorization: "Bearer dw_live_your_key",
"Content-Type": "application/json",
Accept: "application/pdf",
},
body: JSON.stringify({
source: {
type: "template",
templateId: "invoice-v2",
data: { invoiceNumber: "4471", total: 240.0 },
},
idempotencyKey: `invoice-4471-${new Date().toISOString().slice(0, 10)}`,
}),
});
if (!res.ok) throw new Error(`PDF generation failed: ${res.status}`);
const buffer = Buffer.from(await res.arrayBuffer());
await fs.promises.writeFile("invoice-4471.pdf", buffer);More framework-specific setups (retry wrappers, queues, error handling) are in Generate PDF in Node.js.
Choosing a good idempotency key
The key is only useful if it identifies the document, not the moment you happened to call the API. Three rules cover almost every case:
- Derive it from your own domain data, not from Date.now() or a random UUID generated fresh on every attempt. A retry that generates a new random key defeats the whole point — use the invoice number, order ID, contract version, or report period instead.
- Never reuse a key across two different documents. If you send the same key with a different payload, you'll get back the original cached PDF, not a re-render — Docweave doesn't diff the payload, it trusts the key. Encode anything that changes the output (a version number, an edited-at timestamp) into the key itself if the same logical document can legitimately be regenerated with new content.
- Scope it to what actually needs deduplicating. One key per invoice, not one key per API call in a retry loop — the whole point is that the retry loop reuses the same key every time it fires for that invoice.
A practical convention: {document-type}-{stable-id}-{version-or-date}, e.g. receipt-ord_9f21-v1 or contract-acme-2026-07. That's specific enough to avoid collisions across unrelated documents but stable across retries of the same one.
The webhook case, specifically
This is where idempotency keys earn their keep the most. Webhook providers guarantee at-least-once delivery, not exactly-once — if your handler is slow, times out, or returns a non-2xx status for any reason, you should expect the same event to arrive again, sometimes minutes later. If your handler generates a PDF as a side effect (a paid-order receipt, a signed-contract copy), use the webhook's own event ID as the idempotency key:
app.post("/webhooks/order-paid", async (req, res) => {
const event = req.body;
await fetch("https://docweave.dev/api/v1/pdf", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.DOCWEAVE_API_KEY}`,
"Content-Type": "application/json",
Accept: "application/pdf",
},
body: JSON.stringify({
source: { type: "template", templateId: "receipt", data: event.data },
idempotencyKey: `order-receipt-${event.id}`,
}),
});
res.sendStatus(200);
});Because the key is tied to the immutable event ID rather than the wall-clock time the handler ran, a re-delivered webhook safely resolves to the same stored PDF no matter how many times it fires. This pattern shows up constantly in receipts and invoices generated off payment or order events, and it's worth wiring up before you connect a no-code tool like Zapier or Make, both of which retry failed steps by default.
Idempotency keys in the MCP server
idempotencyKey is a plain field on the same generate_pdf call the open-source MCP server exposes, so agent-driven generation gets the same protection without extra work — relevant if you're calling Docweave from an AI agent loop that might itself retry a failed tool call (see PDF generation for AI agents).
await generate_pdf({
source: {
type: "template",
templateId: "invoice-v2",
data: { invoiceNumber: "4471", total: 240.0 },
},
outputPath: "./invoice-4471.pdf",
idempotencyKey: "invoice-4471-2026-07-14",
});If you're not sure whether a caller might retry, add the key anyway. A key that never collides costs nothing; a missing key on a system that does retry costs you double-billed documents in production.
One honest caveat: idempotency keys dedupe requests to Docweave, they don't dedupe your own application logic upstream. If your code calls the render endpoint twice with two different keys for what a human would call "the same invoice," you'll still get two PDFs and two charges — the key only helps once you've decided on a stable identifier for the document. See the general docs for the full request/response shape, including how idempotency keys interact with error responses (a failed render isn't cached, so retrying after a genuine failure will re-render, not stay stuck).
FAQ
How long does Docweave remember an idempotency key?
Stored results are retained for 24 hours from the original request. Reusing the same key within that window returns the original PDF; after it expires, the same key will trigger a fresh render (and a new usage event) if sent again.
What happens if I send the same idempotencyKey with a different source or template data?
You get back the PDF from the original request, not a new render of the new payload. Docweave keys the cache off the idempotencyKey string alone and doesn't diff the request body, so treat the key as a promise that the document is unchanged — don't reuse it across genuinely different content.
Do I get billed again for a duplicate request with the same key?
No. A repeated idempotencyKey within the retention window short-circuits before rendering and doesn't create a new usage_event, so it isn't billed as a second document.
Is idempotencyKey required on every request?
No, it's optional. Skip it for one-off or interactive generations where duplicates don't matter (a preview click in a UI, for example). Add it anywhere a request might be retried automatically: webhook handlers, queue workers, background jobs, or any client with retry-on-timeout logic.
Related
Generate your first PDF in minutes.
Get an API key