Skip to content

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.

  • An API key with webhook:manage to register the endpoint, and render:read for 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. localhost and private network addresses are refused; see allowed destinations.
  • PDF_URL and API_KEY set in your terminal as in the quickstart.

Tell Typefold where to send events and which events you want:

Terminal window
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 secret is shown only once, so store it with your application straight away. In Studio, only owners and admins see Manage → Webhooks.

Check every request’s signature with your secret before you trust it; see signatures.

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.

Events carry no PDF or download link. Read the render named in the event’s data.renderId, and download the PDF when it succeeded:

Terminal window
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.

A failed render has an error instead; see Render a PDF.

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.

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.
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.

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-timestamp is 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.

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.

Terminal window
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.

  • A delivery succeeds when your endpoint answers any 2xx status 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 id you 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 failed and not sent again.

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"
}
]
}
  • id is the webhook-id header; eventId is the event’s id.
  • status is pending (more attempts to come), succeeded or failed.
  • nextAttemptAt is set only while pending.
  • 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.

Webhook URLs must point at the public internet. A URL is refused when:

  • its scheme is not http or https, or it is http on the hosted service;
  • it contains credentials, such as https://user:pass@example.com;
  • its host is localhost or 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.

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 failed at 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.