Enterprise edition
Webhook
At a glance
Section titled “At a glance”NextPDF Enterprise delivers job events to per-tenant webhook endpoints over HTTP POST, signs each payload with an HMAC-SHA256 signature, retries with exponential backoff, and routes permanently failed deliveries to a dead-letter queue for inspection and replay. This page describes the observable webhook behavior and the public contract.
Availability & licensing
Section titled “Availability & licensing”This capability ships in NextPDF Enterprise (nextpdf/enterprise) and activates with an Enterprise-tier license envelope. A deployment without that entitlement does not load the capability’s classes. Compare editions and get a license.
The webhook surface is a base Enterprise capability, available once the Enterprise package is installed; there is no separate per-feature flag.
Conceptual overview
Section titled “Conceptual overview”A tenant registers a callback URL, a signing secret, and an optional list of event types. An empty event list means “subscribe to all events”. Registrations are strictly tenant-scoped: a tenant can only see and manage its own registrations, and registering under a mismatched tenant is rejected. Unregistering deactivates the registration rather than deleting it, so history is preserved; only active registrations receive dispatches.
When a job event is dispatched for a tenant, each active registration that subscribes to the event type receives a delivery. The payload is a standardized JSON document — a unique delivery identifier, the job identifier, the event type, the event data, an RFC 3339 timestamp, and the tenant identifier. The delivery is an HTTP POST carrying the JSON body and four headers: an HMAC-SHA256 signature, a unix-seconds timestamp, the delivery identifier, and the event type. The signature is computed over the canonical base string {timestamp}.{body} with the registration’s secret, so the timestamp header is cryptographically bound to the body. The receiver recomputes the HMAC over the same base string and rejects deliveries whose timestamp falls outside an acceptable freshness window, which bounds replay.
Delivery uses exponential backoff. A 2xx response is success. A 4xx response other than 429 is treated as a permanent rejection and is not retried. Other failures — 5xx, 429, or a connection error — are retried up to the policy’s attempt count with a doubling delay capped at a maximum. When all attempts are exhausted the delivery is recorded in an in-memory dead-letter queue with the original payload, the attempt count, the last error, and the last HTTP status; a dead-letter entry can be marked replayed. Two retry policies ship — a default (5 attempts, 1s base, 5min cap) and an aggressive one (10 attempts, 2s base, 10min cap).
Why it works this way
Section titled “Why it works this way”Delivery is treated as an operational surface, not a fire-and-forget call. Failures are classified by intent. A 4xx other than 429 is a genuine receiver rejection, so it stops at once. A 5xx, a 429, or a connection error is transient, so it earns a capped, backing-off retry. Deliveries that exhaust every attempt are never dropped silently; they land in an inspectable dead-letter queue that can be replayed. The signature binds a timestamp into its base string, and every destination clears an egress gate, so authenticity and replay resistance hold by construction for each tenant.
Design background: Operating NextPDF in production.
Public API surface
Section titled “Public API surface”composer require nextpdf/enterprise:^3The supported integration points are the webhook manager (register, unregister, activeRegistrations, dispatch), the registration value object (subscribesTo, deactivate), the payload (fromJobEvent, toJson, toArray, sign, signedTimestamp), the delivery engine (deliver, deadLetters, clearDeadLetters), the retry policy (delayForAttempt, shouldRetry, default, aggressive), and the dead-letter entry (markReplayed).
Code sample — quick start
Section titled “Code sample — quick start”use NextPDF\Enterprise\Webhook\WebhookManager;use NextPDF\Enterprise\Webhook\WebhookRegistration;
$manager->register($tenant, new WebhookRegistration( id: $id, tenantId: $tenant->tenantId, url: 'https://customer.example.com/hooks/nextpdf', events: [], // empty = subscribe to all event types secret: $signingSecret,));
$delivered = $manager->dispatch($tenant, $jobEvent); // count of successesReceiver-side verification:
$ts = (int) $request->header('X-NextPDF-Timestamp');if (abs(time() - $ts) > 300) { return new Response(401); // stale timestamp: reject to bound replay}$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $rawBody, $sharedSecret);if (! hash_equals($expected, $request->header('X-NextPDF-Signature'))) { return new Response(401);}Code sample — production
Section titled “Code sample — production”use NextPDF\Enterprise\Webhook\WebhookDelivery;use NextPDF\Enterprise\Webhook\WebhookRetryPolicy;
$delivery = new WebhookDelivery( $httpClient, $requestFactory, $streamFactory, retryPolicy: WebhookRetryPolicy::aggressive(), // 10 attempts, 2s base, 10min cap logger: $logger,);
$manager = new WebhookManager($delivery, $logger);$manager->dispatch($tenant, $jobEvent);
foreach ($delivery->deadLetters() as $dead) { $this->scheduleReplay($dead); // inspect last error + last HTTP status}Edge cases & gotchas
Section titled “Edge cases & gotchas”- Empty event list subscribes to all. A registration with no event types receives every event; pass an explicit list to scope it.
- Tenant isolation is enforced. Registering with a tenant ID that differs from the context tenant is rejected; dispatch only iterates the calling tenant’s active registrations.
- 4xx (except 429) is terminal. A 4xx other than 429 is not retried — it is treated as a permanent receiver rejection and goes to the dead-letter queue.
- Unregister is soft. Unregistering deactivates; the record persists and is excluded from dispatch.
- Dead-letter queue is in-memory. It is for inspection and replay within the process lifetime; persist entries yourself if you need durable replay across restarts.
Performance
Section titled “Performance”Dispatch cost is proportional to the number of active registrations for the tenant that subscribe to the event. Each delivery is one HMAC-SHA256 over the signed base string plus the HTTP round trip; retries add bounded exponential-backoff delays. Signing is O(payload size).
Security notes
Section titled “Security notes”Each payload is authenticated with an HMAC-SHA256 signature keyed by the registration’s secret and sent in the X-NextPDF-Signature header as sha256=<hex>. The signature covers the {timestamp}.{body} base string, and the timestamp travels in the X-NextPDF-Timestamp header; receivers verify with a constant-time comparison and reject deliveries outside a freshness window to bound replay. Destination URLs pass a central egress gate before every send: HTTPS is required, and hosts that resolve to private, loopback, link-local, or cloud-metadata addresses are refused without a request and routed to the dead-letter queue. The signing secret is per-registration; treat it as a credential. The signature authenticates payload integrity and origin; it is not an encryption layer — do not place secrets in event data that the receiver should not see.
Conformance
Section titled “Conformance”- Payload authentication uses HMAC with SHA-256, the keyed-hash message authentication code of FIPS PUB 198-1; OWASP ASVS 5.0 lists HMAC-SHA-256 among its approved message-authentication algorithms.
- Payload timestamps are RFC 3339 date-time strings.
Behavior contract
Section titled “Behavior contract”- Registrations are strictly tenant-scoped; registering under a mismatched tenant is rejected and unregister is a soft deactivate that preserves history.
- An empty event list subscribes to all events; only active registrations subscribing to the event type receive a dispatch.
- Each delivery is an HTTP POST with the JSON body plus an HMAC-SHA256 signature header (over the
{timestamp}.{body}base string), a unix-seconds timestamp header, the delivery identifier, and the event type. - A 2xx is success; a 4xx other than 429 is a permanent rejection (no retry); 5xx, 429, or a connection error is retried up to the policy’s attempt count with capped doubling backoff.
- Exhausted attempts record the delivery in an in-memory dead-letter queue (payload, attempt count, last error, last status); a dead-letter entry can be marked replayed.
Publication boundary
Section titled “Publication boundary”This page documents externally observable behavior and the supported public API surface only. Internal namespace paths, helper classes, mechanism tables, runbook filenames, and ticket prefixes are out of scope.
Core fallback
Section titled “Core fallback”NextPDF Core (Apache-2.0) has no webhook registration or delivery surface — none; this capability has no Core-tier equivalent.
Pro fallback
Section titled “Pro fallback”NextPDF Pro has no webhook registration or delivery surface — none; this capability has no Pro-tier equivalent. The webhook manager, registration, payload, delivery engine, and retry policy ship in the nextpdf/enterprise package only.
Enterprise boundary note
Section titled “Enterprise boundary note”The retry policy, backoff schedule, and dead-letter handling are described at the behavior level. The dead-letter queue is in-memory for inspection and replay within the process lifetime; durable cross-restart persistence and any internal delivery internals are out of scope for the public surface.
Deployment boundary
Section titled “Deployment boundary”The operator owns the callback endpoints, the per-registration signing secrets (treated as credentials), durable persistence of dead-letter entries if cross-restart replay is required, and the HTTPS posture of receiver URLs. NextPDF Enterprise signs and delivers but does not itself persist registrations or dead letters beyond the process lifetime.
Legal-compliance boundary
Section titled “Legal-compliance boundary”No export-control restriction applies to the webhook surface. The HMAC signature authenticates payload integrity and origin; it is not an encryption layer — operators must not place secrets in event data the receiver should not see. This documentation is not a legal opinion; consult your own compliance and legal advisers.