Enterprise Edition
Webhook — Ausführliche Referenz
Auf einen Blick
Abschnitt betitelt „Auf einen Blick“Der Namensraum NextPDF\Enterprise\Webhook liefert mandantengebundene Webhook-Zustellung für Auftragsereignisse. Die öffentliche Oberfläche umfasst sechs Symbole: WebhookManager, WebhookRegistration, WebhookPayload, WebhookDelivery, WebhookRetryPolicy und DeadLetterEntry. Der Manager registriert Endpunkte pro Mandant und dispatcht Auftragsereignisse an abonnierende Registrierungen. Die Zustell-Engine POSTet eine HMAC-SHA256-signierte JSON-Payload, validiert jedes Ziel gegen die Core-SSRF-Ausgangssperre, wiederholt mit exponentiellem Backoff und protokolliert dauerhafte Fehler in einer speicherresidenten Dead-Letter-Queue. Seit 3.1.0 bindet die Signatur den Header X-NextPDF-Timestamp in die MAC-Basiszeichenfolge ein, sodass Empfänger Frische und Integrität gemeinsam prüfen. Den Leitfaden auf Workflow-Ebene finden Sie unter Webhook.
Verfügbarkeit & Lizenzierung
Abschnitt betitelt „Verfügbarkeit & Lizenzierung“Diese Fähigkeit ist in NextPDF Enterprise (nextpdf/enterprise) enthalten und wird mit einer Lizenzhülle der Enterprise-Stufe aktiviert. Eine Bereitstellung ohne diese Berechtigung lädt die Klassen der Fähigkeit nicht. Editionen vergleichen und Lizenz erwerben.
Die Webhook-Oberfläche ist eine Enterprise-Basisfähigkeit, verfügbar sobald das Enterprise-Paket installiert ist; es gibt kein separates Flag pro Feature. NextPDF Core (Apache-2.0) und NextPDF Pro besitzen keine Webhook-Registrierungs- oder Zustelloberfläche; Manager, Registrierung, Payload, Zustell-Engine, Wiederholungsrichtlinie und Dead-Letter-Eintrag sind ausschließlich in nextpdf/enterprise enthalten.
Öffentliche API-Oberfläche
Abschnitt betitelt „Öffentliche API-Oberfläche“| Symbol | Parameter | Standardverhalten | Rückgabe | Wirft oder scheitert mit | Hinweise |
|---|---|---|---|---|---|
WebhookManager::__construct | WebhookDelivery $delivery, ?LoggerInterface $logger = null | Erstellt einen Manager mit leerem speicherresidentem Registrierungsindex | Neuer WebhookManager | Wirft nicht | Registrierungen werden pro Mandant indiziert |
WebhookManager::register | TenantContext $tenant, WebhookRegistration $registration | Hängt die Registrierung an den Index des aufrufenden Mandanten an | void | InvalidArgumentException, wenn der Mandant der Registrierung nicht mit dem Kontextmandanten übereinstimmt | Mandantenübergreifende Registrierung wird vor der Speicherung abgelehnt |
WebhookManager::unregister | TenantContext $tenant, string $registrationId | Ersetzt die passende Registrierung durch eine deaktivierte Kopie | bool | Wirft nicht; gibt false zurück, wenn die ID nicht gefunden wird | Sanfte Deaktivierung; die Historie bleibt erhalten |
WebhookManager::activeRegistrations | TenantContext $tenant | Filtert die Registrierungen des Mandanten auf aktive | list<WebhookRegistration> | Wirft nicht | Nur die Registrierungen des aufrufenden Mandanten sind sichtbar |
WebhookManager::dispatch | TenantContext $tenant, JobEvent $event | Stellt das Ereignis an jede aktive Registrierung zu, die den Ereignistyp abonniert | int (erfolgreiche Zustellungen) | Propagiert JsonException, wenn Ereignisdaten nicht JSON-codierbar sind; Zustellfehler werfen nicht | Pro Registrierungszustellung wird eine frische 32-Hex-Zustell-ID erzeugt |
WebhookRegistration::__construct | string $id, string $tenantId, string $url, array $events, string $secret, bool $active = true, ?string $description = null | Speichert die übergebenen Werte wortgetreu | Neue WebhookRegistration | Kein deklariertes @throws; PHP löst unter strict_types bei nicht passenden Argumenttypen TypeError aus | final readonly; leeres $events bedeutet Abonnement für alle |
WebhookRegistration::subscribesTo | JobEventType $eventType | true, wenn $events leer ist oder den Typ enthält | bool | Wirft nicht | Strenger Identitätsvergleich |
WebhookRegistration::deactivate | — | Gibt eine inaktive Kopie zurück | self | Wirft nicht | Die ursprüngliche Instanz bleibt unverändert |
WebhookPayload::fromJobEvent | JobEvent $event, string $tenantId, string $deliveryId | Kopiert Auftrags-ID, Ereignistyp, Daten und Zeitstempel aus dem Ereignis | self | Wirft nicht | Statische Factory, die von dispatch verwendet wird |
WebhookPayload::toJson | — | Serialisiert den sechsfeldrigen Body mit nicht maskierten Schrägstrichen | non-empty-string | JsonException, wenn Ereignisdaten nicht JSON-codierbar sind | JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES |
WebhookPayload::toArray | — | Gibt den Body als assoziatives Array zurück | array<string, mixed> | Wirft nicht | Zeitstempel im Format RFC 3339 extended |
WebhookPayload::signedTimestamp | — | Ereigniszeit in Unix-Sekunden, auf null oder größer begrenzt | int<0, max> | Wirft nicht | Wird als X-NextPDF-Timestamp ausgegeben und in die MAC eingebunden |
WebhookPayload::sign | string $secret | HMAC-SHA256 über die Basiszeichenfolge {signedTimestamp}.{jsonBody} | non-empty-string (Hex) | JsonException via toJson(), wenn der Body nicht codierbar ist | Bindet den Zeitstempel-Header kryptografisch an den Body |
WebhookDelivery::__construct | ClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(), ?LoggerInterface $logger = null | PSR-18/PSR-17-Zustell-Engine mit leerer Dead-Letter-Queue | Neue WebhookDelivery | Wirft nicht | Standardrichtlinie: 5 Versuche, 1 s Basis, 300 s Obergrenze |
WebhookDelivery::deliver | WebhookRegistration $registration, WebhookPayload $payload | POSTet die signierte Payload mit SSRF-Ausgangsvalidierung pro Versuch und exponentiellem Backoff | bool | JsonException vor dem ersten Versuch, wenn der Body nicht codierbar ist; andernfalls wirft nicht — false bedeutet, dass die Payload in die Dead-Letter-Queue geleitet wurde | true nur bei einer 2xx-Antwort |
WebhookDelivery::deadLetters | — | Gibt alle erfassten Einträge zurück | list<DeadLetterEntry> | Wirft nicht | Speicherresident, prozessbezogen |
WebhookDelivery::clearDeadLetters | — | Leert die Dead-Letter-Queue | void | Wirft nicht | Unwiderruflich; exportieren Sie die Einträge zuerst, falls Wiedergabe erforderlich ist |
WebhookRetryPolicy::__construct | int $maxRetries = 5, int $baseDelaySeconds = 1, int $maxDelaySeconds = 300 | Speichert die Richtlinienwerte | Neue WebhookRetryPolicy | Kein deklariertes @throws; Parameter sind als positive-int dokumentiert | $maxRetries zählt die Gesamtzahl der Versuche |
WebhookRetryPolicy::delayForAttempt | int $attempt | baseDelaySeconds × 2^(attempt − 1), begrenzt auf maxDelaySeconds | positive-int | Wirft nicht | Versuchsnummern sind 1-basiert |
WebhookRetryPolicy::shouldRetry | int $currentAttempt | true, solange der aktuelle Versuch unter dem Maximum liegt | bool | Wirft nicht | Nach dem letzten Versuch wird die Wartezeit übersprungen |
WebhookRetryPolicy::default | — | 5 Versuche, 1 s Basis, 300 s Obergrenze | self | Wirft nicht | Statische Factory; Produktionsstandard |
WebhookRetryPolicy::aggressive | — | 10 Versuche, 2 s Basis, 600 s Obergrenze | self | Wirft nicht | Statische Factory für kritische Endpunkte |
DeadLetterEntry::__construct | string $id, string $registrationId, WebhookPayload $payload, int $attempts, string $lastError, ?int $lastHttpStatus, DateTimeImmutable $failedAt, bool $replayed = false | Speichert den Fehlerdatensatz wortgetreu | Neuer DeadLetterEntry | Kein deklariertes @throws; TypeError unter strict_types | final readonly; null $lastHttpStatus bedeutet Transportfehler |
DeadLetterEntry::markReplayed | — | Gibt eine Kopie mit replayed = true zurück | self | Wirft nicht | Gleiche ID; der ursprüngliche Eintrag bleibt unverändert |
public function __construct( private readonly WebhookDelivery $delivery, private readonly ?LoggerInterface $logger = null,) {}
public function register(TenantContext $tenant, WebhookRegistration $registration): void
public function unregister(TenantContext $tenant, string $registrationId): bool
public function activeRegistrations(TenantContext $tenant): array
public function dispatch(TenantContext $tenant, JobEvent $event): intpublic function __construct( public string $id, public string $tenantId, public string $url, public array $events, public string $secret, public bool $active = true, public ?string $description = null,) {}
public function subscribesTo(JobEventType $eventType): bool
public function deactivate(): selfpublic static function fromJobEvent( JobEvent $event, string $tenantId, string $deliveryId,): self
public function toJson(): string
public function toArray(): array
public function signedTimestamp(): int
public function sign(string $secret): stringpublic function __construct( private readonly ClientInterface $httpClient, private readonly RequestFactoryInterface $requestFactory, private readonly StreamFactoryInterface $streamFactory, private readonly WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(), private readonly ?LoggerInterface $logger = null,) {}
public function deliver(WebhookRegistration $registration, WebhookPayload $payload): bool
public function deadLetters(): array
public function clearDeadLetters(): voidpublic function __construct( public int $maxRetries = 5, public int $baseDelaySeconds = 1, public int $maxDelaySeconds = 300,) {}
public function delayForAttempt(int $attempt): int
public function shouldRetry(int $currentAttempt): bool
public static function default(): self
public static function aggressive(): selfpublic function __construct( public string $id, public string $registrationId, public WebhookPayload $payload, public int $attempts, public string $lastError, public ?int $lastHttpStatus, public DateTimeImmutable $failedAt, public bool $replayed = false,) {}
public function markReplayed(): selfVerhaltensvertrag
Abschnitt betitelt „Verhaltensvertrag“- Registrierungen werden pro Mandant indiziert.
register()lehnt eine Registrierung ab, deren Mandantenkennung nicht mit dem aufrufenden Kontext übereinstimmt.unregister()ist eine sanfte Deaktivierung: Die Registrierung wird durch eine inaktive Kopie ersetzt, wodurch die Historie erhalten bleibt, sie aber von künftigem Dispatch ausgeschlossen wird. dispatch()iteriert nur über die aktiven Registrierungen des aufrufenden Mandanten, die den dispatchten Ereignistyp abonnieren. Eine leere Liste abonnierter Ereignisse bedeutet Abonnement für alle. Der Rückgabewert zählt die erfolgreichen Zustellungen.- Jede Zustellung ist ein HTTP-POST mit JSON-Body und fünf Headern:
Content-Type: application/json,X-NextPDF-Signature(sha256=<hex>),X-NextPDF-Timestamp(Unix-Sekunden),X-NextPDF-Delivery-IdundX-NextPDF-Event. - Die Felder des JSON-Body sind
delivery_id,job_id,event_type,data,timestamp(RFC 3339 extended) undtenant_id, serialisiert mit nicht maskierten Schrägstrichen. Die Ereignistypwerte stammen ausJobEventTypeinnextpdf/core:progress,completed,failed,cancelled. - Signaturschema (geändert in 3.1.0, breaking). Die HMAC-SHA256-Basiszeichenfolge ist
{signedTimestamp}.{jsonBody}, geschlüsselt mit dem Registrierungsgeheimnis — nicht der Body allein. Der WertX-NextPDF-Timestampist die Zeitstempelkomponente der MAC, sodass ein manipulierter oder wiedergegebener Zeitstempel-Header die Signatur ungültig macht. - Empfängerprüfung: Lesen Sie den Header
X-NextPDF-TimestampT; verwerfen Sie, wennTaußerhalb eines akzeptablen Frischefensters liegt (zum Beispiel 300 s); berechnen Siehash_hmac('sha256', T . '.' . rawBody, secret)über die roh empfangenen Bytes neu; vergleichen Sie in konstanter Zeit gegen den Header-Wert, nachdem Sie das Präfixsha256=entfernt haben. - Body, Signatur und Zustell-ID werden einmal pro Zustellung berechnet und bleiben über die Wiederholungsversuche hinweg konstant.
- SSRF-Ausgangssperre. Vor jedem Versuch durchläuft die Ziel-URL die Core-Sperre
UrlValidator::validateExternalUrl(): nur HTTPS-Schema; Loopback-, private, reservierte, Carrier-Grade-NAT-, Cloud-Metadaten- und in IPv4 eingebettete IPv6-Übergangsbereiche werden blockiert; Hostnamen werden per DNS aufgelöst (A und AAAA), und nicht auflösbare Hosts werden fail-closed abgelehnt. Eine blockierte URL wird niemals gesendet: Die Versuchsschleife bricht ab und die Payload wird direkt mit dem letzten FehlerBlocked SSRF destination:und einem null-HTTP-Status in die Dead-Letter-Queue geleitet. - Ergebnisklassifizierung pro Versuch: 2xx ist Erfolg und kehrt sofort zurück; ein 4xx außer 429 ist terminal und geht direkt ins Dead-Letter; jedes andere Ergebnis — 3xx, 429, 5xx oder eine Transportausnahme — ist bis zur Gesamtversuchszahl der Richtlinie wiederholbar.
- Der Backoff ist exponentiell: Die Wartezeit vor dem nächsten Versuch beträgt
baseDelaySeconds × 2^(attempt − 1), begrenzt aufmaxDelaySeconds. Nach dem letzten Versuch wird die Wartezeit übersprungen. - Wenn kein Versuch erfolgreich ist, erfasst ein
DeadLetterEntryeine eindeutige ID, die Registrierungs-ID, die ursprüngliche Payload, die Versuchszahl (auf das Richtlinienmaximum begrenzt), die letzte Fehlermeldung, den letzten HTTP-Status (null bei Transportfehler oder SSRF-Block) und den Fehlerzeitstempel. - Die Dead-Letter-Queue ist speicherresident und auf die Prozesslebensdauer beschränkt.
markReplayed()erzeugt eine markierte Kopie; es sendet nicht erneut, und die Queue behält den ursprünglichen Eintrag.
Grenzfälle & Fehlermodi
Abschnitt betitelt „Grenzfälle & Fehlermodi“- Leere Ereignisliste. Die Registrierung empfängt jeden Ereignistyp. Grenzen Sie die Liste explizit ein, wenn der Empfänger nicht alle Ereignisse sehen darf.
- Terminaler 4xx versus Transportfehler. Eine 4xx-Ablehnung erfasst einen gefüllten
lastHttpStatus; ein Verbindungsfehler erfasst null. Verwenden Sie das null, um Empfängerablehnung von Transportfehler zu unterscheiden. - SSRF-blockiertes Ziel. Eine Registrierung, die auf eine HTTP-, private, Loopback- oder Metadatenadresse zeigt, landet beim ersten Versuch im Dead-Letter mit einem
Blocked SSRF destination:-Fehler und null-Status. Es wird keine ausgehende Anfrage gestellt. Korrigieren Sie die URL und registrieren Sie erneut. - Legacy-Empfänger nach dem Upgrade. Ein Empfänger, der noch die reine Body-HMAC vor 3.1.0 prüft, scheitert fail-closed gegen 3.1.0-Zustellungen. Migrieren Sie den Empfänger auf die Basiszeichenfolge
{timestamp}.{body}und konsumieren SieX-NextPDF-Timestamp. - Nicht codierbare Ereignisdaten.
toJson()undsign()werfenJsonException, die ausdeliver()unddispatch()propagiert, bevor ein Versuch unternommen wird. - Synchrones Blockieren.
deliver()schläft zwischen den Versuchen inline. Der kumulierte Backoff erreicht 15 s unter der Standardrichtlinie und etwa 17 Minuten unter der aggressiven Richtlinie. Dispatchen Sie aus einem Queue-Worker, wenn die Empfängerlatenz nicht vertrauenswürdig ist. - Versuchszahl-Begrenzung. Die erfasste Versuchszahl überschreitet niemals das Richtlinienmaximum, auch wenn der interne Schleifenzähler bei Erschöpfung darüber hinaus fortschreitet.
- Queue-Wachstum und Dauerhaftigkeit. Die Dead-Letter-Queue wächst innerhalb des Prozesses unbegrenzt und verschwindet beim Neustart. Exportieren Sie die Einträge über
deadLetters()und persistieren Sie sie extern, bevor SieclearDeadLetters()aufrufen, wenn dauerhafte Wiedergabe erforderlich ist. - Wiedergabe ist betreibergesteuert. Erneute Zustellung bedeutet,
deliver()erneut mit der Payload des Eintrags aufzurufen;markReplayed()erfasst die Tatsache lediglich auf einer Kopie. - DNS-Rebinding-Restrisiko. Die URL wird bei jedem Versuch erneut validiert, was das Rebinding-Fenster verengt, aber nicht schließt: Die PSR-18-Abstraktion kann die Verbindung nicht an die validierte IP heften. Fügen Sie dort, wo dieses Restrisiko relevant ist, Ausgangskontrollen auf Netzwerkebene hinzu.
- Umgang mit dem Geheimnis. Das Registrierungsgeheimnis ist eine Anmeldeinformation. Die HMAC authentifiziert nur Integrität und Herkunft — sie bietet keine Vertraulichkeit. Legen Sie keine Daten in die Ereignis-Payload, die der Empfänger nicht sehen darf.
Verhalten im FIPS-Modus
Abschnitt betitelt „Verhalten im FIPS-Modus“Die Payload-Signierung erfolgt mit HMAC-SHA256 über PHPs hash_hmac() und ist daher vom Krypto-Provider des Hosts abhängig. In einem FIPS-beschränkten Build scheitert ein nicht zugelassenes Primitiv an der kryptografischen Grenze, statt herabzustufen. Die Webhook-Schicht fügt keine eigene kryptografische Richtlinie hinzu.
Konformität
Abschnitt betitelt „Konformität“- Die Payload-Authentifizierung implementiert HMAC, den keyed-hash message authentication code aus FIPS PUB 198-1 §1, instanziiert mit SHA-256.
- Der Replay-Schutz folgt der Webhook-Sicherheitsanleitung der OWASP Cheat Sheet Series: Der Ereigniszeitstempel reist in einem eigenen Header und wird in die Signaturberechnung eingespeist, sodass ein manipulierter Zeitstempel die Prüfung nicht besteht.
- Body-Zeitstempel verwenden das Datum-Zeit-Format RFC 3339 extended. Code-deklariert: RFC 3339 wurde für diese Seite nicht aus dem RAG-Korpus abgerufen.
- Dies sind Fähigkeitsaussagen, die im Produktquellcode und den zitierten Klauseln begründet sind. NextPDF erhebt für diese Oberfläche keinen Konformitäts- oder Zertifizierungsanspruch.
Entwicklungshinweise
Abschnitt betitelt „Entwicklungshinweise“- Alle Klassen deklarieren
strict_types=1und sindfinal;WebhookRegistration,WebhookPayload,WebhookRetryPolicyundDeadLetterEntrysindfinal readonlymit hochgezogenen öffentlichen Eigenschaften. - Das Modul trägt eine
@since-Annotation von2.2.0; das zeitstempelgebundene Signaturschema ist eine dokumentierte breaking change in 3.1.0. - Die Zustell-Engine nimmt PSR-18/PSR-17-Abstraktionen entgegen, sodass ein Mock-HTTP-Client den vollständigen Sende-, Wiederholungs- und Dead-Letter-Pfad offline durchläuft. Der Logger ist standardmäßig null; injizieren Sie in der Produktion einen PSR-3-Logger, sonst treten Fehler nur über Rückgabewerte zutage.
- Empfängerimplementierungen sollten
hash_equals()für den Signaturvergleich verwenden und ein Frischefenster aufX-NextPDF-Timestampdurchsetzen. - Empfohlene Grenzfalltests: Registrierung mit Mandanten-Diskrepanz, Fan-out bei leerer Ereignisliste, terminaler 4xx, Erschöpfung der Wiederholungen, SSRF-blockierte URL, Ablehnung einer zeitstempelmanipulierten Signatur gegen einen festen Vektor und Begrenzung der Dead-Letter-Versuchszahl.
Publikationsgrenze
Abschnitt betitelt „Publikationsgrenze“Diese Seite dokumentiert ausschließlich extern beobachtbares Verhalten und die unterstützte öffentliche API-Oberfläche. Interne Namensraumpfade, Hilfsklassen, Mechanismustabellen, Runbook-Dateinamen und Ticket-Präfixe liegen außerhalb des Umfangs.
Siehe auch
Abschnitt betitelt „Siehe auch“- Webhook — NextPDF Enterprise — die Fähigkeitsseite: Workflow, Konfiguration und ausgearbeitete Registrierungsbeispiele.
- SaaS — Ausführliche Referenz — Mandantenidentität, API-Schlüssel und Kontingente; die Quelle von
TenantContext. - Metering — Detailreferenz — Fan-out für Nutzungsmessung mit derselben PSR-18-Zustelldisziplin.