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:
| Header | Value |
|---|---|
Content-Type | application/json |
X-Webhook-Event | batch.completed |
X-Webhook-Timestamp | Unix-Epoch in Millisekunden |
X-Webhook-Signature | sha256=<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"
}jobTypeistbatch_distilloderbatch_extract.statusist einer vonCOMPLETED,FAILEDoderCANCELLED.
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).