Blog/Guides

Generate PDFs Asynchronously with Jobs and Webhooks

Submit an async document job, persist its ID, verify signed notifications, and save the completed file.

Guides··4 min read

An async PDF request returns a job ID before the document is ready. Your application stores that ID, checks the job's status, and downloads the completed artifact. A signed webhook can notify the application sooner, but the job record remains the place to confirm the outcome.

Use this pattern for document operations that may outlast a browser request or serverless timeout. For a complete resumable batch application, see bulk PDF generation.

Submit the same operation with an async response

Generating endpoints accept response: "binary", "url", or "async". Async is a response mode on the document operation, not a different rendering API.

This complete Node script reads an HTML file and saves the resulting PDF. It uses the official SDK:

npm install @relaypdf/sdk
npx @relaypdf/cli setup --env .env

Load the environment written by the CLI after approving the browser login. Save the following as generate.mjs and run node generate.mjs invoice.html invoice.pdf:

import { readFile } from "node:fs/promises";
import { RelayPDF } from "@relaypdf/sdk";

const client = new RelayPDF({ apiKey: process.env.RELAYPDF_API_KEY });
const inputPath = process.argv[2];
const outputPath = process.argv[3];
if (!inputPath || !outputPath) {
  throw new Error("Usage: node generate.mjs input.html output.pdf");
}
const html = await readFile(inputPath, "utf8");
const job = await client.pdf.fromHtml(html, {
  filename: "document.pdf",
  response: "async",
});
if (job.kind !== "async") throw new Error("Expected an async job");
console.log(`Accepted job ${job.id}`);
const completed = await client.jobs.wait(job.id, {
  intervalMs: 3000,
  timeoutMs: 180000,
});
const file = await client.files.download(completed.id);
await file.save(outputPath);

This script demonstrates the response lifecycle. It does not persist state across process restarts. In an application, commit the job ID to a database immediately after acceptance. A local timeout does not mean the service canceled or failed the job.

Poll the job record

GET /v1/jobs/:id requires account authentication. Processing jobs are still in progress; completed jobs expose the result; failed jobs need error handling. Keep the ID when polling times out so a worker can resume without submitting the document again.

Choose a polling interval appropriate to the operation and account rate limits. Hundreds of clients polling every second can create more traffic than the generation requests themselves. Honor rate-limit responses and avoid running several independent pollers for one document.

The async reference describes the response fields. The Node SDK reference documents jobs.get, jobs.wait, and file download behavior.

Verify signed webhook bytes before acting

An account webhook endpoint is configured through the dashboard or webhook API. Keep its signing secret separately from the API key. The signature header is RelayPDF-Signature; the SDK verifier accepts the secret, original request body, and that header.

A verifier helper for a Fetch-compatible server is:

import { verifyWebhook } from "@relaypdf/sdk";

export async function readVerifiedEvent(request) {
  const rawBody = await request.text();
  const signature = request.headers.get("RelayPDF-Signature") ?? "";
  const valid = await verifyWebhook(
    process.env.RELAYPDF_WEBHOOK_SECRET,
    rawBody,
    signature,
  );
  if (!valid) return null;
  return JSON.parse(rawBody);
}

Read the raw body before a framework parses JSON. Parsing and reserializing can change whitespace or key ordering, producing different signed bytes. Use the SDK's timestamp tolerance rather than accepting indefinitely replayable signatures.

After verification, commit the event to a durable inbox before returning a successful acknowledgment. Make downstream state transitions idempotent, because an event can arrive more than once. A received event should prompt a state check rather than blindly trigger another PDF generation.

The downloadable batch project includes a Python inbox with HMAC, timestamp, and tampering tests. Its polling worker can recover even when no webhook is received.

Persist the output, not only the temporary URL

Completed file URLs expire after the documented retention period. Download the artifact into your own durable storage before marking the application's work item complete. A job ID or temporary URL is not a permanent document archive.

Store the output's storage key and any useful integrity metadata with the business record. If a download fails, retain the job ID and retry the download while the artifact remains available. Do not automatically regenerate a document simply because a temporary URL expired.

Retry deliberately

Use stable idempotency keys with supported URL or async requests when retrying an ambiguous submission. The same key must represent the same request. A changed template or changed invoice data needs a new document version and request identity.

The basic SDK script above has no durable retry policy. The batch tutorial supplies one with bounded attempts, backoff, submission pacing, and a local queue. See the idempotency reference for the server's retention and conflict rules.

Failed operations are not billed. An application timeout, a failed download, or a failed webhook delivery is not necessarily a failed document operation; check the authoritative job result. Rates and spending controls are documented in billing.

For the underlying inputs, use HTML-to-PDF. For acceptance checks after downloading, use testing PDF generation in CI.

Ready to generate?

One API for HTML, Markdown, URLs, and Office. REST, SDK, CLI, or MCP.