Leitfaden

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-Konstruktion des signierten Inhalts 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 unten stehenden 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 Account-weiter 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 erweitert werden kann, um zukünftige Ereignistypen abzudecken, 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 Typen 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-Annotationsauftrags für ein Dokument verwendet den Webhook, der bereits für den Batch dieses Dokuments registriert ist, und liefert ein eigenes Ereignis anstelle 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-Auftrag erhalten, selbst wenn mehrere Zeilengruppen innerhalb kurzer Zeit 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 kontoübergreifenden Feed: GET /api/v1/events. Dieser liefert gespeicherte Ereignisse neueste zuerst mit einem stabilen id-Feld, das Zapier zur Deduplizierung nutzen 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 festgelegt hat, sodass ein Konsument sofort document_url oder batch_results_url 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 meist das falsche Modell.

Signaturen verifizieren

Jede Zustellung enthält drei Header gemäß der Standard-Webhooks-Spezifikation:

HeaderZweck
webhook-idEindeutige ID für diesen Zustellversuch. Verwenden Sie sie zur Deduplizierung – eine erneute Zustellung verwendet dieselbe ID.
webhook-timestampUnix-Zeitstempel der Zustellung, als Schutz vor Replay-Angriffen (lehnen Sie Zustellungen mit einem zu weit zurückliegenden Zeitstempel ab).
webhook-signatureDie 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) nachgebildet – nützlich, um das Schema manuell zu verstehen,
# bevor Sie den echten Prüfcode 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 \
  | base64
import 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 dies manuell 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ügen könnte. Die curl/openssl-Version oben ist dagegen zufällig immun (bashs $(cat ...) entfernt abschließende Zeilenumbrüche), aber die Python- und JavaScript-Versionen lesen die exakten Bytes der Datei und erzeugen stillschweigend eine nicht passende Signatur, wenn ein zusätzliches \n eingeschlichen ist – das klassische Symptom ist: „curl sagt gültig, aber mein Python-Prüfer sagt ungültig“, was genau deshalb verwirrend ist, weil der Code selbst keinen Fehler hat. Erfassen Sie den Body in Ihrem echten Webhook-Handler über den Raw-Request-Body-Accessor Ihres Web-Frameworks (Flask: request.get_data(), Express: Raw-Body-Middleware), anstatt ihn per Copy & Paste manuell zu speichern – dann tritt dieses Problem in der Produktion nicht auf; es ist nur eine Falle beim manuellen Testen.

Wiederholungen

Die Zustellung wird sofort 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 — bis zu 7 Versuche insgesamt (der erste Versuch plus 6 Wiederholungen). Wenn keiner dieser Versuche innerhalb von 24 Stunden nach dem ersten Versuch erfolgreich ist, wird die Zustellung als fehlgeschlagen markiert und es erfolgen keine weiteren Wiederholungen. 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) erst nach der Antwort durch, nicht davor, damit ein langsamer nachgelagerter Schritt nicht selbst eine Wiederholung auslöst.

Erneute Verarbeitung eines Batches: eine Benachrichtigung pro Welle

Eine bestimmte batch.completed-Benachrichtigung wird einmal ausgelöst, sobald jedes Dokument im Batch einen Endstatus erreicht hat. Wenn Sie später weitere Dokumente zum selben batch_name hinzufügen und erneut process aufrufen — "eine neue Welle" — werden Sie automatisch erneut benachrichtigt, sobald auch diese Welle abgeschlossen ist. process_batch aktiviert den Webhook selbst, sobald es neue berechtigte Dokumente in einem Batch findet, dessen vorherige Welle Sie bereits benachrichtigt hat; Sie müssen den Webhook zwischen den Wellen weder erneut per PUT setzen noch etwas anderes tun.

Wenn sich zwei Wellen überschneiden — Sie fügen Dokumente hinzu und verarbeiten sie, während eine frühere Welle noch läuft, anstatt nach deren Abschluss — erhalten Sie genau eine Benachrichtigung, die alle Dokumente aus beiden Wellen abdeckt, und zwar sobald das letzte Dokument (aus einer der beiden Wellen) abgeschlossen ist. Sie erhalten nur dann zwei separate Benachrichtigungen, wenn der Batch zwischen den beiden process-Aufrufen tatsächlich inaktiv war (jedes Dokument hat mindestens einmal einen Endstatus erreicht).

Frühere Versionen dieses Dokuments beschrieben das einmalige Auslösen als dauerhafte Einschränkung, die Sie durch erneutes PUT des Webhooks vor jeder Welle umgehen mussten. Dieser Ratschlag hat nie wirklich funktioniert — der Registrierungsendpunkt war und ist bewusst zurückhaltend mit fired_at (siehe die Webhooks-Referenz) — und die erneute Aktivierung wird jetzt automatisch von process übernommen. Wenn Sie eine Integration verwenden, die nach dem alten Ratschlag erstellt wurde, können Sie die erneute Registrierung vor jeder Welle getrost einstellen; sie hat nie etwas bewirkt.

📮 contact email: [email protected]