Webhooks
GET /batches/{batch_name}を完了するまでポーリングする代わりに、 コールバックURLを一度登録すれば、バッチ完了時にHTTP POSTを受け取れます。3つの署名ヘッダーと id.timestamp.bodyの署名コンテンツ構造は、 Standard Webhooksオープン仕様に従います — OpenAIのWebhooksでも採用されているのと同じアプローチです。 仕様の慣例との違いが1つあります: ここでのwebhook_secretはプレーンな16進文字列であり、whsec_プレフィックス付きのbase64値ではありません — 既製のStandard Webhooks/Svix検証ライブラリはbase64デコードしようとして失敗します。シークレットは返されたままの値で、 生のHMACキーバイトとして、プレフィックス対応ライブラリではなく以下の検証コード(または同等の独自コード)で使用してください。
Webhookの登録
バッチのコールバックを登録(または更新)するには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"}'レスポンスにはWebhookごとのwebhook_secretが含まれ、自動生成されます — アカウント全体で共有されるキーではなく登録ごとに1つのシークレットなので、漏洩しても影響はそのコールバックのみに限定されます:
{
"batch_name": "260716-4K9P",
"callback_url": "https://example.com/hooks/imagetotable",
"webhook_secret": "9f2a3b7c1d8e4f5061728394a5b6c7d81a2b3c4d5e6f7089",
"created_at": "2026-07-16T09:12:03Z",
"fired_at": null
}webhook_secretは保存しておいてください — 受信した配信を検証する際に必要になります。「シークレット表示」専用のエンドポイントはありませんが、同じcallback_urlを再PUTすれば安全に再取得できます(シークレットのローテーションやリセットは行われません)。
イベントエンベロープ
すべての配信はエンベロープに包まれたJSONオブジェクトです — 生のバッチデータではありません。これにより、既存の統合を壊すことなく、将来のイベントタイプに対応するために形状を拡張できます。typeを確認してイベントを区別してください。バッチに登録されたコールバックURLは、以下の両方の種類を混在して受け取ることができます。
{
"type": "batch.completed",
"created_at": "2026-07-16T09:14:31Z",
"data": {
"batch_name": "260716-4K9P",
"status": "succeeded",
"document_count": 3
}
}data.statusはバッチの最終結果を反映します(succeededまたはfailed — 完全なステータス一覧は非同期タスクモデルを参照)。ペイロードは完了通知であり、結果そのものではありません — 受信後にGET /batches/{batch_name}/resultsを呼び出して、実際の抽出データを取得してください。
ドキュメントでbboxアノテーションジョブをトリガーすると、そのドキュメントのバッチに既に登録されているWebhookが再利用され、別の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"
}
}batch.completedと同様に、このイベントの配信はアトミックに要求されます。そのため、異なるスレッドで複数の行グループがほぼ同時に終了ステータスに達しても、1つのbboxジョブに対して2つの別々のdocument.bbox_completed配信を受け取ることはありません。通常のwebhook-idベースの重複排除(下記参照)は、配信失敗時の再試行という通常のケースでも引き続き適用されます。
ポーリングトリガー用のイベントフィード
バッチコールバックWebhookは、既知の1つのbatch_nameにスコープされます。Zapierの「New Completed Batch」や「Failed Extraction」などのポーリングトリガーには、アカウント全体にスコープされたフィードが必要です:GET /api/v1/events。これは永続化されたイベントを新しい順に返し、Zapierが重複排除に使用できる安定したidフィールドを含みます。
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
}サポートされているイベントタイプはbatch.completed、document.completed、document.failedです。イベントはワーカーがドキュメントの終了状態をコミットした後にのみ書き込まれるため、コンシューマーはdocument_urlまたはbatch_results_urlをすぐに安全にフォローできます。失敗したドキュメントの再処理や、同じバッチ内の後続のウェーブの処理では、新しいイベントidが生成されます。batch_nameやdocument_idだけで重複排除しないでください。
バッチが既にわかっていて、1つの待機中の非同期アクションを再開したい場合はコールバックを使用してください。トリガーがアカウント全体で新しい作業を発見する必要がある場合は/eventsを使用してください。ユーザーにbatch_nameを手入力させるトリガーは、通常は誤ったモデルです。
署名の検証
Standard Webhooks 仕様に従い、各配信には次の3つのヘッダーが含まれます。
| ヘッダー | 目的 |
|---|---|
webhook-id | この配信試行の一意のID。重複排除に使用します。再試行された配信は同じIDを再利用します。 |
webhook-timestamp | 配信が送信されたUnixタイムスタンプ。リプレイ攻撃を防ぐために使用します(タイムスタンプが古すぎる配信は拒否します)。 |
webhook-signature | 署名そのもので、v1,<base64 signature> の形式で表されます。 |
署名は、webhook-id、webhook-timestamp、生のリクエストボディをこの順序で . で連結したものに対して、Webhookの webhook_secret をキーとしてHMAC-SHA256を適用したものです。
signed_content = "{webhook_id}.{webhook_timestamp}.{raw_request_body}"
signature = base64(hmac_sha256(webhook_secret, signed_content))ご自身の環境でこれを再計算し、webhook-signature ヘッダーの v1, 以降の値と(定数時間比較を使用して)比較してください。必ず生のリクエストボディのバイト列に対して検証を行ってください。パース済みJSONを再シリアライズしたものではありません。再シリアライズすると、空白やキーの順序が変わり、正規の配信でも署名が一致しなくなる可能性があります。
# 署名検証はHTTP呼び出しではないため、このスクリプトは
# ディスクに保存した配信データ(headers.txt / body.json)を
# opensslで再現します。実際のWebhookハンドラに検証コードを
# 実装する前に、手動でスキームの理解を確認するのに便利です。
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);手動でテストする場合(配信のボディを自分でbody.jsonに保存してから上記のスニペットを実行する場合)、エディタや取得方法によって末尾に改行が追加されていないか注意してください。curl/openssl版は偶然この問題の影響を受けません(bashの$(cat ...)は末尾の改行を削除します)が、Python版とJavaScript版はファイルの正確なバイトを読み取るため、余分な\nが混入すると、一致しない署名を静かに生成します。典型的な症状は「curlでは有効と言われるのにPythonの検証機では無効と言われる」というもので、コード自体にバグがないため混乱を招きます。実際のWebhookハンドラでは、Webフレームワークの生リクエストボディアクセサ(Flaskのrequest.get_data()、Expressのraw-bodyミドルウェアなど)を使ってボディを取得してください。コピペや手動保存は避ければ、本番環境ではこの問題は発生しません。これは手動テストの落とし穴にすぎません。
リトライ
バッチ完了時に、即座に1回目の配信が試行されます。その試行が失敗した場合(タイムアウト、接続エラー、または2xx以外のレスポンス)、指数バックオフでリトライされます:1分、5分、30分、2時間、6時間、24時間 — 合計7回の試行(初回+6回のリトライ)まで行われます。初回試行から24時間以内にいずれも成功しなかった場合、配信は失敗とマークされ、それ以降のリトライは行われません。イベントを確実に記録したら、すぐに2xxステータスで応答するようにエンドポイントを設定してください。遅い処理(バッチ結果の実際の取得や解析など)は応答後に行い、応答前には行わないでください。そうしないと、下流の遅いステップ自体がリトライの原因になります。
バッチの再処理:ウェーブごとに1つの通知
特定の batch.completed 通知は、バッチ内のすべてのドキュメントが最終ステータスに達した瞬間に1回だけ送信されます。後で同じ batch_name にさらにドキュメントを追加し、再度 process を呼び出すと — 「新しいウェーブ」 — そのウェーブが完了した時点でも自動的に通知されます。process_batch は、以前のウェーブですでに通知済みのバッチ内に新しい対象ドキュメントを見つけると、Webhook自体を自動的に再アームします。ウェーブ間でWebhookを再 PUT したり、その他の操作を行う必要はありません。
2つのウェーブが重なった場合 — 前のウェーブがまだ実行中に、さらにドキュメントを追加して処理した場合(前のウェーブが終了した後ではなく) — 両方のウェーブのすべてのドキュメントをカバーする 1つ の通知が、最後のドキュメント(どちらかのウェーブの)が完了した時点で配信されます。2つの別々の通知が送られるのは、2回の process 呼び出しの間にバッチが実際にアイドル状態になった場合(すべてのドキュメントが少なくとも1回は最終ステータスに達した場合)のみです。
このドキュメントの以前のバージョンでは、1回のみの通知を恒久的な制限として説明し、各ウェーブの前にWebhookを再 PUT して回避する必要があるとしていました。そのアドバイスは実際には機能しませんでした — 登録エンドポイントは、以前も今も、意図的に fired_at に関与しません(Webhookリファレンス を参照) — そして、再アームは現在 process によって自動的に処理されます。古いアドバイスに従って作成された統合を使用している場合は、各ウェーブの前に再登録を安全に停止できます。それは何もしていなかったからです。