API Signed render events · 10 min read

Webhooks

Webhooks notify your application when a render succeeds or fails. Use dashboard endpoints for account-level notifications, and per-request webhooks when one asynchronous render should notify a specific endpoint.

Delivery modes

Mode Configured in Best for
Dashboard endpoints BladePDF dashboard Central event handling for all renders in your account.
Template render events Dashboard endpoint Routing automation by the template fields in the payload.
Per-request webhook Laravel render call One-off completion callbacks for a single async render.

Events

Event When it fires
pdf.rendered A render completed successfully.
pdf.failed A render failed validation, timed out, hit a limit, or failed during rendering.

Dashboard endpoints

Create a webhook endpoint in the dashboard when your application should receive render events consistently. Choose the events to subscribe to, copy the signing secret when the endpoint is created, and monitor delivery attempts from the delivery log.

  1. Open Webhooks in the BladePDF dashboard.
  2. Add an endpoint URL, for example https://example.com/webhooks/bladepdf.
  3. Select pdf.rendered, pdf.failed, or both.
  4. Store the generated signing secret in your application environment.
  5. Use the test action to confirm that your endpoint accepts signed events.

Dashboard endpoints receive events for local view renders, raw HTML renders, and dashboard template renders.

Template render events

A dashboard template render emits the same pdf.rendered and pdf.failed events as any other render. The payload identifies the template so your endpoint can route the event to the right workflow.

Route template events
php
 1if (($payload['type'] ?? null) === 'pdf.rendered'
 2    && data_get($payload, 'render.template_name') === 'invoice.standard') {
 3    SyncInvoicePdf::dispatch($payload);
 4}

For local view and raw HTML renders, render.template_id is null. If you call templateName(), that value appears as render.template_name for dashboard filtering and webhook routing.

Per-request webhooks

Use webhook() when a single async render should notify an endpoint in addition to any dashboard-configured endpoints. The URL, secret, and event list are sent with that render request only. Async renders must also call storePdf().

Template render callback
php
 1$submission = BladePDF::fromTemplate('invoice.standard', $context)
 2    ->reference('INV-2026-0042')
 3    ->storePdf()
 4    ->webhook('https://example.com/bladepdf/webhook', 'whsec_request_secret')
 5    ->async();

Limit a per-request webhook to specific events by passing the third argument:

Only successful renders
php
 1BladePDF::fromHtml($html)
 2    ->storePdf()
 3    ->webhook('https://example.com/bladepdf/webhook', 'whsec_request_secret', [
 4        'pdf.rendered',
 5    ])
 6    ->async();
Per-request webhooks use the same delivery system

They are signed, queued, retried, and shown in the dashboard delivery log just like dashboard endpoints. They do not create a reusable dashboard endpoint. See Async Renders for submission and failure semantics.

Payload shape

Webhook payloads are JSON. Every delivery has a unique event id and a nested render summary.

pdf.rendered payload
json
 1{
 2  "id": "0190f7a8-8f7d-7c7b-9b08-2f9878d9c3b2",
 3  "type": "pdf.rendered",
 4  "created_at": "2026-07-01T12:34:56+00:00",
 5  "workspace_id": "0190f78c-93ff-72c4-bb30-9e049e2d7a70",
 6  "render": {
 7    "request_id": "req_abc123",
 8    "status": "success",
 9    "template_id": "0190f790-2bc1-7b2f-9f9c-9dba36bfe2a4",
10    "template_name": "invoice.standard",
11    "render_ms": 420,
12    "queue_ms": 18,
13    "total_duration_ms": 491,
14    "pdf_bytes": 84213,
15    "pdf_url": "https://app.bladepdf.com/render-pdfs/...",
16    "error_message": null,
17    "occurred_at": "2026-07-01T12:34:56+00:00"
18  }
19}
Field Notes
render.request_idStable id for the render request.
render.template_idDashboard template record id for template renders, otherwise null.
render.template_namePublic template identifier such as invoice.standard, or the custom templateName() value for HTML renders.
render.pdf_urlSigned download URL when the generated PDF was stored, otherwise null.
render.error_messagePresent on pdf.failed events.

Delivery headers

Header Description
BladePDF-EventThe event type, such as pdf.rendered.
BladePDF-DeliveryThe delivery id.
BladePDF-TimestampUnix timestamp used in the signature.
BladePDF-SignatureHMAC SHA-256 signature in the form v1=....
Content-Typeapplication/json.

