Webhooks
Statt zu pollen, kannst du dir von Accessful ein signiertes Event an deinen Server POSTen lassen, sobald ein Job seinen Zustand ändert.
Wie die Registrierung funktioniert
Abschnitt betitelt „Wie die Registrierung funktioniert“Es gibt kein Dashboard und keinen Registrierungs-Endpunkt. Du hängst einen Callback pro Upload an, und du wählst das Signing-Secret selbst — Accessful stellt keins aus.
- Multipart-Upload — sende die Felder
webhookUrlundsecret. - Upload per URL — sende die JSON-Felder
callbackUrlundhmacSignature.
curl -X POST "https://api.accessful.de/api/v1/upload-service/pdf/upload" \ -H "X-API-Key: $ACCESSFUL_API_KEY" \ -F "files=@dokument.pdf" \ -F "webhookUrl=https://deine-app.example.com/hooks/accessful" \ -F "secret=$DEIN_WEBHOOK_SECRET"Event type | Feuert, wenn |
|---|---|
case.running | Die Verarbeitung gestartet ist. Kann mehrfach feuern (einmal pro Iteration). |
case.completed | Das PDF/UA-Ergebnis zum Download bereit ist. |
case.failed | Die Verarbeitung fehlgeschlagen ist. |
case.canceled | Der Case abgebrochen wurde. |
case.quota_exceeded | Abgelehnt, weil das Vertragskontingent erschöpft ist. |
Die Zustände queued und quota_pending werden nicht als Webhook ausgeliefert.
Payload
Abschnitt betitelt „Payload“Der Request-Body ist JSON. Content-Type: application/json.
{ "id": "f1d2c3b4-0000-4a1e-8f3c-2d6b5a9e1c40", "type": "case.completed", "apiVersion": "2026-06-05", "occurredAt": "2026-06-05T12:34:56Z", "data": { "caseId": "7c2f1e4a-9b0d-4a1e-8f3c-2d6b5a9e1c40", "fileName": "dokument.pdf", "jobStatus": "completed" }}| Feld | Typ | Hinweise |
|---|---|---|
id | UUID | Event-ID. Nutze sie als Idempotenz-Schlüssel (siehe unten). |
type | string | Einer der obigen Event-Typen. |
apiVersion | string | Vertragsversion (2026-06-05). Pinne sie, um Änderungen zu erkennen. |
occurredAt | ISO-8601 | Erstellzeitpunkt des Events; stabil über Retries. |
data.caseId | UUID | Der Case, um den es geht. |
data.fileName | string | Der Dateiname. |
data.jobStatus | string | Roher Job-Status, z. B. completed, failed, canceled, quota_exceeded. |
Null-Felder werden im JSON weggelassen.
Zustellungs-Header
Abschnitt betitelt „Zustellungs-Header“| Header | Beispiel | Zweck |
|---|---|---|
X-Accessful-Signature | t=1749126896,v1=9f86d0… | HMAC-Signatur — diese verifizieren |
X-Accessful-Webhook-Timestamp | 1749126896 | Unix-Sekunden; identisch mit t= oben |
X-Accessful-Event-Id | f1d2c3b4-… | Entspricht id; Idempotenz-Schlüssel |
X-Accessful-Event-Type | case.completed | Routing ohne Body-Parsing |
X-Accessful-Case-Id | 7c2f1e4a-… | Die Case-ID |
X-Accessful-Delivery-Attempt | 1 | Versuchszähler, 1-basiert |
X-Signature | n4bQgY… | legacy base64-HMAC nur über den Body |
Signatur verifizieren
Abschnitt betitelt „Signatur verifizieren“Der X-Accessful-Signature-Header hat die Form t=<unix>,v1=<hex>. Berechne sie neu und
vergleiche in konstanter Zeit:
- Lies
tundv1aus dem Header. - Berechne
HMAC-SHA256(secret, "<t>.<roher Body>")und hex-kodiere es. Die signierte Zeichenkette ist der Timestamp, ein wörtlicher Punkt, dann der exakte rohe Request-Body — verifiziere vor dem JSON-Parsen. - Vergleiche in konstanter Zeit gegen
v1. - Optional: lehne ab, wenn
tälter als ein paar Minuten ist (Replay-Schutz).
import crypto from 'node:crypto';
// `rawBody` müssen die exakt empfangenen Bytes sein (z. B. express.raw()).function verify(rawBody, signatureHeader, secret) { const parts = Object.fromEntries(signatureHeader.split(',').map((p) => p.split('='))); const expected = crypto .createHmac('sha256', secret) .update(`${parts.t}.${rawBody}`) .digest('hex');
const valid = crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected)); const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; // 5 Min return valid && fresh;}String[] kv = signatureHeader.split(","); // ["t=...", "v1=..."]long t = Long.parseLong(kv[0].substring(2));String v1 = kv[1].substring(3);
Mac mac = Mac.getInstance("HmacSHA256");mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));byte[] sig = mac.doFinal((t + "." + rawBody).getBytes(StandardCharsets.UTF_8));String expected = HexFormat.of().formatHex(sig);
boolean valid = MessageDigest.isEqual( expected.getBytes(StandardCharsets.UTF_8), v1.getBytes(StandardCharsets.UTF_8));Retries & Idempotenz
Abschnitt betitelt „Retries & Idempotenz“- Erfolg = jede 2xx-Antwort innerhalb des Timeouts (5s zum Verbinden, 10s zum Antworten). Antworte schnell und erledige deine Arbeit asynchron.
- Fehler (kein 2xx, Timeout oder Verbindungsfehler) wird bis zu 10 Mal mit
exponentiellem Backoff und Jitter wiederholt — etwa
10s → 30s → 1,5m → 4,5m → 13,5m → 40m → 2h, dann gedeckelt bei 6h, abzüglich bis zu 20% Jitter. Das Gesamtfenster erstreckt sich über mehrere Stunden. - Nach 10 fehlgeschlagenen Versuchen wird das Event aufgegeben. Für eine erneute Zustellung wende dich an den Support.
- Idempotenz: Dasselbe Event kann mehr als einmal ankommen (z. B. ein Retry, nachdem
dein Endpunkt bereits erfolgreich war). Dedupliziere über
X-Accessful-Event-Id— sie ist über jeden Retry stabil.