Leitfaden

Webhooks

Anstatt GET /batches/{batch_name} abzufragen, bis der Batch fertig ist, registrieren Sie einmal eine Callback-URL und erhalten einen HTTP-POST, sobald ein Batch abgeschlossen ist. Die drei Signatur-Header und die id.timestamp.body-Konstruktion des signierten Inhalts folgen der offenen Spezifikation Standard Webhooks – 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-Webhook-/Svix-Verifier-Bibliothek wird versuchen, ihn Base64 zu dekodieren und scheitern. Verwenden Sie das Secret genau so, wie es zurückgegeben wird, als rohe HMAC-Schlüsselbytes, mit dem untenstehenden Verifizierungscode (oder Ihrem eigenen Äquivalent) anstelle 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 Geheimnis pro Registrierung, kein gemeinsamer Account-Schlüssel. Ein durchgesickertes Geheimnis betrifft also nur diesen einen Callback:

{
  "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 „Geheimnis anzeigen“-Endpunkt, aber ein erneutes PUT mit derselben callback_url ist eine sichere Methode, es bei Verlust erneut abzurufen (das Geheimnis wird dabei nicht rotiert und nichts zurückgesetzt).

Event-Envelope

Jede Zustellung ist ein JSON-Objekt, das in einer Envelope verpackt ist – niemals rohe Batch-Daten. So kann das Format um zukünftige Event-Typen erweitert werden, ohne bestehende Integrationen zu brechen. Prüfen Sie type, um Events zu unterscheiden; eine auf einem Batch registrierte Callback-URL 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 gibt das zusammengefasste Ergebnis des Batches an (succeeded oder failed – siehe Async Task Model für das vollständige Status-Vokabular). Die Nutzlast ist eine Abschlussbenachrichtigung, 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 Bounding-Box-Annotationsauftrags für ein Dokument verwendet den bereits für den Batch dieses Dokuments registrierten Webhook und liefert ein eigenes Event 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 Events atomar beansprucht. Sie erhalten also keine zwei separaten document.bbox_completed-Zustellungen für einen Bounding-Box-Auftrag, selbst wenn mehrere Zeilengruppen innerhalb kurzer Zeit auf verschiedenen Threads einen Endstatus erreichen. Die übliche webhook-id-basierte Deduplizierung (siehe unten) gilt zusätzlich für den Normalfall einer fehlgeschlagenen und erneut versuchten Zustellung.

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]