Verify signatures

The signature is computed as HMAC_SHA256(timestamp + "." + rawBody, endpointSecret). The BladePDF Laravel package verifies the signature, raw request body, and timestamp for you. Always run this check before decoding or processing the event.

.env
env
 1BLADEPDF_WEBHOOK_SECRET=whsec_...
routes/api.php
php
 1use BladePDF\Laravel\Webhooks\SignatureValidator;
 2use Illuminate\Http\Request;
 3use Illuminate\Support\Facades\Route;
 4
 5Route::post('/webhooks/bladepdf', function (Request $request) {
 6    abort_unless(SignatureValidator::isValid($request), 401);
 7
 8    $payload = $request->json()->all();
 9
10    match ($payload['type'] ?? null) {
11        'pdf.rendered' => ProcessRenderedPdf::dispatch($payload),
12        'pdf.failed' => ProcessFailedPdf::dispatch($payload),
13        default => null,
14    };
15
16    return response()->noContent();
17});

Plain PHP verifier

The Laravel wrapper only extracts the Illuminate request and configuration. Other PHP applications call the core verifier with the exact header values and raw body:

Framework-agnostic PHP
php
 1use BladePDF\Webhooks\SignatureVerifier;
 2
 3$valid = SignatureVerifier::isValid(
 4    rawBody: $rawBody,
 5    timestamp: $headers['BladePDF-Timestamp'] ?? null,
 6    signature: $headers['BladePDF-Signature'] ?? null,
 7    secret: $webhookSecret,
 8);

Node.js verifier

Pass the exact raw Buffer or Uint8Array to verifyWebhookSignature() before parsing JSON:

Fastify with raw body
typescript
 1import rawBody from 'fastify-raw-body';
 2import { verifyWebhookSignature } from '@bladepdf/node';
 3
 4await fastify.register(rawBody, {
 5  field: 'rawBody',
 6  global: false,
 7  encoding: false,
 8});
 9
10fastify.post('/webhooks/bladepdf', {
11  config: { rawBody: true },
12}, async (request, reply) => {
13  const valid = verifyWebhookSignature({
14    rawBody: request.rawBody,
15    timestamp: request.headers['bladepdf-timestamp'],
16    signature: request.headers['bladepdf-signature'],
17    secret: webhookSecret,
18  });
19
20  if (!valid) return reply.code(401).send();
21
22  const payload = JSON.parse(request.rawBody.toString('utf8'));
23  await queueWebhook(payload);
24  return reply.code(204).send();
25});
Use the raw request body

Do not re-encode JSON before verifying. Signature verification must use the exact raw body bytes received by your endpoint.

The default timestamp tolerance is 300 seconds in either direction. This prevents an intercepted, correctly signed request from being replayed later. If your infrastructure needs a different window, set BLADEPDF_WEBHOOK_TOLERANCE to a number of seconds.

Per-request signing secret

When a per-request webhook uses a different secret from BLADEPDF_WEBHOOK_SECRET, pass that secret as the second argument. Use the same value when submitting the render and verifying its callback.

Explicit per-request secret
php
 1$secret = config('services.bladepdf.request_webhook_secret');
 2
 3abort_unless(SignatureValidator::isValid($request, $secret), 401);

Signing secrets

Dashboard endpoints get a generated secret when the endpoint is created or rotated. Per-request webhooks use the secret you pass to webhook(). Store secrets in environment variables and rotate them if they are exposed.

Retries

BladePDF retries failed deliveries up to five attempts. Retry delays are approximately 1 minute, 5 minutes, 15 minutes, 1 hour, and 6 hours. A dashboard endpoint is marked failing after repeated failures.

Delivery log retention

Delivery attempts (status, response code, latency, response snippet) are visible in the dashboard delivery log for 90 days and are deleted automatically afterwards. If you need a longer delivery audit trail, record webhook receipts on your side using the payload id.

Idempotency

Webhooks may be delivered more than once. Store the payload id or BladePDF-Delivery header and ignore duplicates.

Endpoint rules

  • Use an https:// URL in production.
  • Return any 2xx response after successfully accepting the event.
  • Process expensive work asynchronously in your own queue.
  • Keep endpoint secrets private.
  • Do not make rendering success depend on the webhook endpoint being online; webhook delivery happens after the render event is recorded.