To run a long PDF job without holding the HTTP connection, set response to async on any generating RelayPDF endpoint. The API returns 202 with a job id and pollUrl. Poll GET /v1/jobs/:id until status is completed or failed, or attach a pdf api webhook: optional callbackUrl on that request, or an account endpoint in the dashboard. Both POSTs carry HMAC RelayPDF-Signature. Failed jobs are never billed. Reference: https://relaypdf.com/docs/async and https://relaypdf.com/docs/webhooks. REST hub: https://relaypdf.com/docs. Sibling queue recipe: https://relaypdf.com/blog/laravel-html-to-pdf.
Use async when the work may outlast a typical request timeout: LibreOffice convert, large merges, raster, or an agent that should not block. Default response is binary (file bytes). response url returns JSON with a 24-hour public download. async is the third mode on the same POST body. There is no separate create-async path.
Submit with response async
Every generating endpoint accepts response: binary | url | async. Host is https://api.relaypdf.com. Authorization is Bearer with a key that starts pdf_live_…. Do not put the key on a query string. GET /health and GET /v1/files/:id are the documented exceptions that do not send a key.
The async docs show a convert-style body. sourceFilename names the Office bytes. to is the target. response is the string async. Optional callbackUrl is an HTTPS URL that receives that job’s completion or failure payload. A non-https callbackUrl is url_not_allowed.
curl -X POST https://api.relaypdf.com/v1/convert
-H "Authorization: Bearer $RELAYPDF_API_KEY"
-H "Content-Type: application/json"
-d '{
"file": "UEsDB...",
"sourceFilename": "deck.pptx",
"to": "pdf",
"response": "async",
"callbackUrl": "https://example.com/hooks/relaypdf"
}'
HTML, Markdown, URL, and published templates use POST /v1/pdf with the same response field. Node: result.kind === "async". Python: AsyncResult. The 202 body is id, status processing, and pollUrl pointing at https://api.relaypdf.com/v1/jobs/job_… .
{
"id": "job_...",
"status": "processing",
"pollUrl": "https://api.relaypdf.com/v1/jobs/job_..."
}
Poll GET /v1/jobs/:id
Poll with the same API key. status is processing, completed, or failed. Unknown ids return not_found. SDKs: client.jobs.get(id) for one read; client.jobs.wait(id) until a terminal status. Node wait options documented on the SDK README: intervalMs and timeoutMs. Python wait defaults on PyPI: interval_ms 1000, timeout_ms 120000. CLI: relaypdf jobs get|wait.
A completed job includes a temporary download URL. GET /v1/files/:id does not require a key and is good for 24 hours. SDKs: client.files.download(id). That is not permanent storage. Copy the bytes to your bucket if you need them later. A 404 on the file path means missing or expired.
import { RelayPDF } from "@relaypdf/sdk";
const client = new RelayPDF({ apiKey: process.env.RELAYPDF_API_KEY });
const job = await client.convert.fromPath("./deck.pptx", {
to: "pdf",
response: "async",
});
if (job.kind === "async") {
const done = await client.jobs.wait(job.id, {
intervalMs: 1500,
timeoutMs: 120_000,
});
const file = await client.files.download(done.id);
await file.save("deck.pdf");
}
callbackUrl versus dashboard webhooks
Two delivery paths, both HMAC-signed. callbackUrl is optional on a single generating request and is POSTed when that job finishes. Dashboard endpoints are configured at /dashboard/webhooks (REST: GET/POST/DELETE /v1/webhooks). Dashboard events include job.completed, job.failed, and the wallet events listed below. Use callbackUrl when one caller needs one result. Use the dashboard list when several services should see job and wallet traffic.
OpenAPI: POST /v1/webhooks requires url; events is an optional string array. The create response returns the signing secret once. List responses omit raw secrets. Delete is DELETE /v1/webhooks/:id. The webhook signing secret is not the API key. Store it as RELAYPDF_WEBHOOK_SECRET.
| Name | Where it is documented |
|---|---|
| callbackUrl | Async page; OpenAPI on generating bodies; https only |
| RelayPDF-Signature | Webhooks page and both official SDKs |
| RelayPDF-Event | Webhooks page, dashboard path |
| t=,v1= | Webhooks page; HMAC-SHA256 of {t}.{raw_body} |
| job.completed | Webhooks events list |
| job.failed | Webhooks events list |
| wallet.topup | Webhooks events list |
| wallet.auto_reload | Webhooks events list |
| wallet.auto_reload_failed | Webhooks events list |
| wallet.payment_required | Webhooks events list |
Verify RelayPDF-Signature
Both POSTs are HMAC-signed with RelayPDF-Signature. The documented header format is t=,v1=. The signed payload is HMAC-SHA256 of {t}.{raw_body} (Node README also writes {timestamp}.{raw_body}; same bytes). Verify against the exact raw body. Do not parse JSON and re-serialize. Default clock skew on the official verifiers is 300 seconds. Reject a bad signature before you act on the event.
Node exports verifyWebhook, WEBHOOK_SIGNATURE_HEADER, and WEBHOOK_EVENT_HEADER. Python exports verify_webhook and the same two header constants. Secret is the dashboard webhook secret.
import { verifyWebhook } from "@relaypdf/sdk";
const ok = await verifyWebhook(
process.env.RELAYPDF_WEBHOOK_SECRET,
rawBody,
request.headers.get("RelayPDF-Signature") ?? "",
);
from relaypdf import verify_webhook
ok = verify_webhook(
os.environ["RELAYPDF_WEBHOOK_SECRET"],
raw_body,
signature_header,
)
On a dashboard delivery, read RelayPDF-Event after the signature check. Documented values are job.completed, job.failed, wallet.topup, wallet.auto_reload, wallet.auto_reload_failed, and wallet.payment_required. Do not invent X-RelayPDF-Signature, BladePDF-Signature, or a sha256= prefix. Those names are not in the RelayPDF docs.
Failed jobs are unbilled
Errors are structured { error: { code, message } }. SDKs throw RelayPDFError with status, code, message, and optional retryAfter / retry_after. Branch on code. The errors page and the hub both state that failed operations, 429s, and 402s are never billed. Only a successful generation or conversion debits the wallet. The Node README is more specific for async: only HTTP 200 or a completed async job debits.
Codes you will see around this path: invalid_request (400) for a bad body; url_not_allowed (400) for private, loopback, or metadata hosts, or a non-https callbackUrl; unauthorized (401); payment_required (402) when the wallet is empty; rate_limited (429) with Retry-After; not_found (404) for an unknown job or expired file; payload_too_large (413); render_failed and processing_failed (502); convert_unavailable, ai_unavailable, storage_unavailable (503); internal_error (500). Those failure codes are unbilled.
Trial wallets: 20 requests/minute. Funded or auto-reload: 60/minute. Burst: 5 / 10 seconds. New accounts start with $5.00 trial (5000 millicents). Launch rates on the SDK README: HTML/URL/Markdown/template PDF $0.015; screenshot $0.015; LibreOffice convert $0.04; wkhtml $0.025; tools $0.005. Confirm live numbers at https://relaypdf.com/pricing.
What not to do
- Do not hold a serverless request open for Office convert. Use response async and poll or a webhook.
- Do not invent a second async endpoint. Same POST, field response.
- Do not verify HMAC on re-serialized JSON. Use the raw body.
- Do not treat GET /v1/files/:id as a CMS. Twenty-four hours, then 404.
- Do not assume a failed job charged the wallet. Documented policy: it did not.
- Do not send callbackUrl over http. The API rejects it as url_not_allowed.
Async jobs: https://relaypdf.com/docs/async. Webhooks: https://relaypdf.com/docs/webhooks. Errors: https://relaypdf.com/docs/errors. Node SDK: https://relaypdf.com/docs/sdks/node. Python SDK: https://relaypdf.com/docs/sdks/python. OpenAPI: https://relaypdf.com/openapi.json. Product: https://relaypdf.com/html-to-pdf.