各リクエストに対して Auto、TokenLab Verified、または Official を選択でき、価格は事前に表示されます。 新機能を見る

Async AI Task Webhooks: 署名を検証し、タスクを読み取る

CryptoCrypto
·2026年9月28日·約 7 分で読了·更新日 2026年9月28日·31 回表示
#ウェブフック#非同期タスク#API連携#セキュリティ
Async AI Task Webhooks: 署名を検証し、タスクを読み取る

Webhookは、タスクが最終状態に到達したことを示す署名付きのヒントです。それ自体がレコードではありません。したがって、ルールはシンプルです。生のバイト列を検証し、イベントIDで重複排除を行い、2xxを素早く返し、その後GET /v1/tasks/{id}で結果と課金ステータスを読み取ってください。

ワークスペースのタスクWebhookは2026年9月27日にリリースされました。Webhookのライフサイクル管理、テスト配信、シークレットのローテーション、配信履歴のためのManagement APIが提供されています。ダッシュボードおよびMCPによる管理も可能です。

冒頭に1点訂正があります。以前の非同期画像生成ガイドでは、TokenLabにはタスクコールバックがないと記載していましたが、それは2026年9月27日以前の話であり、当該ガイドは本記事とともに更新されています。

Webhookかポーリングか?両方使いましょう

これらは異なる問題を解決するものであり、どちらかがもう一方を置き換えるものではありません。

状況 推奨される手法
タスク終了の瞬間に反応したい Webhook
信頼できる結果やコストが必要 GET /v1/tasks/{id}
受信側がしばらくダウンしていた 保存したタスクIDを用いたポーリング
配信が消失した場合のフォールバックが欲しい 低頻度でのポーリング

Webhookはステータス照会を不要にするものではなく、ポーリングの制限を設けるものでもありません。両方維持してください。Webhookを有効にしていても、保存したタスクIDを読み取る低頻度の照合ループは安価な保険となります。

ポーリングを行う場合はpoll_urlを使用し、タスクが保留中の間はバックオフを行い、最終状態になったら停止してください。401、403、404、またはerror.retryable == falseの場合は停止します。503 async_task_owner_unavailableの場合はバックオフを伴ってリトライしてください。タスクが見つからない、または期限切れの場合は404 async_task_not_foundが返されます。ポーリングの規約についてはAsync jobs and polling guideを参照してください。

3つの認証情報、3つの役割

これらを混同することは、受信側を壊す最短の道です。

認証情報 プレフィックス 役割 備考
Management Token mt-… /v1/management/webhooks*でのWebhookの作成、一覧、更新、削除、テスト、ローテーション Authorization: Bearer mt-…として送信。ワークスペーススコープ
API key sk-… モデルリクエストの送信およびGET /v1/tasks/{id}によるタスクステータスの読み取り Management APIでは拒否されます
Signing secret whsec_… 受信側での配信の検証 Bearerトークンではありません

Management Tokenについて2点。第一に、これは他のワークスペース管理操作も承認するため、Webhook専用の認証情報ではありません。タスクを送信するAPIキーと同じワークスペースを選択してください。第二に、ダッシュボードの「API」→「Management Tokens」から作成します。別のManagement APIの例も参照してください。

mt-…とwhsec_…はバックエンドのみで保持してください。ブラウザやモバイルクライアントには決して送信しないでください。

エンドポイントを作成し、直ちにシークレットを保存する

作成呼び出しは、Webhookのidとwhsec_…で始まる一度限りのsecretを含む201を返します。一覧取得、個別取得、更新では二度とシークレットは表示されません。表示された瞬間に保存してください。

export TOKENLAB_MANAGEMENT_TOKEN="mt-your-management-token"
curl https://api.tokenlab.sh/v1/management/webhooks \
  -H "Authorization: Bearer $TOKENLAB_MANAGEMENT_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://your-app.example/webhooks/tokenlab","events":["task.completed","task.failed","task.timeout"],"description":"Production task results"}'

同じエンドポイントを3つの方法で管理でき、すべて同じオブジェクトを編集します:

URLのルールは厳格です。エンドポイントは公開されたHTTPSである必要があります。URL内に認証情報、クエリ文字列、フラグメントを含めることはできません。リダイレクトは追跡されないため、301は配信失敗としてカウントされます。

1ワークスペースにつき最大10個のエンドポイントを作成できます。11個目を作成しようとすると409 webhook_limit_reachedが返されます。

