Zum Inhalt springen
getnextpdf.com

Enterprise Edition

Webhook — Ausführliche Referenz

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.

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.

SymbolParameterStandardverhaltenRückgabeWirft oder scheitert mitHinweise
WebhookManager::__constructWebhookDelivery $delivery, ?LoggerInterface $logger = nullErstellt einen Manager mit leerem speicherresidentem RegistrierungsindexNeuer WebhookManagerWirft nichtRegistrierungen werden pro Mandant indiziert
WebhookManager::registerTenantContext $tenant, WebhookRegistration $registrationHängt die Registrierung an den Index des aufrufenden Mandanten anvoidInvalidArgumentException, wenn der Mandant der Registrierung nicht mit dem Kontextmandanten übereinstimmtMandantenübergreifende Registrierung wird vor der Speicherung abgelehnt
WebhookManager::unregisterTenantContext $tenant, string $registrationIdErsetzt die passende Registrierung durch eine deaktivierte KopieboolWirft nicht; gibt false zurück, wenn die ID nicht gefunden wirdSanfte Deaktivierung; die Historie bleibt erhalten
WebhookManager::activeRegistrationsTenantContext $tenantFiltert die Registrierungen des Mandanten auf aktivelist<WebhookRegistration>Wirft nichtNur die Registrierungen des aufrufenden Mandanten sind sichtbar
WebhookManager::dispatchTenantContext $tenant, JobEvent $eventStellt das Ereignis an jede aktive Registrierung zu, die den Ereignistyp abonniertint (erfolgreiche Zustellungen)Propagiert JsonException, wenn Ereignisdaten nicht JSON-codierbar sind; Zustellfehler werfen nichtPro Registrierungszustellung wird eine frische 32-Hex-Zustell-ID erzeugt
WebhookRegistration::__constructstring $id, string $tenantId, string $url, array $events, string $secret, bool $active = true, ?string $description = nullSpeichert die übergebenen Werte wortgetreuNeue WebhookRegistrationKein deklariertes @throws; PHP löst unter strict_types bei nicht passenden Argumenttypen TypeError ausfinal readonly; leeres $events bedeutet Abonnement für alle
WebhookRegistration::subscribesToJobEventType $eventTypetrue, wenn $events leer ist oder den Typ enthältboolWirft nichtStrenger Identitätsvergleich
WebhookRegistration::deactivateGibt eine inaktive Kopie zurückselfWirft nichtDie ursprüngliche Instanz bleibt unverändert
WebhookPayload::fromJobEventJobEvent $event, string $tenantId, string $deliveryIdKopiert Auftrags-ID, Ereignistyp, Daten und Zeitstempel aus dem EreignisselfWirft nichtStatische Factory, die von dispatch verwendet wird
WebhookPayload::toJsonSerialisiert den sechsfeldrigen Body mit nicht maskierten Schrägstrichennon-empty-stringJsonException, wenn Ereignisdaten nicht JSON-codierbar sindJSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
WebhookPayload::toArrayGibt den Body als assoziatives Array zurückarray<string, mixed>Wirft nichtZeitstempel im Format RFC 3339 extended
WebhookPayload::signedTimestampEreigniszeit in Unix-Sekunden, auf null oder größer begrenztint<0, max>Wirft nichtWird als X-NextPDF-Timestamp ausgegeben und in die MAC eingebunden
WebhookPayload::signstring $secretHMAC-SHA256 über die Basiszeichenfolge {signedTimestamp}.{jsonBody}non-empty-string (Hex)JsonException via toJson(), wenn der Body nicht codierbar istBindet den Zeitstempel-Header kryptografisch an den Body
WebhookDelivery::__constructClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(), ?LoggerInterface $logger = nullPSR-18/PSR-17-Zustell-Engine mit leerer Dead-Letter-QueueNeue WebhookDeliveryWirft nichtStandardrichtlinie: 5 Versuche, 1 s Basis, 300 s Obergrenze
WebhookDelivery::deliverWebhookRegistration $registration, WebhookPayload $payloadPOSTet die signierte Payload mit SSRF-Ausgangsvalidierung pro Versuch und exponentiellem BackoffboolJsonException vor dem ersten Versuch, wenn der Body nicht codierbar ist; andernfalls wirft nicht — false bedeutet, dass die Payload in die Dead-Letter-Queue geleitet wurdetrue nur bei einer 2xx-Antwort
WebhookDelivery::deadLettersGibt alle erfassten Einträge zurücklist<DeadLetterEntry>Wirft nichtSpeicherresident, prozessbezogen
WebhookDelivery::clearDeadLettersLeert die Dead-Letter-QueuevoidWirft nichtUnwiderruflich; exportieren Sie die Einträge zuerst, falls Wiedergabe erforderlich ist
WebhookRetryPolicy::__constructint $maxRetries = 5, int $baseDelaySeconds = 1, int $maxDelaySeconds = 300Speichert die RichtlinienwerteNeue WebhookRetryPolicyKein deklariertes @throws; Parameter sind als positive-int dokumentiert$maxRetries zählt die Gesamtzahl der Versuche
WebhookRetryPolicy::delayForAttemptint $attemptbaseDelaySeconds × 2^(attempt − 1), begrenzt auf maxDelaySecondspositive-intWirft nichtVersuchsnummern sind 1-basiert
WebhookRetryPolicy::shouldRetryint $currentAttempttrue, solange der aktuelle Versuch unter dem Maximum liegtboolWirft nichtNach dem letzten Versuch wird die Wartezeit übersprungen
WebhookRetryPolicy::default5 Versuche, 1 s Basis, 300 s ObergrenzeselfWirft nichtStatische Factory; Produktionsstandard
WebhookRetryPolicy::aggressive10 Versuche, 2 s Basis, 600 s ObergrenzeselfWirft nichtStatische Factory für kritische Endpunkte
DeadLetterEntry::__constructstring $id, string $registrationId, WebhookPayload $payload, int $attempts, string $lastError, ?int $lastHttpStatus, DateTimeImmutable $failedAt, bool $replayed = falseSpeichert den Fehlerdatensatz wortgetreuNeuer DeadLetterEntryKein deklariertes @throws; TypeError unter strict_typesfinal readonly; null $lastHttpStatus bedeutet Transportfehler
DeadLetterEntry::markReplayedGibt eine Kopie mit replayed = true zurückselfWirft nichtGleiche 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): int
public 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(): self
public 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): string
public 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(): void
public 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(): self
public 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(): self
  • 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-Id und X-NextPDF-Event.
  • Die Felder des JSON-Body sind delivery_id, job_id, event_type, data, timestamp (RFC 3339 extended) und tenant_id, serialisiert mit nicht maskierten Schrägstrichen. Die Ereignistypwerte stammen aus JobEventType in nextpdf/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 Wert X-NextPDF-Timestamp ist 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-Timestamp T; verwerfen Sie, wenn T außerhalb eines akzeptablen Frischefensters liegt (zum Beispiel 300 s); berechnen Sie hash_hmac('sha256', T . '.' . rawBody, secret) über die roh empfangenen Bytes neu; vergleichen Sie in konstanter Zeit gegen den Header-Wert, nachdem Sie das Präfix sha256= 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 Fehler Blocked 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 auf maxDelaySeconds. Nach dem letzten Versuch wird die Wartezeit übersprungen.
  • Wenn kein Versuch erfolgreich ist, erfasst ein DeadLetterEntry eine 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.
  • 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 Sie X-NextPDF-Timestamp.
  • Nicht codierbare Ereignisdaten. toJson() und sign() werfen JsonException, die aus deliver() und dispatch() 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 Sie clearDeadLetters() 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.

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.

  • 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.
  • Alle Klassen deklarieren strict_types=1 und sind final; WebhookRegistration, WebhookPayload, WebhookRetryPolicy und DeadLetterEntry sind final readonly mit hochgezogenen öffentlichen Eigenschaften.
  • Das Modul trägt eine @since-Annotation von 2.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 auf X-NextPDF-Timestamp durchsetzen.
  • 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.

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.