Get notified with webhooks
Typefold sends a signed POST request to your URL each time a PDF render ends, so you don’t
have to poll the Renders API for its state.
Before you start
Section titled “Before you start”- An API key with
webhook:manageto register the endpoint, andrender:readfor your application to fetch renders. - A URL on the public internet in your application that will receive the events, such as
https://example.com/hooks/pdf.localhostand private network addresses are refused; see allowed destinations. PDF_URLandAPI_KEYset in your terminal as in the quickstart.
1. Register an endpoint
Section titled “1. Register an endpoint”Tell Typefold where to send events and which events you want:
curl -X POST "$PDF_URL/v1/webhook-endpoints" \ -H "x-api-key: $API_KEY" \ -H "content-type: application/json" \ -d '{ "url": "https://example.com/hooks/pdf", "eventTypes": ["render.succeeded", "render.failed"] }'The response holds the endpoint’s id and its signing secret.
The client cannot register endpoints. In Studio, open Manage → Webhooks, click Add endpoint, enter the Endpoint URL and pick the Events. Studio then shows the signing secret.
The client cannot register endpoints. In Studio, open Manage → Webhooks, click Add endpoint, enter the Endpoint URL and pick the Events. Studio then shows the signing secret.
The client cannot register endpoints. In Studio, open Manage → Webhooks, click Add endpoint, enter the Endpoint URL and pick the Events. Studio then shows the signing secret.
The client cannot register endpoints. In Studio, open Manage → Webhooks, click Add endpoint, enter the Endpoint URL and pick the Events. Studio then shows the signing secret.
The secret is shown only once, so store it with your application straight away. In Studio, only owners and admins see Manage → Webhooks.
2. Verify each request
Section titled “2. Verify each request”Check every request’s signature with your secret before you trust it; see signatures.
3. Store the event, then answer 2xx
Section titled “3. Store the event, then answer 2xx”Store or enqueue the event first, then answer with any 2xx status within 10 seconds. Do slower
work, such as fetching the render, after you have answered. Once Typefold gets a 2xx, it
does not retry that delivery. Any other answer, or none, makes it send the delivery again; see
delivery rules and retries.
4. Fetch the render
Section titled “4. Fetch the render”Events carry no PDF or download link. Read the render named in the event’s data.renderId, and
download the PDF when it succeeded:
curl "$PDF_URL/v1/renders/b1f3c1d2-7a55-4d0e-9e59-2f3f5c0a9d11" \ -H "x-api-key: $API_KEY"Download the PDF from result.downloadUrl; see Render a PDF.
using Typefold.Client;
var client = new TypefoldClient( new Uri(Environment.GetEnvironmentVariable("PDF_URL")!), Environment.GetEnvironmentVariable("API_KEY")!);
// The event's data.renderId.Render render = await client.GetRenderAsync("b1f3c1d2-7a55-4d0e-9e59-2f3f5c0a9d11");
if (render.State == RenderState.Succeeded){ byte[] pdf = await client.DownloadAsync(render); await File.WriteAllBytesAsync("document.pdf", pdf);}import { writeFile } from "node:fs/promises";import { TypefoldClient, RenderState } from "@typefold/client";
const client = new TypefoldClient({ baseUrl: process.env.PDF_URL!, apiKey: process.env.API_KEY!,});
// The event's data.renderId.const render = await client.getRender("b1f3c1d2-7a55-4d0e-9e59-2f3f5c0a9d11");
if (render.state === RenderState.Succeeded) { await writeFile("document.pdf", await client.download(render));}import { writeFile } from "node:fs/promises";import { TypefoldClient, RenderState } from "@typefold/client";
const client = new TypefoldClient({ baseUrl: process.env.PDF_URL, apiKey: process.env.API_KEY,});
// The event's data.renderId.const render = await client.getRender("b1f3c1d2-7a55-4d0e-9e59-2f3f5c0a9d11");
if (render.state === RenderState.Succeeded) { await writeFile("document.pdf", await client.download(render));}import osfrom pathlib import Path
from typefold import TypefoldClient, RenderState
client = TypefoldClient(os.environ["PDF_URL"], os.environ["API_KEY"])
# The event's data.renderId.render = client.get_render("b1f3c1d2-7a55-4d0e-9e59-2f3f5c0a9d11")
if render.state == RenderState.SUCCEEDED: Path("document.pdf").write_bytes(client.download(render))A failed render has an error instead; see Render a PDF.
Events
Section titled “Events”| Event | Sent when |
|---|---|
render.succeeded |
The PDF was stored and can be downloaded. |
render.failed |
The render failed, after any automatic retries. |
render.cancelled |
The render was cancelled before it finished. |
- Every PDF render sends one event when it ends, however it was created:
POST /v1/renders, a batch item, a Reports API PDF or a PDF preview. Excel output and string rendering send none. - An endpoint receives only the event types it subscribed to, and only for renders that end after it was created.
- A disabled endpoint misses the events of renders that end while it is disabled. They are not sent later.
- Events usually go out within a few seconds.
Event payload
Section titled “Event payload”Each delivery is a POST with this JSON body:
{ "id": "5e0c8f5e-4a4c-4f55-9a8a-7f1f9b3f1a2c", "type": "render.failed", "createdAt": "2026-10-08T09:15:30.123Z", "data": { "renderId": "b1f3c1d2-7a55-4d0e-9e59-2f3f5c0a9d11", "status": "failed", "test": false, "errorCode": "LIMIT_EXCEEDED" }}| Field | Description |
|---|---|
id |
The event’s ID. Every endpoint that receives this event sees the same ID. |
type |
render.succeeded, render.failed or render.cancelled. |
createdAt |
When the render reached its final state (ISO 8601, UTC). |
data.renderId |
The render’s ID. Fetch it with GET /v1/renders/{id}. |
data.status |
succeeded, failed or cancelled. |
data.test |
true for a test render, else false. |
data.errorCode |
Only on render.failed: the error’s code, as on the render’s error. |
Request headers
Section titled “Request headers”| Header | Value |
|---|---|
webhook-id |
The delivery’s ID. It stays the same on every retry of that delivery. |
webhook-timestamp |
When this attempt was sent, in Unix seconds. |
webhook-signature |
One or more space-separated v1,<base64> signatures, one per active secret. |
content-type |
application/json |
user-agent |
typefold-webhooks/1 |
webhook-id identifies one delivery to one endpoint. The body’s id identifies the event and is
the same for every endpoint, so use it to skip duplicates.
Signatures
Section titled “Signatures”Deliveries are signed as Standard Webhooks specifies, so a
Standard Webhooks library for your language can verify them with the endpoint’s signing secret.
The secret looks like whsec_<base64>.
- Reject a request whose signature does not match.
- Reject a request whose
webhook-timestampis more than five minutes away from your clock, earlier or later, so a captured request cannot be replayed. Every retry is signed again with a fresh timestamp.
Rotate the secret
Section titled “Rotate the secret”If the secret may have leaked, or you lost it, rotate it. It cannot be read again. Update your application with the new secret within 24 hours: until then, deliveries carry two signatures, one from each secret. After that only the new secret signs.
curl -X POST "$PDF_URL/v1/webhook-endpoints/7c9e6679-7425-40de-944b-e07fc1f90ae7/rotate-secret" \ -H "x-api-key: $API_KEY"The response holds the new secret.
The client cannot rotate secrets. In Studio, open Manage → Webhooks, select the endpoint and click Rotate secret. Studio then shows the new secret.
The client cannot rotate secrets. In Studio, open Manage → Webhooks, select the endpoint and click Rotate secret. Studio then shows the new secret.
The client cannot rotate secrets. In Studio, open Manage → Webhooks, select the endpoint and click Rotate secret. Studio then shows the new secret.
The client cannot rotate secrets. In Studio, open Manage → Webhooks, select the endpoint and click Rotate secret. Studio then shows the new secret.
Delivery rules and retries
Section titled “Delivery rules and retries”- A delivery succeeds when your endpoint answers any
2xxstatus within 10 seconds. The response body is ignored. - Anything else fails and is retried: another status, a timeout, a refused connection or a redirect. Redirects are not followed.
- The same event can arrive more than once, for example when your answer is lost. Skip events
whose
idyou have already handled. - Deliveries can arrive out of order. Act on the render’s current
state, not on event order. - A failed attempt is retried after 30 seconds, then 2, 5, 15 and 30 minutes, then 1, 2, 4, 8 and
8 hours, each wait counted from the previous attempt. If the 11th attempt fails, the delivery is
marked
failedand not sent again.
Delivery history
Section titled “Delivery history”GET /v1/webhook-endpoints/{id}/deliveries lists an endpoint’s deliveries, newest first, in
pages like other list endpoints. Studio does not show deliveries.
{ "items": [ { "id": "c2a5f1b0-3d4e-4f60-8a7b-9c0d1e2f3a4b", "eventId": "5e0c8f5e-4a4c-4f55-9a8a-7f1f9b3f1a2c", "eventType": "render.failed", "renderId": "b1f3c1d2-7a55-4d0e-9e59-2f3f5c0a9d11", "status": "pending", "attempts": 2, "lastStatusCode": 503, "lastError": "The endpoint answered HTTP 503.", "lastAttemptAt": "2026-10-08T09:16:01.512Z", "nextAttemptAt": "2026-10-08T09:18:01.512Z", "createdAt": "2026-10-08T09:15:31.004Z" } ]}idis thewebhook-idheader;eventIdis the event’sid.statusispending(more attempts to come),succeededorfailed.nextAttemptAtis set only whilepending.- Delivery records are kept as long as renders, 30 days by default.
lastError says why the last attempt failed:
lastError |
Meaning |
|---|---|
The endpoint answered HTTP <status>. |
Your endpoint answered with a status other than 2xx. |
The endpoint did not respond in time. |
No answer within 10 seconds. |
The request failed (<code>). |
The connection failed, such as ECONNREFUSED. |
The endpoint is disabled. |
The endpoint was disabled while the delivery was waiting for a retry. |
The endpoint's signing secret cannot be decrypted. |
Self-hosted only: the server can no longer read the secret. Rotate it. |
| A destination message | The URL is no longer an allowed destination. |
Allowed destinations
Section titled “Allowed destinations”Webhook URLs must point at the public internet. A URL is refused when:
- its scheme is not
httporhttps, or it ishttpon the hosted service; - it contains credentials, such as
https://user:pass@example.com; - its host is
localhostor ends in.localhost; - it is, or its host name resolves to, a private, loopback, link-local, shared, reserved,
documentation or multicast address (IPv4 or IPv6, including cloud metadata addresses such as
169.254.169.254); - its host name does not resolve.
Creating an endpoint or changing its URL to a refused URL answers 400 BAD_REQUEST, with the
reason in details. The check runs again on every delivery, so a host name that later points at
a private address fails that delivery, which is then retried.
Self-hosted installations also accept http, and can allow private addresses; see
configuration.
Manage endpoints
Section titled “Manage endpoints”In Studio, open Manage → Webhooks, select endpoints and click Enable, Disable, Rotate secret or Delete.
- A disabled endpoint receives no new events. Deliveries waiting for a retry are marked
failedat their next attempt. - Deleting an endpoint discards its pending deliveries and its delivery history.
- A workspace can have up to 10 endpoints. An eleventh answers
409 CONFLICT.
With the API, every operation needs the webhook:manage scope. Request and response fields are
in the API reference.
| Operation | What it does |
|---|---|
POST /v1/webhook-endpoints |
Create an endpoint. Answers 201 Created with the secret. |
GET /v1/webhook-endpoints |
List all endpoints, without secrets. |
GET /v1/webhook-endpoints/{id} |
Read one endpoint, without its secret. |
PATCH /v1/webhook-endpoints/{id} |
Change only the fields you send: url, description, eventTypes or enabled. "description": null removes the description. |
DELETE /v1/webhook-endpoints/{id} |
Delete the endpoint and its delivery history. |
POST /v1/webhook-endpoints/{id}/rotate-secret |
Issue a new signing secret. Returns the endpoint and new secret. |
GET /v1/webhook-endpoints/{id}/deliveries |
List recent deliveries; see delivery history. |