本文へスキップ

実装ガイド

リクエストが消えたあとで、ジョブを終わらせる。

メディアタスクは、それを作った HTTP 呼び出しよりも、起点となったページよりも、しばしばそのとき動いていたデプロイよりも長く生き残ります。ここはそれに耐える作り方の話です。

解決する課題

接続はトランザクションではない

送信からファイル入手までに、作業を失う場所が四つあります。タイムアウト後に何も考えず再送する作成、検証できないコールバック、叩きすぎるか諦めるポーリング、そして扱いを決めているあいだに期限が切れた成果物 URL です。

01

キーなしの再送は二枚目の請求書です

Idempotency-Key があれば、24 時間以内の同じモデル・入力・コールバックの再送は、資金を二度保留せず元のタスクを返します。スコープは API キー単位で、兄弟キーが同じ値を再生しても 409 が返るだけでタスク ID は得られません。

02

検証しないコールバックは開いた口です

配信は X-Webhook-Timestamp と、taskId.timestamp.sha256(raw_body) から計算した Base64 の HMAC-SHA256 を X-Webhook-Signature で運びます。生のバイト列に対して定時間で比較し、古いタイムスタンプはタスクや課金状態に触れる前に拒否してください。

03

上限のないポーリングはレート制限の予約です

recordInfo は補助であって転送路ではありません。二秒程度から始め、毎回 1.5 倍し、十五秒で頭打ちにし、ネットワークではなく自分が決めた期限で止めます。

実装方針

タスクごとに一本の永続レコードを、応答より先に書く

冪等キー、taskId、自分の相関 ID を、呼び出し元へ応答するのと同じトランザクションで永続化します。以後のコールバック、ポーリング、再試行、問い合わせはすべてその行に解決し、重複配信は二件目の注文ではなく一回の更新になります。

先に検証し、それから読む。署名は生のバイト列を対象にするため、比較が通ってから解析します。

Verify the callback before you trust it

import { createHash, createHmac, timingSafeEqual } from "node:crypto";

// rawBody is the exact bytes that arrived. Parse only to read the task ID:
// the digest is always computed over those raw bytes, never over a re-encode.
export function verifyDelivery(rawBody, headers, webhookKey) {
  const timestamp = headers["x-webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const taskId = JSON.parse(rawBody).data.taskId;          // payload version 2
  const digest = createHash("sha256").update(rawBody).digest("hex");
  const expected = createHmac("sha256", webhookKey)
    .update(`${taskId}.${timestamp}.${digest}`)
    .digest("base64");

  const sent = Buffer.from(headers["x-webhook-signature"] ?? "", "utf8");
  const mine = Buffer.from(expected, "utf8");
  return sent.length === mine.length && timingSafeEqual(sent, mine);
}

Delivery headers

X-Webhook-Timestamp: 1788652800
X-Webhook-Signature: <base64>
X-Webhook-Payload-Version: 2

{
  "code": 200,
  "msg": "success",
  "request_id": "whd_01k3m8x9q2z4v7n5p6r8s0t1w3",
  "data": { "taskId": "job_01k3…", "state": "succeeded", "settled": true, … }
}

契約

先に押さえておく項目

非同期メディア API:タスク、Webhook、再試行 — 先に押さえておく項目
フィールド / ヘッダー値・上限
Idempotency-Key24 h · scoped to one API key
X-Webhook-Signaturebase64(HMAC-SHA256)
signed stringtaskId.timestamp.sha256(raw_body)
X-Webhook-Payload-Version2 for new integrations

横にスワイプして全項目を確認できます。

導入

最初の呼び出しから本番まで

  1. 01冪等キーを生成して保存し、それを付けて createTask を送ります。callBackUrl はポート 80 か 443 の HTTPS エンドポイントにします。
  2. 02呼び出し元へ応答する前に data.taskId をデータベースへ書きます。202 はそれが最初に現れる唯一の場所です。
  3. 03コールバックを受け、生の本文で署名を検証し、request_id で重複排除し、レコードを更新します。
  4. 04補助として指数バックオフの recordInfo を使い、署名付き URL が切れる前に output.assets を自社ストレージへ複製します。

向き不向き

この方法が向いている場面

こんなときに選ぶ

  • メディアを生成するもの全般。画像、動画、音声のエンドポイントは構造上すべて非同期です。
  • 何を送り、いくらかかる見込みで、どう終わったかの永続的な記録が必要な場合。

こんなときは別の方法へ

  • テキストモデルを呼ぶ場合。あちらはリクエスト内でストリームし、実測使用量で精算します。
  • ブラウザにジョブを持たせたい場合。キーはサーバーのものであり、タスクレコードも同じです。

裏付け

自分で確認できること

すべての応答が request_id を持ちます

/api/v1 の全操作で封筒は {code, msg, data, request_id} です。ペイロード版 2 では配信の request_id が再送をまたいで不変なので、そのまま重複排除キーになります。

保留はレコードに見えています

settled: false の cost が保留中の見積額、settled: true が最終請求です。成功時に保留額を超えることはなく、失敗や期限切れでは全額解放されます。

失敗の語彙は閉じています

errorCode は九つの安定値のいずれかです。invalid_request、unsupported_combination、content_rejected、rate_limited、upstream_unavailable、generation_failed、timeout、invalid_asset、upstream_failed。それ以外は upstream_failed として扱ってください。

公開前に

確認しておくプラットフォームの挙動

  • 冪等キーはプロセスのメモリではなくタスクと一緒に保存します。
  • コールバック処理を冪等にします。配信は設計上、再試行されます。
  • ポーリングに期限を設け、期限を超えたタスクに警告を出します。
  • expiresAt より前に成果物を複製し、署名付き URL をキャッシュせず取り直します。

よくある質問

このページでよく聞かれること

同じ Idempotency-Key で同じリクエストを再送します。24 時間以内かつ同じ API キーなら、二重の保留なしに元のタスクが返ります。タイムアウト後に新しい本文を送ってはいけません。それは別に課金される新しいタスクです。

次に

デプロイをまたいで生き残るタスクフローを作る

長時間のメディアジョブをサーバーから送り、確実に終わらせる。冪等キー、署名付きコールバック、上限付きポーリング、五状態の状態機械。

関連ガイド