NezhaGateNezhaGate
API Documentation

Webhook:タスク終了時に自動で通知

動画と画像の生成は非同期です。リクエストを送るとタスク ID が返り、結果は少し後で届きます。ポーリングを繰り返さなくても、タスクが終わった時点で結果をご指定のサーバーへ送信します。残高が少なくなったときにもお知らせできます。

どんな場面で使えるか、3 ステップの設定方法:Webhook ガイドを読む →

イベントを受け取る 2 つの方法

  1. アカウントの既定の送信先:コンソールの「設定 → Webhook」で送信先 URL を入力し、受け取りたいイベントにチェックを入れます。以降、API から送信したすべてのタスクについて、終了時にチェックした内容に従って通知します。 Webhook 設定を開く →
  2. リクエストごとの callback_url:動画・画像タスクを送信するときに callback_url フィールドを追加します。そのタスクの結果(成功・失敗とも)はこの URL にだけ送信され、「設定」のチェックには左右されません。

callback_url は、インターネットからアクセスできる http(s) の URL(2,000 文字以内)にしてください。callBackUrl という書き方も使えます。どちらの方法でも同じ署名シークレットを使います。シークレットは「設定 → Webhook」で確認できます。

curl · callback_url を付けてタスクを送信する例:
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 です。

リクエスト本文の例

動画の生成が完了した場合:

video.completed
{
  "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 が入ります。料金は全額返金済みです:

image.failed
{
  "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" }
  }
}
balance.low
{
  "id": "evt_1a2b3c4d5e6f708192a3b4c5",
  "object": "event",
  "type": "balance.low",
  "created_at": 1791203000,
  "data": { "balance_credits": 412.5, "threshold_credits": 500 }
}

署名の検証

イベントは Standard Webhooks 仕様に沿って署名しています(OpenAI の Webhook も同じ仕様を採用しています)。検証の手順:

  1. リクエストヘッダーの webhook-id、webhook-timestamp、webhook-signature と、生のリクエスト本文を取得します(JSON としてパースしてから再シリアライズしないでください)。
  2. 署名対象の文字列を組み立てます:webhook-id + "." + webhook-timestamp + "." + 生の本文。
  3. 鍵は whsec_ の後ろの部分を Base64 デコードしたものです。これで HMAC-SHA256 を計算し、結果を Base64 エンコードします。
  4. webhook-signature には複数の署名が含まれる場合があります(スペース区切りで、それぞれ v1, で始まります)。いずれか 1 つが一致すれば検証成功です。比較には定数時間の比較関数を使ってください。
  5. リプレイ攻撃を防ぐため、タイムスタンプが現在時刻から 5 分を超えてずれているリクエストは拒否してください。
Python · Flask
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 "", 204
Node.js · Express
import 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 イベントを送信し、サーバーが返したステータスコードと応答時間を表示します。

Webhook 設定を開く