Skip to content

タスクとWebhook ​

メディア生成(動画、画像、音楽)は非同期です。タスク(job)を作成し、完了したら結果を取得します。タスクの完了を確認する方法は、ポーリング(polling)とWebhook(push)の2つです。以下では、タスクの仕組みと、結果を確実に取得する方法を説明します。

タスクのライフサイクル ​

POST /media/generate  →  status: processing  ──►  done   (result_url が利用可能)
        │                                     └─►  failed (error、料金は返金)
        └── (任意) callback_url → done/failed 時にプッシュWebhook
  1. 作成。 モデルとパラメータを指定して POST /media/generate を送信すると、id とポーリング用URLが返されます。
json
{ "id": "beefb531-…", "model": "image/nano-banana-2", "status": "processing",
  "poll": "https://nordrouter.com/media/job/beefb531-…" }
  1. 待機。 タスクは processing から done(または failed)へ進みます。画像は数秒、動画や音楽は数十秒から数分かかります。

  2. 結果。 完了したタスクには result_url(nordrouter.com 上のURL)が設定されます。ファイルは限られた期間(GET /media/models の retention_days)だけ保存されるため、保管する場合はダウンロードしてください。

GET /media/job/:id のレスポンス形式(およびWebhookの本文。後述)は共通です。

json
{ "id": "beefb531-…", "status": "done",
  "result_url": "https://nordrouter.com/media/file/beefb531-…",
  "cost_usd": 0.052, "error": null }
  • status — processing | done | failed。
  • result_url — done の場合のみ設定され、それ以外は null。
  • result_url_2 — 音楽モデル(Suno)の2つ目のトラック。それ以外は null。
  • results — タスクのすべてのファイル: [{ "index": 1, "name": "結果 1", "ext": "png", "url": "…" }](3Dの場合はモデルとプレビュー、トラック分離の場合は「ボーカル」「インストゥルメンタル」など)。
  • cost_usd — 実際の料金(生成完了後に請求されます。失敗した生成には課金されず、予約分は返金されます)。
  • error — failed の場合はエラーテキスト、それ以外は null。

方法1. ポーリング(polling) ​

最も簡単な方法は、status が done / failed になるまで GET /media/job/:id を定期的にリクエストすることです。

bash
curl https://nordrouter.com/media/job/beefb531-…

2〜3秒間隔でポーリングしてください。ポーリングは常に利用でき、公開アドレスも不要なため、スクリプト、ノートブック、CIに適しています。

方法 2. Webhook(プッシュ) ​

ポーリングを避けるには、POST /media/generate に callback_url を指定してください。タスクが完了すると、こちらから指定先に POST を送信します。

bash
curl https://nordrouter.com/media/generate \
  -H "Authorization: Bearer sk-nr-YOUR-KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "image/nano-banana-2",
    "input": { "prompt": "…" },
    "callback_url": "https://your-server.com/webhooks/nordrouter",
    "callback_secret": "あなたの任意の文字列"
  }'

callback_url が受け付けられると、作成時のレスポンスに "webhook": "registered" が含まれます。callback_url はタスク作成時に直ちに検証されます。無効な URL(https ではない、またはプライベート/ローカルアドレス)の場合、リクエストは理由とともに 400 を返し({ "error": "callback_url отклонён: …", "code": "invalid_callback" })、タスクは作成されません。

Webhook の本文 ​

タスクが done/failed になると、GET /media/job/:id と同じ本文に event フィールドを加えた POST が callback_url に送信されます。

json
{ "event": "media.completed", "id": "beefb531-…", "status": "done",
  "result_url": "https://nordrouter.com/media/file/beefb531-…",
  "cost_usd": 0.052, "error": null }
  • event は media.completed(成功)または media.failed(エラー)です。

署名の検証 ​

