Zum Inhalt springen

Webhooks

Statt zu pollen, kannst du dir von Accessful ein signiertes Event an deinen Server POSTen lassen, sobald ein Job seinen Zustand ändert.

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 webhookUrl und secret.
  • Upload per URL — sende die JSON-Felder callbackUrl und hmacSignature.
Terminal window
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.queued Das PDF angenommen wurde und in die Analyse-Warteschlange kam. Feuert einmal und enthält die Start-Warteposition.
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.

Der Zustand quota_pending wird nicht als Webhook ausgeliefert.

Der Request-Body ist JSON. Content-Type: application/json.

{
"id": "f1d2c3b4-0000-4a1e-8f3c-2d6b5a9e1c40",
"type": "case.completed",
"apiVersion": "2026-06-26",
"occurredAt": "2026-06-26T12: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-26). 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. queued, completed, failed, canceled, quota_exceeded.
data.queuePosition integer nur case.queued — 1-basierter Start-Rang in der Warteschlange.
data.queueTotal integer nur case.queued — Gesamtzahl wartender Cases zu dem Zeitpunkt.
data.estimatedWaitSeconds integer nur case.queued — grobe Wartezeit-Schätzung in Sekunden.

Null-Felder werden im JSON weggelassen.

Das case.queued-Event ist das Push-Gegenstück zur Job-Status-Warteposition: Es feuert einmal, im Moment der Annahme eines PDFs, und enthält die Startposition, sodass du „du bist 7. in der Reihe, ~5 Min“ ohne Pollen anzeigen kannst.

{
"id": "a3b1c2d4-0000-4a1e-8f3c-2d6b5a9e1c40",
"type": "case.queued",
"apiVersion": "2026-06-26",
"occurredAt": "2026-06-26T12:30:00Z",
"data": {
"caseId": "7c2f1e4a-9b0d-4a1e-8f3c-2d6b5a9e1c40",
"fileName": "dokument.pdf",
"jobStatus": "queued",
"queuePosition": 7,
"queueTotal": 23,
"estimatedWaitSeconds": 300
}
}

Das ist ein diskretes Lifecycle-Event, kein Live-Stream — die Position ist ein Snapshot zum Annahmezeitpunkt und wird nicht erneut gepusht, während die Schlange vorrückt. Für eine fortlaufend aktualisierte Position polle GET /job-status/{caseId}, dessen Antwort dieselben queue*-Felder trägt, solange der Case wartet. Die queue*-Felder erscheinen nur bei case.queued; jedes andere Event lässt sie weg.

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

Der X-Accessful-Signature-Header hat die Form t=<unix>,v1=<hex>. Berechne sie neu und vergleiche in konstanter Zeit:

  1. Lies t und v1 aus dem Header.
  2. 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.
  3. Vergleiche in konstanter Zeit gegen v1.
  4. 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;
}
  • 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.