動画と画像の生成は非同期です。リクエストを送るとタスク ID が返り、結果は少し後で届きます。ポーリングを繰り返さなくても、タスクが終わった時点で結果をご指定のサーバーへ送信します。残高が少なくなったときにもお知らせできます。
どんな場面で使えるか、3 ステップの設定方法:Webhook ガイドを読む →
イベントを受け取る 2 つの方法
- アカウントの既定の送信先:コンソールの「設定 → Webhook」で送信先 URL を入力し、受け取りたいイベントにチェックを入れます。以降、API から送信したすべてのタスクについて、終了時にチェックした内容に従って通知します。 Webhook 設定を開く →
- リクエストごとの callback_url:動画・画像タスクを送信するときに callback_url フィールドを追加します。そのタスクの結果(成功・失敗とも)はこの URL にだけ送信され、「設定」のチェックには左右されません。
callback_url は、インターネットからアクセスできる http(s) の URL(2,000 文字以内)にしてください。callBackUrl という書き方も使えます。どちらの方法でも同じ署名シークレットを使います。シークレットは「設定 → Webhook」で確認できます。
curl https://nezhagate.com/v1/videos/generations -H 'Authorization: Bearer YOUR_API_KEY' -H 'Content-Type: application/json' -d '{"model": "seedance-2.5", "prompt": "a paper boat drifting down a rainy street", "duration": 5, "callback_url": "https://example.com/webhooks/nezhagate"}'イベント
| type | 送信タイミング | data の内容 |
|---|---|---|
video.completed | 動画の生成が完了したとき | タスク照会エンドポイント(GET /v1/videos/jobs/{id}、GET /v1/images/jobs/{id})が返すものとまったく同じ JSON |
video.failed | 動画の生成に失敗したとき(全額返金済み) | |
image.completed | 画像の生成が完了したとき | |
image.failed | 画像の生成に失敗したとき(全額返金済み) | |
balance.low | 残高が設定したクレジット数を下回ったとき(下回るたびに 1 回だけ送信。チャージ後に再び有効になります) | balance_credits(現在の残高、クレジット単位)と threshold_credits(設定した通知のしきい値) |
webhook.test | 「設定」で「テストイベントを送信」をクリックしたとき | 受信と署名検証ができることを確認するための短いメッセージ |
リクエストの形式
ご指定の URL に、JSON 本文の POST リクエストを次のヘッダー付きで送信します:
webhook-id | イベント ID。再試行しても変わらないので、重複排除に使ってください。 |
webhook-timestamp | この送信を行った時刻(Unix 秒)。送信のたびに更新されます。 |
webhook-signature | 署名。v1, の後に Base64 エンコードした HMAC-SHA256 が続きます。 |
Content-Type | 常に application/json です。 |
リクエスト本文の例
動画の生成が完了した場合:
{
"id": "evt_5f0c3d2e8a9b4c71d2e3f405",
"object": "event",
"type": "video.completed",
"created_at": 1791200000,
"data": {
"id": "img_3f9a...c2",
"object": "video.generation.job",
"status": "succeeded",
"created": 1791199400,
"model": "seedance-2.5",
"data": [{ "url": "https://img.nezhagate.com/i/9f86d081a8....mp4" }],
"usage": { "clips": 1, "model": "seedance-2.5" }
}
}失敗した場合は、タスク照会エンドポイントと同じく data に error が入ります。料金は全額返金済みです:
{
"id": "evt_8b1e47c09d3a2f6e5b7c1d90",
"object": "event",
"type": "image.failed",
"created_at": 1791200120,
"data": {
"id": "img_7c21...e4",
"object": "image.generation.job",
"status": "failed",
"created": 1791200050,
"model": "gpt-image-2",
"error": { "message": "...", "type": "upstream_error", "code": "upstream_error" }
}
}{
"id": "evt_1a2b3c4d5e6f708192a3b4c5",
"object": "event",
"type": "balance.low",
"created_at": 1791203000,
"data": { "balance_credits": 412.5, "threshold_credits": 500 }
}署名の検証
イベントは Standard Webhooks 仕様に沿って署名しています(OpenAI の Webhook も同じ仕様を採用しています)。検証の手順:
- リクエストヘッダーの webhook-id、webhook-timestamp、webhook-signature と、生のリクエスト本文を取得します(JSON としてパースしてから再シリアライズしないでください)。
- 署名対象の文字列を組み立てます:webhook-id + "." + webhook-timestamp + "." + 生の本文。
- 鍵は whsec_ の後ろの部分を Base64 デコードしたものです。これで HMAC-SHA256 を計算し、結果を Base64 エンコードします。
- webhook-signature には複数の署名が含まれる場合があります(スペース区切りで、それぞれ v1, で始まります)。いずれか 1 つが一致すれば検証成功です。比較には定数時間の比較関数を使ってください。
- リプレイ攻撃を防ぐため、タイムスタンプが現在時刻から 5 分を超えてずれているリクエストは拒否してください。
import base64, hashlib, hmac, time
from flask import Flask, abort, request
SECRET = "whsec_..." # Settings -> Webhook -> Signing secret
KEY = base64.b64decode(SECRET[len("whsec_"):])
def verify(headers, body: bytes) -> bool:
msg_id = headers.get("webhook-id", "")
ts = headers.get("webhook-timestamp", "")
if not msg_id or not ts.isdigit() or abs(time.time() - int(ts)) > 300:
return False
signed = f"{msg_id}.{ts}.".encode() + body
expected = base64.b64encode(hmac.new(KEY, signed, hashlib.sha256).digest()).decode()
return any(sig.startswith("v1,") and hmac.compare_digest(sig[3:], expected)
for sig in headers.get("webhook-signature", "").split())
app = Flask(__name__)
@app.post("/webhooks/nezhagate")
def webhook():
if not verify(request.headers, request.get_data()):
abort(401)
event = request.get_json()
if event["type"] == "video.completed":
print(event["data"]["id"], event["data"]["data"][0]["url"])
return "", 204import crypto from "node:crypto";
import express from "express";
const SECRET = "whsec_..."; // Settings -> Webhook -> Signing secret
const KEY = Buffer.from(SECRET.slice("whsec_".length), "base64");
const app = express();
app.post("/webhooks/nezhagate", express.raw({ type: "application/json" }), (req, res) => {
const id = req.header("webhook-id"), ts = req.header("webhook-timestamp");
if (!id || !ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(401);
const expected = Buffer.from(
crypto.createHmac("sha256", KEY).update(`${id}.${ts}.`).update(req.body).digest("base64"));
const ok = (req.header("webhook-signature") || "").split(" ").some((sig) => {
const got = Buffer.from(sig.slice(3));
return sig.startsWith("v1,") && got.length === expected.length && crypto.timingSafeEqual(got, expected);
});
if (!ok) return res.sendStatus(401);
const event = JSON.parse(req.body.toString("utf8"));
console.log(event.type, event.data.id);
res.sendStatus(204);
});
app.listen(3000);各言語向けの Standard Webhooks 公式ライブラリを使うこともできます(Python: pip install standardwebhooks、Node: npm install standardwebhooks)。
応答と再試行
- 10 秒以内に 2xx を返せば配信成功とみなします。レスポンス本文の内容は問いません。まず 2xx を返し、そのあとで処理してください。
- 2xx が返らない場合(タイムアウト、接続エラー、3xx、4xx、5xx)は、1 分後、5 分後、30 分後、2 時間後、6 時間後に 1 回ずつ再試行し、それでも届かなければ失敗として記録します。
- 同じイベントが複数回届くことがあり、届く順序も保証されません。webhook-id で重複を排除し、data.status を基準に判断してください。
- 「設定 → Webhook」に直近 30 日間の送信履歴と各回の結果が表示され、個別に再送信することもできます。
セキュリティ
- 送信先は公開アドレスに限ります。プライベート、ローカル、予約済みのアドレスはすべて拒否します。
- リダイレクトには従いません。3xx が返った場合は失敗として扱います。
- 署名シークレットは API Key と同じように厳重に保管してください。漏えいした場合は「設定」で再生成してください。古いシークレットはすぐに無効になります。
テスト
「設定 → Webhook」で URL を保存し、「テストイベントを送信」をクリックしてください。すぐに webhook.test イベントを送信し、サーバーが返したステータスコードと応答時間を表示します。