Anleitungen

Webhooks

Benachrichtigungen über den Abschluss von Batch-Jobs empfangen

Für Batch-Jobs, die länger als eine Minute laufen, sind Webhooks günstiger und schneller als Polling. Thunderbit sendet ein POST an deine URL, wenn ein Job einen terminalen Zustand erreicht.

Konfiguration beim Submit

{
  "urls": ["https://example.com/page1"],
  "webhook": {
    "url": "https://your-server.com/api/webhook/distill",
    "secret": "whsec_your_secret_key",
    "headers": { "X-Custom-Auth": "your-token" }
  }
}

Das Feld webhook.headers akzeptiert eine Map mit eigenen Headern, ist aber für zukünftige Nutzung reserviert und wird vom Server derzeit nicht gesendet. Plane deine Verifizierung ausschließlich auf Basis der unten dokumentierten Header.

Request-Header

Jede Zustellung enthält die folgenden 4 Header:

HeaderValue
Content-Typeapplication/json
X-Webhook-Eventbatch.completed
X-Webhook-TimestampUnix-Epoch in Millisekunden
X-Webhook-Signaturesha256=<base64-encoded HMAC-SHA256>

Payload

{
  "id": "batch_a1b2c3d4...",
  "jobType": "batch_distill",
  "status": "COMPLETED",
  "total": 50,
  "completed": 49,
  "failed": 1,
  "creditsUsed": 100,
  "createdAt": "2026-04-26T10:00:00Z",
  "completedAt": "2026-04-26T10:05:23Z"
}
  • jobType ist batch_distill oder batch_extract.
  • status ist einer von COMPLETED, FAILED oder CANCELLED.

Das Payload ist bewusst klein gehalten — vollständige Ergebnisse holst du nach dem Callback über GET /batch/distill/{id} (bzw. /batch/extract/{id}).

Signaturprüfung

Wenn secret gesetzt ist, enthält jede Zustellung einen X-Webhook-Signature-Header.

  • Algorithmus: HMAC-SHA256
  • Zu signierender String: <X-Webhook-Timestamp>.<raw-json-body> (ein literaler . dazwischen)
  • Ausgabeformat: sha256=<base64-encoded-hash> (Base64, nicht hex)

Verifiziere die rohen Request-Bytes (das JSON nicht neu serialisieren) und vergleiche in konstanter Zeit.

import hmac, hashlib, base64

def verify(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
    base = f"{timestamp}.{raw_body.decode('utf-8')}".encode('utf-8')
    digest = hmac.new(secret.encode('utf-8'), base, hashlib.sha256).digest()
    expected = "sha256=" + base64.b64encode(digest).decode('ascii')
    return hmac.compare_digest(expected, signature)

Replay-Schutz

Lehne die Anfrage ab, wenn |now - X-Webhook-Timestamp| > 60000 (60-Sekunden-Fenster). Der Timestamp ist in Millisekunden seit der Unix-Epoche. Vertraue in der Produktion niemals einem nicht signierten Webhook.

Retry-Verhalten

  • Timeout pro Zustellung: 30 Sekunden
  • Max. Retries: 3
  • Backoff: exponentiell, Start bei 1 Sekunde, gedeckelt bei 10 Sekunden
  • Gesamtes Ende-zu-Ende-Fenster: ~120 Sekunden

Auch wenn alle Retries scheitern, ist der Job bei uns abgeschlossen — dein Endpunkt muss idempotent sein (nutze id als Dedup-Key).