メソッド パス 目的
GET /v1/management/webhooks エンドポイントの一覧
POST /v1/management/webhooks エンドポイントの作成
GET /v1/management/webhooks/{webhookId} エンドポイントの読み取り
PATCH /v1/management/webhooks/{webhookId} 更新、一時停止、再開
DELETE /v1/management/webhooks/{webhookId} 削除
POST /v1/management/webhooks/{webhookId}/rotate-secret 署名シークレットのローテーション
POST /v1/management/webhooks/{webhookId}/test webhook.testの送信
GET /v1/management/webhooks/{webhookId}/deliveries?page=1&limit=50 配信履歴(最大100件まで)

PATCH {"is_active": false}で一時停止、PATCH {"is_active": true}で再開します。再開すると連続失敗回数がリセットされます。これは障害発生後に重要となります。

実際に届くもの

すべての配信はJSONエンベロープを含むPOSTリクエストです。Management APIのフィールドはsnake_caseですが、コールバックのフィールドはcamelCaseです。一方のケースが他方に引き継がれると想定しないでください。

フィールド 意味
id イベントID。重複排除に使用
type イベントタイプ
created Unix秒
data イベントペイロード。イベントにより形状が異なる
イベント 発生条件
task.completed タスクが正常終了
task.failed タスクが失敗で終了
task.timeout タスクが制限時間に到達
webhook.test テスト操作によってのみ送信

task.completedには、taskType(例:videoやimage)、taskId、オプションのmodel、durationMs、resultUrls、settledCostが含まれます。

task.failedには、taskType、taskId、error、errorCode、retryable、refundOutcomeが含まれます。

task.timeoutには、taskType、taskId、refundOutcomeおよび待機時間フィールドが含まれます。これらの値についてはタスクレコードを読み取ってください。フィールドセットはタスクによって異なります。

サブスクリプションは、ワークスペース内の非同期タスクの将来の最終イベントを対象とします。同期的な結果や過去のタスクは再送されません。選択したイベントタイプのすべてのワークスペースタスクを受信するため、タスク作成時に保存したIDとdata.taskIdを照合してください。

タスクによってはフィールドが存在しない場合があります。そのため、元のワークスペースのsk-…キーを使用したGET /v1/tasks/{id}が、結果と課金ステータスの信頼できる情報源となります。イベントは「何かが終了したこと」を伝え、タスクレコードは「何が生成され、いくらかかったか」を伝えます。

失敗イベントのretryableについてもう一点。これは生成の失敗を表すものであり、自動的に再提出せよという指示ではありません。新しい提出は新しい課金対象のタスクとなります。

生のバイト列を検証し、一度だけ処理する

すべてのPOSTには3つのヘッダーが含まれます:

  • X-Webhook-ID
  • X-Webhook-Timestamp(Unix秒)
  • X-Webhook-Signature(sha256=形式)

署名は、正確なタイムスタンプ文字列、ピリオド、および生のリクエストボディのバイト列に対して、完全なwhsec_…シークレットをキーとしてHMAC-SHA256を適用したものです。順序とボディの内容が重要です。

署名チェックを壊す最も多いミスは以下の2点です:

  1. パース済みのJSONを検証すること。ボディをパースして再シリアライズするとバイト列が変化し、HMACが一致しなくなります。生のボディを読み取り、検証が完了するまでバイト列として保持してください。
  2. ローテーション中に1つのシークレットのみで検証すること。ローテーション後、配信中のリクエストには以前の署名が付いている可能性があります。短い期間はシークレットのリストを受け入れるようにしてください。

以下のNode.jsの受信側は依存関係がなく、node:httpを使用しています。生のボディを読み取り、シークレットのリストに対して検証し、300秒のウィンドウを確認し、ボディのidとX-Webhook-IDを比較し、イベントIDで重複排除を行い、キューに入れ、204を返します。サンプル内の重複排除はメモリ上のSetですが、本番環境では一意のデータベース制約を使用してください。

import { createServer } from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';

// ローテーション中は、新しいwhsec_シークレットと以前のシークレットの両方をリストしてください。
const SECRETS = (process.env.TOKENLAB_WEBHOOK_SECRETS ?? '').split(',').filter(Boolean);
const TOLERANCE_SECONDS = 300;
const seen = new Set(); // 本番環境ではメモリではなく、一意のDB制約を使用してください。

