Pulp EngineDocument Rendering
Get started

Outbound event webhooks

PostgreSQL and SQL Server installations can register tenant subscriptions at /webhooks. All management and delivery-log operations require admin scope. File storage returns 503 service_unavailable. The editor’s Webhooks dialog offers subscriptions, one-time secret reveal, test delivery, rotation, delivery payloads and redelivery.

Create a subscription with POST /webhooks:

{"url":"https://integrator.example/events","events":["label.*","render.failed"],"templateKeys":["invoice"]}

events accepts catalogue names, a matching prefix wildcard, or *. Multiple matching patterns produce one delivery per subscription. An optional nonempty templateKeys list matches the event’s templateKey; events without a template key cannot match that filter. Omit it or send an empty list for all templates. The server generates a whsec_ secret, or accepts a supplied secret of at least 16 characters. Save the returned secret: subsequent reads show ***.

Envelope and headers

The exact UTF-8 body is stored and signed without reserialising it:

{"id":"evt_example","type":"label.promoted","apiVersion":"1","createdAt":"2026-10-12T00:00:00.000Z","correlationId":"inv-1234","data":{"templateKey":"invoice","label":"live","version":"1.2.0","previousVersion":"1.1.0","actor":"alice"}}

Headers are X-PulpEngine-Event, X-PulpEngine-Event-Id, X-PulpEngine-Delivery-Id, X-PulpEngine-Timestamp (Unix seconds) and X-PulpEngine-Signature (sha256=<hex>). The MAC is HMAC-SHA256 over timestamp + "." + rawBody. The envelope and public delivery DTO omit tenant IDs. The authenticated subscription determines the tenant. Request IDs remain server-owned; a valid caller correlation ID travels in the envelope.

Catalogue (GET /webhooks/event-types) and representative data values:

Type Example data
template.created {"templateKey":"invoice","version":"1.0.0","actor":"alice"}
template.updated {"templateKey":"invoice","version":"1.0.1","previousVersion":"1.0.0","actor":"alice","changeNote":"Updated footer"}
template.deleted {"templateKey":"invoice","actor":"alice"}
template.version_restored {"templateKey":"invoice","restoredFrom":"1.0.0","version":"1.0.2","actor":"alice"}
label.promoted {"templateKey":"invoice","label":"live","version":"1.0.2","previousVersion":"1.0.1","actor":"alice","testRunId":"test-run"}
label.deleted {"templateKey":"invoice","label":"staging","version":"1.0.1","actor":"alice"}
asset.uploaded {"id":"asset-id","filename":"logo.png","mimeType":"image/png","actor":"alice"}
asset.deleted {"id":"asset-id","filename":"logo.png","actor":"alice"}
render.failed {"templateKey":"invoice","format":"pdf","mode":"single","errorCode":"template_expression_error","message":"Invalid expression","requestId":"server-uuid","metadata":{"invoiceNo":"1234"}}
job.completed {"jobId":"job-uuid","kind":"batch","successCount":9,"failureCount":1}
job.failed {"jobId":"job-uuid","kind":"single","error":"render_failed"}
schedule.execution.completed {"scheduleId":"schedule-id","executionId":"execution-id","status":"completed"}
schedule.execution.failed {"scheduleId":"schedule-id","executionId":"execution-id","status":"failed","error":"Render failed"}
schedule.execution.skipped {"scheduleId":"schedule-id","reason":"catchup_window_exceeded","fireTime":"2026-10-12T00:00:00.000Z"}
delivery.exhausted {"jobId":"job-uuid","kind":"batch-callback","attempts":3} (scheduled deliveries instead identify the schedule/execution and target)
webhook.ping {"message":"Pulp Engine webhook test"}

Actors, previous pointers and correlation IDs can be null. Optional fields depend on the event. render.failed.message is capped at 500 characters and omitted when RENDER_LOG_ERROR_MESSAGES=false. Render input data and document bytes are never included. Existing plugin events and completion callbacks continue to work; restoring a version produces one domain restore event.

Verify before parsing

Node (the TypeScript SDK supports both its legacy positional helper and this header-based form):

import { verifyWebhookSignature } from '@pulp-engine/sdk'
const valid = verifyWebhookSignature({ body: rawBody, headers, secret })
if (!valid) throw new Error('Invalid signature')

Python:

from pulp_engine.webhooks import verify_webhook_signature
valid = verify_webhook_signature(body=raw_body, headers=headers, secret=secret)
if not valid:
    raise ValueError("Invalid signature")

Preserve received bytes. Both helpers accept case-insensitive headers, reject timestamps more than 300 seconds from the receiver clock by default, and accept any valid comma-separated signature during rotation. Maintain synchronised clocks.

Shared golden vector (tests override the clock): secret golden-secret-key-32-characters!!, timestamp 1791763200, body exactly:

{"id":"evt_golden","type":"webhook.ping","apiVersion":"1","createdAt":"2026-10-12T00:00:00.000Z","correlationId":null,"data":{"message":"hello"}}

Expected hex MAC: 87bee10971bb35db3428507d599456ef5823f75c5a31b6502b9a14cda038ef65.

Delivery, recovery and rotation

Delivery is at least once with no ordering guarantee. Deduplicate using the event ID, including across manual redelivery (which keeps the event ID and exact body, but creates a new delivery ID with redeliveryOf). Make processing idempotent, then acknowledge quickly with 2xx. A process crash after receiver acceptance can cause a duplicate when its stalled claim is recovered.

Eight attempts use durable delays: immediate, then 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours after the previous attempt. 5xx, 408, 429 and network errors retry. Other 4xx and SSRF blocks are terminal. HTTP 410 also disables the subscription with reason gone; re-enable after fixing the endpoint. Targets are validated at creation/update and again on delivery, with DNS pinning and private-address protection.

POST /webhooks/:id/ping sends one synchronous attempt even when disabled and records it without automatic retry. POST /webhooks/:id/rotate-secret reveals the new secret once; its body accepts previousSecretTtlSeconds (default 86400, range 0–604800). During that window deliveries have both new and previous signatures. Update your receiver, then expire the previous secret. GET /webhooks/:id/deliveries paginates attempts; get an individual delivery for its exact payload. Redelivery of a disabled subscription returns 409.

Lifecycle events await enqueue for up to two seconds after the mutation commits. An enqueue outage does not roll back the mutation. Render-failure enqueue is fire-and-forget. Subscription and pending-delivery caps bound storage; dropped events and enqueue failures increment metrics. Retention deletes terminal deliveries after 30 days by default; pending work is retained. Deleting a subscription also deletes its delivery history. Secrets are stored in the database to sign requests: restrict database access and encrypt backups.