Webhooks
Was zugestellt wird
PDFgine ruft Sie aus zwei Gründen zurück: Die Warteschlange meldet ein fertig erstelltes Dokument, und der Viewer meldet, wenn die Person, mit der Sie ein Dokument geteilt haben, es öffnet oder herunterlädt.
document.render.succeeded— ein Dokument aus der Warteschlange wurde erstellt und kann heruntergeladen werdendocument.render.failed— ein Dokument aus der Warteschlange konnte nicht erstellt werden; der Inhalt enthält failureCodedocument.viewed— ein geteiltes Dokument wurde im Viewer geöffnetdocument.downloaded— ein geteiltes Dokument wurde heruntergeladen
Erstellungsereignisse gehen an die webhookUrl der Erstellungsanfrage. Viewer-Ereignisse gehen an Endpunkte, die Sie einmalig mit POST /api/v1/engagement-webhooks registrieren; jeder Endpunkt nennt die gewünschten Ereignisse.
Header bei jedem Aufruf
Beide Webhook-Arten verwenden dieselben Header. Eine Prüfroutine deckt daher alles ab.
| Kopfzeile | Enthaltener Wert |
|---|---|
X-PDFgine-Event | Der Ereignistyp, zum Beispiel document.render.succeeded. |
X-PDFgine-Delivery | Die Zustellungs-ID. Bei Wiederholungen bleibt sie gleich — verwenden Sie sie für einen idempotenten Handler. |
X-PDFgine-Timestamp | Zeitpunkt der Signierung als Unix-Sekunden in UTC. |
X-PDFgine-Signature | v1= gefolgt von der Signatur in hexadezimalen Kleinbuchstaben. |
User-Agent | PDFgine-Webhooks/1.0 |
So entsteht die Signatur
Die Signatur umfasst Zeitstempel und Inhalt gemeinsam. Ein abgefangener Aufruf lässt sich daher später nicht mit einem neuen Zeitstempel wiederholen.
X-PDFgine-Signature: v1=<hex>
hex = HMAC-SHA256(
key = <the secret you registered>,
message = <X-PDFgine-Timestamp> + "." + <raw request body>
) // lower-case hexadecimal
Signieren Sie die empfangenen Rohbytes, niemals ein erneut serialisiertes Objekt. Ein JSON-Parser kann Schlüssel umordnen oder Leerzeichen verändern; dann stimmt die Signatur nie überein. Dies ist die häufigste Ursache fehlgeschlagener Prüfungen.
Zeitstempeltoleranz
Sie bestimmen die Toleranz, nicht PDFgine. Lehnen Sie Aufrufe ab, deren Zeitstempel mehr als fünf Minuten von Ihrer Uhr abweicht. Das erlaubt normale Uhrabweichungen und Wiederholungen, macht abgefangene Aufrufe aber schnell unbrauchbar.
Vergleichen Sie die beiden Signaturen in konstanter Zeit. Ein einfacher Zeichenfolgenvergleich endet bei einem falschen ersten Zeichen früher als bei einem falschen letzten; dieser Unterschied ist messbar.
Prüfung, direkt ausführbar
Beide Beispiele führen dieselben vier Schritte aus: Rohinhalt lesen, Alter des Zeitstempels prüfen, Signatur neu berechnen und in konstanter Zeit vergleichen.
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.PDFGINE_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300;
// Der ROHINHALT ist erforderlich: express.json() verändert das JSON und zerstört dadurch die Signatur.
app.post("/hooks/pdfgine", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.get("X-PDFgine-Timestamp");
const header = req.get("X-PDFgine-Signature") ?? "";
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (!timestamp || Number.isNaN(age) || age > TOLERANCE_SECONDS) {
return res.sendStatus(400);
}
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${timestamp}.${req.body.toString("utf8")}`)
.digest("hex");
const supplied = header.startsWith("v1=") ? header.slice(3) : "";
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(supplied, "utf8");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
console.log(req.get("X-PDFgine-Event"), req.get("X-PDFgine-Delivery"), event);
res.sendStatus(204);
});
app.MapPost("/hooks/pdfgine", async (HttpRequest request) =>
{
const int toleranceSeconds = 300;
var secret = Environment.GetEnvironmentVariable("PDFGINE_WEBHOOK_SECRET")!;
using var reader = new StreamReader(request.Body);
var body = await reader.ReadToEndAsync();
var timestampHeader = request.Headers["X-PDFgine-Timestamp"].ToString();
if (!long.TryParse(timestampHeader, out var timestamp)
|| Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - timestamp) > toleranceSeconds)
{
return Results.BadRequest();
}
var expected = Convert.ToHexString(HMACSHA256.HashData(
Encoding.UTF8.GetBytes(secret),
Encoding.UTF8.GetBytes($"{timestamp}.{body}")))
.ToLowerInvariant();
var header = request.Headers["X-PDFgine-Signature"].ToString();
var supplied = header.StartsWith("v1=", StringComparison.Ordinal) ? header[3..] : "";
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(supplied)))
{
return Results.Unauthorized();
}
return Results.NoContent();
});
Aufbau des Inhalts
Erstellungsereignisse enthalten eine Hülle mit Zustellungs-ID, Ereignistyp und einem data-Objekt. Viewer-Ereignisse enthalten die Felder direkt.
{
"id": "evt_8f1c4a7d6b2e4f0a91c3d5e7f9a1b3c5",
"type": "document.render.succeeded",
"createdAt": "2026-09-26T14:39:45+00:00",
"data": {
"queueId": "queue_fe86ad38402a4909b3b5a7965847924a",
"batchId": "batch_69c4ecb63f8642628b72bca89d4f280c",
"status": "succeeded",
"templateCode": "invoice",
"format": "pdf",
"renderJobId": "job_bfae00cc59354089bffd41fa298377b2",
"documentId": "doc_a96063f5dcd5400286ce9b3be9b0595b",
"failureCode": null
}
}
{
"previewId": "prv_510ff800bbcd4f848b0b769cf37ad4c2",
"templateCode": "invoice",
"deviceClass": "desktop",
"accessedAt": "2026-09-26T14:41:02+00:00",
"format": "pdf"
}
Die beiden Strukturen unterscheiden sich derzeit bewusst. Beachten Sie vor dem Schreiben des Handlers: Bei Viewer-Ereignissen steht der Ereignistyp nur im Header X-PDFgine-Event, nicht im Inhalt.
Zustellung und Wiederholungen
Ein Webhook wird in die Warteschlange gestellt und nicht unmittelbar gesendet. Ein langsamer Endpunkt bei Ihnen verzögert daher nie die Dokumenterstellung.
- Jeder 2xx-Status gilt als zugestellt. Alles andere gilt als Fehler, auch eine Weiterleitung.
- Bis zu 6 Versuche, mit exponentiell wachsendem Abstand ab 15 Sekunden.
- Ein Aufruf ohne Antwort innerhalb von 15 Sekunden gilt als fehlgeschlagen und wird wiederholt.
- Das Ziel muss https verwenden und auf eine öffentliche Adresse auflösen. localhost, .local-Namen und private Adressbereiche werden bei der Registrierung des Endpunkts und erneut vor jedem Aufruf abgelehnt.
Bei Wiederholungen bleibt die ID X-PDFgine-Delivery gleich. Speichern Sie daher bereits verarbeitete IDs und antworten Sie bei Wiederholung mit 2xx, ohne die Arbeit erneut auszuführen.
Fehler →Häufige Fehler
- Die Prüfung eines erneut serialisierten Inhalts passt nie. Frameworks ordnen beim JSON-Parsen Schlüssel um und ändern Leerzeichen. Sichern Sie die Rohbytes, bevor etwas sie verarbeitet.
- Nur 2xx gilt als zugestellt. Eine 301- oder 302-Weiterleitung zum eigentlichen Handler zählt als Fehler und wird wiederholt. Verwenden Sie die endgültige Adresse.
- Wer die Signatur, aber nicht den Zeitstempel prüft, lässt abgefangene Aufrufe unbegrenzt wiederholbar. Lehnen Sie alles außerhalb Ihrer Toleranz ab.
- Wiederholungen verwenden dieselbe X-PDFgine-Delivery-ID. Führt ein Handler die Arbeit erneut aus, verschickt er dieselbe Rechnung zweimal. Speichern Sie verarbeitete IDs.
- Registrierte Endpunkte müssen https verwenden und öffentlich auflösbar sein. localhost und private Bereiche werden bei der Registrierung abgelehnt. Testen Sie über einen Tunnel, statt die Ablehnung zu umgehen.