function verify(rawBody, headers) {
  const timestamp = headers['x-webhook-timestamp'];
  const signature = headers['x-webhook-signature'];
  if (typeof timestamp !== 'string' || !/^\d+$/.test(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false;
  if (typeof signature !== 'string' || !/^sha256=[a-f0-9]{64}$/.test(signature)) return false;
  const received = Buffer.from(signature.slice(7), 'hex');
  return SECRETS.some((secret) => {
    const expected = createHmac('sha256', secret).update(timestamp + '.').update(rawBody).digest();
    return timingSafeEqual(expected, received);
  });
}

const server = createServer((req, res) => {
  if (req.method !== 'POST' || req.url !== '/webhooks/tokenlab') {
    res.writeHead(404).end();
    return;
  }
  const chunks = [];
  req.on('data', (chunk) => chunks.push(chunk));
  req.on('end', () => {
    const rawBody = Buffer.concat(chunks); // JSON.parseの前に、正確なバイト列を検証
    if (!verify(rawBody, req.headers)) {
      res.writeHead(401).end();
      return;
    }
    const event = JSON.parse(rawBody.toString('utf8'));
    if (event.id !== req.headers['x-webhook-id']) {
      res.writeHead(400).end();
      return;
    }
    if (!seen.has(event.id)) {
      seen.add(event.id);
      enqueue(event); // リクエストの外で重い処理を行う
    }
    res.writeHead(204).end();
  });
});

function enqueue(event) {
  console.log('queued', event.type, event.data?.taskId);
}

server.listen(Number(process.env.PORT ?? 3000));

この受信側は2026年9月28日にローカルでテスト済みです。有効な配信、重複配信、ローテーション中の以前のシークレット、誤ったシークレット、古いタイムスタンプ、ヘッダーとボディのID不一致、改ざんされたボディ、再シリアライズされたJSONなど、8つのケースすべてに合格しました。重複は一度だけキューに入れられました。

Python側は単一の検証関数です。hmac.compare_digestで署名を比較し、Flaskのrequest.get_data()またはFastAPIのawait request.body()から生のボディバイト列を取得します。

import hashlib
import hmac
import re
import time

TOLERANCE_SECONDS = 300
SIGNATURE_RE = re.compile(r"^sha256=[a-f0-9]{64}$")


def verify_webhook(raw_body: bytes, headers, secrets: list[str]) -> bool:
    """1つ以上のwhsec_シークレットに対してTokenLab Webhookをチェックします。

    raw_bodyは正確なリクエストバイト列である必要があります(Flask: request.get_data(),
    FastAPI/Starlette: await request.body())。JSONパースの前に読み取ってください。
    """
    timestamp = headers.get("x-webhook-timestamp", "")
    signature = headers.get("x-webhook-signature", "")
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
        return False
    if not SIGNATURE_RE.match(signature):
        return False
    received = signature.removeprefix("sha256=")
    for secret in secrets:
        expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
        if hmac.compare_digest(expected, received):
            return True
    return False

2026年9月28日にテスト済み:有効、以前のシークレット、誤ったシークレット、古いタイムスタンプ、改ざんされたボディ、デフォルトのjson.dumpsのスペースで再シリアライズされたボディ。6つのケースすべてに合格しました。

署名以外に、すべてのリクエストで以下の3点を行ってください:

  • 現在から300秒以上離れたタイムスタンプを拒否する。これは5分であり、リプレイ攻撃の有効期限を制限します。
  • ボディのidがX-Webhook-IDと一致することを確認する。
  • イベントIDを作業項目と一緒にアトミックな書き込みで保存する(一意の制約を推奨)。その後、2xxを素早く返し、重い処理は自身のキューから実行する。

配信は繰り返される可能性があり、順序は保証されません。タイムスタンプのウィンドウはリプレイの有効期限を制限します。イベントIDの重複排除は、二重処理を防ぐために必須です。

リトライ、自動一時停止、リカバリ手順

各配信サイクルは最大3回試行されます。

試行 待機時間 試行タイムアウト
1 なし 10 s
2 1 s 10 s
3 4 s 10 s

出典:TokenLab Webhookガイド、2026年9月28日確認。

試行ごとに新しいタイムスタンプと署名が生成されます。そのため、署名チェックにはキャッシュされた値ではなく、そのリクエストのタイムスタンプを使用する必要があります。

リトライ可能なレスポンス:ネットワークエラー、429、5xx。サイクル内でリトライされないもの:その他の4xx、リダイレクト、無効なネットワークターゲット。一時的な障害により、同じ配信IDを持つ同じイベントが後でリトライされる可能性があるため、重複排除は必須です。

10回連続で配信サイクルが失敗すると、エンドポイントは自動的に一時停止されます。

受信側がダウンしていた場合、以下の順序で対応してください:

  1. 受信側を修正する。生のバイト列を読み取り、2xxを素早く返すことを確認する。
  2. PATCH {"is_active": true}でエンドポイントを再開する。これにより失敗回数がリセットされます。
  3. POST …/testでテストを送信する。テストAPIからの200は、試行が記録されたことを意味するだけです。配信履歴を確認し、outcome == "delivered"であることを確認してください。
  4. ギャップを照合する。エンドポイントが一時停止中に保存したタスクIDを使用して、それぞれGET /v1/tasks/{id}を呼び出します。
  5. その後初めて、Webhookストリームを再び信頼してください。

配信履歴にはoutcome、http_status、attempts、delivered_atが含まれます。メタデータのみを保存し、ペイロードは保存しません。古いイベントを手動で再送することはできないため、ステップ4は必須です。保存したタスクIDがリカバリのパスとなります。

イベントを落とさずにシークレットをローテーションする

ローテーションは元に戻せないため、開始前にウィンドウを計画してください。

  1. POST /v1/management/webhooks/{webhookId}/rotate-secretを呼び出します。レスポンスで新しいシークレットが一度だけ返されます。
  2. 新しいシークレットを受信側の検証リストに追加します。古いシークレットもそのリストに残してください。
  3. 何かを破棄する前に、受信側の変更をデプロイします。リストには両方のシークレットを同時に保持する必要があります。
  4. テストを送信し、履歴でoutcome == "delivered"を確認します。
  5. 短い期間が経過した後、古いシークレットを削除して再デプロイします。

配信中のリクエストには以前の署名が付いている可能性があります。シークレットを一度に交換すると、それらのイベントが破棄されます。1つのシークレットしか保持しない検証器は、ローテーション直前に署名された配信を拒否する可能性があります。

MCPからWebhookを管理する

TokenLabをエージェントから操作する場合、MCPサーバーが同じライフサイクルを公開しています。fullプロファイルで@tokenlabai/mcp-serverを使用してください。ツールはlist_webhooks、create_webhook、get_webhook、update_webhook、delete_webhook、rotate_webhook_secret、test_webhook、list_webhook_deliveriesです。

サーバーはTOKENLAB_MANAGEMENT_TOKENからManagement Tokenを読み取ります。2026年9月28日時点で確認された最新の公開パッケージは0.6.24です。MCPはダッシュボードで見えるものと同じエンドポイントを編集するため、照合すべき別の状態はありません。

FAQ

画像タスクはWebhookを送信しますか?

はい。画像タスクを含むワークスペース内のすべての非同期タスクは、そのイベントタイプをサブスクライブしているエンドポイントに最終イベントを送信します。ペイロードのtaskTypeフィールドで、それがvideoなのかimageなのかを確認できます。同期的な結果は対象外です。

エンドポイントがダウンしている場合はどうなりますか?

各サイクルで最大3回リトライされます。10回連続で失敗すると、エンドポイントは自動的に一時停止されます。一時的な配信失敗は、同じ配信IDで後からリトライされる可能性があります。エンドポイントが一時停止されると、一時停止中のイベントは後から配信されず、手動で再送することもできません。受信側を修正し、エンドポイントを再開し、テストを送信し、ギャップ期間中に作成したタスクを保存したIDでGET /v1/tasks/{id}を呼び出して照合してください。

古いイベントを再送できますか?

いいえ。配信履歴はメタデータのみを保持し、ペイロードは保持しません。手動再送もありません。タイムスタンプのウィンドウも300秒より古いものは拒否します。タスクAPIを通じた照合が、キャッチアップのためのサポートされた方法です。

retryable: trueのtask.failedは自動的に再提出しても安全ですか?

いいえ。retryableは生成の失敗を表すものであり、再提出の指示ではありません。新しい提出は新しい課金対象のタスクとなるため、再試行するかどうかは自身で判断し、コストを考慮してください。

Seedance互換性APIはこれらのWebhookを使用しますか?

いいえ。リクエストごとのcallback_urlは、独自のペイロードを持つ個別の契約です。ワークスペースイベントやこれらのHMACヘッダーは使用しないため、1つの検証器を両方に向けないでください。

Webhookガイドの完全な規約から始め、APIキーを作成し、タスクを送信するワークスペースで最初のエンドポイントを有効にしてください。

出典

最近公開されたモデル

この記事のモデルで構築を開始

価格を比較し、ルートを試し、調査内容を実際の API 呼び出しへ進めます。