Webhooks
Statt GET /batches/{batch_name} zu pollen, bis der Vorgang abgeschlossen ist, registrieren Sie einmalig eine Callback-URL und erhalten Sie einen HTTP-POST, sobald ein Batch fertig ist. Die drei Signatur-Header und die id.timestamp.body-Signaturkonstruktion folgen der Standard Webhooks-Offenspezifikation — derselbe Ansatz, den auch die Webhooks von OpenAI verwenden. Ein Unterschied zur Konvention der Spezifikation: webhook_secret ist hier ein einfacher Hex-String, kein whsec_-präfixierter Base64-Wert — eine Standard-Webhooks-/Svix-Verifier-Bibliothek von der Stange versucht, ihn Base64-zu-dekodieren, und schlägt fehl. Verwenden Sie das Secret genau wie zurückgegeben, als rohe HMAC-Schlüsselbytes, mit dem untenstehenden Verifizierungscode (oder Ihrem eigenen Äquivalent) statt einer präfixbewussten Bibliothek.
Webhook registrieren
Registrieren (oder aktualisieren) Sie den Callback eines Batches mit PUT /api/v1/batches/{batch_name}/webhook:
curl -X PUT https://imagetotable.ai/api/v1/batches/260716-4K9P/webhook \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"callback_url": "https://example.com/hooks/imagetotable"}'Die Antwort enthält ein pro Webhook generiertes webhook_secret — ein Secret pro Registrierung, kein gemeinsamer kontoübergreifender Schlüssel, sodass ein geleaktes Secret nur diesen einen Callback betrifft:
{
"batch_name": "260716-4K9P",
"callback_url": "https://example.com/hooks/imagetotable",
"webhook_secret": "9f2a3b7c1d8e4f5061728394a5b6c7d81a2b3c4d5e6f7089",
"created_at": "2026-07-16T09:12:03Z",
"fired_at": null
}Speichern Sie webhook_secret — Sie benötigen es, um eingehende Zustellungen zu verifizieren. Es gibt keinen separaten „Secret anzeigen“-Endpunkt, aber ein erneutes PUT mit derselben callback_url ist ein sicherer Weg, es wieder abzurufen, falls Sie es verlieren (es rotiert das Secret nicht und setzt nichts zurück).
Event-Envelope
Jede Zustellung ist ein JSON-Objekt, das in eine Envelope verpackt ist – niemals rohe Batch-Daten –, damit die Struktur um zukünftige Ereignistypen erweitert werden kann, ohne bestehende Integrationen zu brechen. Prüfen Sie type, um Ereignisse zu unterscheiden; eine Callback-URL, die für einen Batch registriert ist, kann beide unten genannten Arten gemischt empfangen.
{
"type": "batch.completed",
"created_at": "2026-07-16T09:14:31Z",
"data": {
"batch_name": "260716-4K9P",
"status": "succeeded",
"document_count": 3
}
}data.status spiegelt das zusammengefasste Ergebnis des Batches wider (succeeded oder failed – siehe Async Task Model für das vollständige Statusvokabular). Die Nutzlast ist eine Abschlussmeldung, nicht die Ergebnisse selbst – rufen Sie nach Erhalt GET /batches/{batch_name}/results auf, um die tatsächlich extrahierten Daten abzurufen.
Das Auslösen eines Bbox-Annotationsjobs für ein Dokument verwendet den Webhook, der bereits für den Batch dieses Dokuments registriert ist, und liefert ein eigenes Ereignis statt eines weiteren batch.completed:
{
"type": "document.bbox_completed",
"created_at": "2026-07-16T09:20:07Z",
"data": {
"document_id": "3f9c9e2a-1b7d-4e2b-9a3e-2f5b6c7d8e9f",
"group_batch_id": "bx_8a3c2e1f",
"status": "succeeded"
}
}Wie bei batch.completed wird die Zustellung dieses Ereignisses atomar beansprucht, sodass Sie keine zwei separaten document.bbox_completed-Zustellungen für einen Bbox-Job erhalten, selbst wenn mehrere Zeilengruppen innerhalb von Augenblicken auf verschiedenen Threads einen Endstatus erreichen. Die übliche Deduplizierung auf Basis von webhook-id (siehe unten) gilt zusätzlich für den normalen Fall einer wiederholten fehlgeschlagenen Zustellung.
Event-Feed für Polling-Trigger
Batch-Callback-Webhooks sind auf einen bekannten batch_name beschränkt. Polling-Trigger wie Zapiers „New Completed Batch“ oder „Failed Extraction“ benötigen stattdessen einen kontobezogenen Feed: GET /api/v1/events. Er gibt gespeicherte Ereignisse neueste zuerst zurück, mit einem stabilen id-Feld, das Zapier zur Deduplizierung verwenden kann.
curl "https://imagetotable.ai/api/v1/events?type=batch.completed&limit=20" \
-H "Authorization: Bearer $API_KEY"{
"events": [
{
"id": "evt_3f9c9e2a1b7d4e2b9a3e2f5b6c7d8e9f",
"type": "batch.completed",
"created_at": "2026-08-04T08:31:22+00:00",
"batch_name": "260804-4K9P",
"document_id": null,
"status": "succeeded",
"document_count": 3,
"filename": null,
"error": null,
"batch_url": "https://imagetotable.ai/api/v1/batches/260804-4K9P",
"batch_results_url": "https://imagetotable.ai/api/v1/batches/260804-4K9P/results"
}
],
"has_more": false,
"next_page_token": null
}Unterstützte Ereignistypen sind batch.completed, document.completed und document.failed. Ereignisse werden erst geschrieben, nachdem der Worker den Endstatus des Dokuments festgeschrieben hat, sodass ein Konsument document_url oder batch_results_url sofort sicher folgen kann. Die erneute Verarbeitung eines fehlgeschlagenen Dokuments oder die Verarbeitung einer späteren Welle im selben Batch erzeugt eine neue Ereignis-id; deduplizieren Sie nicht allein nach batch_name oder document_id.
Verwenden Sie Callbacks, wenn Sie den Batch bereits kennen und eine wartende asynchrone Aktion fortsetzen möchten. Verwenden Sie /events, wenn ein Trigger neue Arbeit über das gesamte Konto entdecken muss. Ein Trigger, der den Benutzer auffordert, einen batch_name manuell einzugeben, ist in der Regel das falsche Modell.
Signaturen verifizieren
Jede Zustellung enthält drei Header gemäß der Standard-Webhooks-Spezifikation:
| Header | Zweck |
|---|---|
webhook-id | Eindeutige ID für diesen Zustellversuch. Verwenden Sie sie zur Deduplizierung – eine erneute Zustellung verwendet dieselbe ID. |
webhook-timestamp | Unix-Zeitstempel der Zustellung, als Schutz vor Replay-Angriffen (lehnen Sie Zustellungen mit einem zu weit zurückliegenden Zeitstempel ab). |
webhook-signature | Die Signatur selbst, formatiert als v1,<Base64-Signatur>. |
Die Signatur ist ein HMAC-SHA256 über die Verkettung von webhook-id, webhook-timestamp und dem rohen Request-Body – in dieser Reihenfolge, verbunden mit . – und mit Ihrem webhook_secret signiert:
signed_content = "{webhook_id}.{webhook_timestamp}.{raw_request_body}"
signature = base64(hmac_sha256(webhook_secret, signed_content))Berechnen Sie dies auf Ihrer Seite neu und vergleichen Sie es (mittels eines konstanten Zeitvergleichs) mit dem Wert nach v1, im webhook-signature-Header. Verifizieren Sie immer anhand der rohen Request-Body-Bytes, nicht anhand einer neu serialisierten Version des geparsten JSON – eine erneute Serialisierung kann Leerzeichen/Reihenfolge der Schlüssel ändern und selbst bei einer echten Zustellung zu einer Signatur-Abweichung führen.
# Die Signaturprüfung ist kein HTTP-Aufruf, daher wird sie hier mit
# openssl gegen eine bereits gespeicherte Zustellung (headers.txt /
# body.json) nachgestellt – nützlich, um Ihr Verständnis des Schemas
# manuell zu überprüfen, bevor Sie echten Verifikationscode in Ihrem
# Webhook-Handler implementieren.
WEBHOOK_ID=$(grep -i '^webhook-id:' headers.txt | cut -d' ' -f2 | tr -d '\r')
WEBHOOK_TS=$(grep -i '^webhook-timestamp:' headers.txt | cut -d' ' -f2 | tr -d '\r')
SIGNED_CONTENT="${WEBHOOK_ID}.${WEBHOOK_TS}.$(cat body.json)"
echo -n "$SIGNED_CONTENT" \
| openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -binary \
| base64import base64
import hashlib
import hmac
import os
headers = dict(line.split(": ", 1) for line in open("headers.txt") if ": " in line)
webhook_id = headers["webhook-id"].strip()
webhook_timestamp = headers["webhook-timestamp"].strip()
body = open("body.json", "rb").read()
signed_content = f"{webhook_id}.{webhook_timestamp}.".encode() + body
signature = base64.b64encode(
hmac.new(os.environ["WEBHOOK_SECRET"].encode(), signed_content, hashlib.sha256).digest()
)
print(signature.decode())import { createHmac } from "node:crypto";
import { readFileSync } from "node:fs";
const headers = Object.fromEntries(
readFileSync("headers.txt", "utf8")
.split("\n")
.filter((line) => line.includes(": "))
.map((line) => line.split(": "))
);
const webhookId = headers["webhook-id"].trim();
const webhookTimestamp = headers["webhook-timestamp"].trim();
const body = readFileSync("body.json", "utf8");
const signedContent = `${webhookId}.${webhookTimestamp}.${body}`;
const signature = createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(signedContent)
.digest("base64");
console.log(signature);Wenn Sie das von Hand testen – also den Body einer Zustellung selbst in body.json speichern, bevor Sie eines der obigen Snippets ausführen – achten Sie auf einen abschließenden Zeilenumbruch, den Ihr Editor oder Ihre Erfassungsmethode hinzufügt. Die curl/openssl-Version oben ist zufällig immun dagegen (das $(cat ...) von bash entfernt abschließende Zeilenumbrüche), aber die Python- und JavaScript-Versionen lesen die exakten Bytes der Datei und erzeugen stillschweigend eine Signatur, die nicht übereinstimmt, wenn sich ein zusätzliches \n eingeschlichen hat – das klassische Symptom ist: „curl sagt, sie sei gültig, aber mein Python-Verifizierer sagt, sie sei es nicht“, was genau deshalb verwirrend ist, weil der Code selbst keinen Fehler hat. Erfassen Sie den Body über die Zugriffsmethode für den rohen Request-Body Ihres Web-Frameworks (request.get_data() von Flask, raw-body-Middleware von Express) in Ihrem tatsächlichen Webhook-Handler, anstatt ihn zu kopieren/einzufügen oder manuell zu speichern – dann tritt das in der Produktion nicht auf; es ist nur eine Falle beim manuellen Testen.
Wiederholungen
Die Zustellung wird sofort einmal versucht, sobald der Batch abgeschlossen ist. Schlägt dieser Versuch fehl (Timeout, Verbindungsfehler oder eine Nicht-2xx-Antwort), wird er mit exponentiellem Backoff wiederholt: 1 Minute, 5 Minuten, 30 Minuten, 2 Stunden, 6 Stunden, 24 Stunden – insgesamt bis zu 7 Versuche (der erste Versuch plus 6 Wiederholungen). Wenn keiner davon innerhalb von 24 Stunden nach dem ersten Versuch erfolgreich ist, wird die Zustellung als fehlgeschlagen markiert und es finden keine weiteren Wiederholungen statt. Lassen Sie Ihren Endpunkt so schnell wie möglich mit einem 2xx-Status antworten, sobald Sie das Ereignis dauerhaft aufgezeichnet haben. Führen Sie langsame Verarbeitung (wie das tatsächliche Abrufen und Parsen der Batch-Ergebnisse) nach dem Antworten aus, nicht davor, damit ein langsamer nachgelagerter Schritt nicht selbst eine Wiederholung auslöst.
Batch erneut verarbeiten: eine Benachrichtigung pro Welle
Eine bestimmte batch.completed-Benachrichtigung wird genau einmal ausgelöst, sobald jedes Dokument im Batch einen Endstatus erreicht hat. Wenn Sie später weitere Dokumente zu demselben batch_name hinzufügen und process erneut aufrufen – „eine neue Welle“ – werden Sie automatisch erneut benachrichtigt, sobald auch diese Welle abgeschlossen ist. process_batch aktiviert den Webhook selbst wieder, sobald es in einem Batch neue berechtigte Dokumente findet, dessen vorherige Welle Sie bereits benachrichtigt hat; Sie müssen den Webhook zwischen den Wellen nicht erneut per PUT aktualisieren oder sonst etwas tun.
Wenn sich zwei Wellen überlappen – Sie fügen weitere Dokumente hinzu und verarbeiten sie, während eine frühere Welle noch läuft, statt erst nach deren Abschluss – erhalten Sie genau eine Benachrichtigung, die alle Dokumente aus beiden Wellen abdeckt und zugestellt wird, sobald das letzte davon (aus einer der beiden Wellen) abgeschlossen ist. Sie erhalten nur dann zwei getrennte Benachrichtigungen, wenn der Batch zwischen den beiden process-Aufrufen tatsächlich inaktiv wurde (jedes Dokument hat mindestens einmal einen Endstatus erreicht).
Frühere Versionen dieser Dokumentation beschrieben das einmalige Auslösen als dauerhafte Einschränkung, die Sie umgehen mussten, indem Sie den Webhook vor jeder Welle erneut per PUT aktualisierten. Dieser Rat hat nie wirklich funktioniert – der Registrierungs-Endpunkt war und ist in Bezug auf fired_at bewusst zurückhaltend (siehe die Webhook-Referenz) – und das erneute Aktivieren wird stattdessen jetzt automatisch von process übernommen. Wenn Siere on an integration written against the old advice, you can safely stop re-registering before each wave; it was never doing anything.