callback_secret を設定すると、X-NR-Signature ヘッダーに sha256=<hmac> が含まれます。これは、キーにシークレットを使ってリクエスト本文の生データから計算した HMAC-SHA256 です。リクエストがこちらから送信されたものであり、偽装ではないことを確認するため、署名を検証してください。

python
import hmac, hashlib

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header)  # 定数時間比較
javascript
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}

JSON をパースする前に、受信したとおりのバイト列から HMAC を計算してください。そうしないと署名が一致しません。

10 行で使える受信サーバー ​

Webhook を受信し、署名を検証して 2xx を返すサーバーの例です。シークレットには callback_secret に指定したものと同じ値を使います。

Node.js / Express:

javascript
import express from "express";
import crypto from "node:crypto";

const SECRET = "あなたの任意の文字列";           // = あなたのcallback_secret
const app = express();

app.post("/webhooks/nordrouter",
  express.raw({ type: "application/json" }),   // 署名検証には生の本文が必須
  (req, res) => {
    const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
    const got = req.headers["x-nr-signature"] || "";
    if (expected.length !== got.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got)))
      return res.sendStatus(401);             // 署名が一致しないため、こちらからのリクエストではありません
    const job = JSON.parse(req.body);
    if (job.event === "media.completed") console.log("完了:", job.result_url, "$" + job.cost_usd);
    res.sendStatus(200);                       // 2xx を返さないと再試行されます
  });

app.listen(3000);

Python / FastAPI:

python
import hmac, hashlib
from fastapi import FastAPI, Request, Response

SECRET = b"あなたの任意の文字列"                  # = あなたのcallback_secret
app = FastAPI()

@app.post("/webhooks/nordrouter")
async def hook(request: Request):
    raw = await request.body()                 # 署名検証には生バイト列が必須
    expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.headers.get("X-NR-Signature", "")):
        return Response(status_code=401)       # 署名が一致しないため、こちらからのリクエストではありません
    job = await request.json()
    if job["event"] == "media.completed":
        print("完了:", job["result_url"], "$", job["cost_usd"])
    return Response(status_code=200)           # 2xx を返さないと再試行されます

ローカルで確認する

callback_url には公開された https URL が必要です。ノート PC で開発する場合は、トンネルを起動します。たとえば ngrok http 3000 を実行し、発行された https URL を callback_url に指定してください。

配信と信頼性 ​

  • 最大 4 回、間隔を延ばしながら再試行します(約 3 → 6 → 9 秒)。各試行のタイムアウトは 15 秒です。2xx を返さないと再試行されます。
  • リダイレクトは追跡されません。callback_url に直接応答してください。
  • 配信は best-effort です。4 回すべての試行に失敗した場合(サーバーが停止していた場合など)、Webhook は後から再送されません。そのため、次の点に注意してください。

Webhook はポーリングの代わりにはなりません

Webhook は通知を早めるものであり、配信を保証するものではありません。予備手段として GET /media/job/:id を使えるようにしておいてください。妥当な時間内にプッシュ通知が届かなければ、自分でステータスをポーリングしてください。冪等性は受信側で確保してください。同じタスクが Webhook で届き、ポーリングでも検出されることがあるため、id を基準に処理してください。

callback_url の要件 ​

  • https:// のみ。
  • 公開アドレスのみ。ローカルおよびプライベートアドレス(localhost、*.local、10.x、172.16–31.x、192.168.x、127.x、169.254.x、100.64–127.x、IPv6 の loopback/ULA/link-local)は、SSRF 対策のため拒否されます。DNS rebinding 対策として、送信のたびにアドレスを再検証します。

どちらを選ぶか ​

ポーリングWebhook
公開 https アドレス不要必要
受信までの遅延ポーリング間隔までほぼ即時
保証自分で制御best-effort + ポーリングを予備として使用
適した用途スクリプト、CI、ノート PCサーバー、本番バックエンド

推奨: バックエンドでは Webhook と保険としてのポーリングを併用し、スクリプトやノート PC ではポーリングのみを使います。