任務與 Webhook
媒體生成(影片、圖片、音樂)採用非同步方式:您建立一個任務(job),並在完成後取得結果。有兩種方式可得知任務已完成:輪詢(polling)與 Webhook(push)。以下說明任務的運作方式,以及如何可靠地取得結果。
任務生命週期
POST /media/generate → status: processing ──► done (result_url 已就緒)
│ └─► failed (error,款項已退回)
└── (選用) callback_url → 在 done/failed 時推送 Webhook- 建立。 使用模型與參數呼叫
POST /media/generate,會回傳id與輪詢連結:
{ "id": "beefb531-…", "model": "image/nano-banana-2", "status": "processing",
"poll": "https://nordrouter.com/media/job/beefb531-…" }等待。 任務狀態會從
processing變為done(或failed)。圖片需要幾秒,影片與音樂則需要數十秒到幾分鐘。結果。 已完成的任務會填入
result_url(指向nordrouter.com的連結);檔案僅保留有限時間(見GET /media/models的retention_days),請下載保存。
GET /media/job/:id 的回應格式(以及 Webhook 的本文,詳見下方)相同:
{ "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)的第二首曲目,否則為null。results— 任務的所有檔案:[{ "index": 1, "name": "Результат 1", "ext": "png", "url": "…" }](3D 任務包含模型與預覽,音軌分離任務則包含「人聲」、「伴奏」等音軌)。cost_usd— 實際費用(依實際結果扣款;生成失敗不計費,預留款項會退回)。error— 在failed時顯示錯誤訊息,否則為null。
方式 1. 輪詢(polling)
最簡單的方式是定期請求 GET /media/job/:id,直到 status 變為 done/failed:
curl https://nordrouter.com/media/job/beefb531-…每 2–3 秒輪詢一次。輪詢隨時可用,且不需要公開網址,適合用於指令碼、筆記本與 CI。
方法 2. Webhook(推送)
若不想輪詢,請在 POST /media/generate 中傳入 callback_url,任務完成時,我們會主動向您的網址傳送 POST:
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:若不符合要求(非 https、私人/本機位址),請求會回傳 400 並附上原因({ "error": "callback_url отклонён: …", "code": "invalid_callback" }),且不會建立任務。
Webhook 本文
當任務進入 done/failed 狀態時,您的 callback_url 會收到 POST,本文與 GET /media/job/:id 的回應相同,並多出 event 欄位:
{ "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。請驗證簽章,以確認請求來自我們,而非偽造:
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) # 固定時間比較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:
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:
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 網址。在筆電上開發時,請建立通道,例如執行 ngrok http 3000,再將取得的 https 網址傳入 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 回送/ULA/連結本機位址)均會遭到拒絕,以防範 SSRF。每次傳送前也會重新檢查位址,以防範 DNS 重新綁定攻擊。
如何選擇
| 輪詢 | Webhook | |
|---|---|---|
公開的 https 網址 | 不需要 | 需要 |
| 接收延遲 | 最長為輪詢間隔 | 幾乎即時 |
| 可靠性保證 | 由您掌控 | best-effort + 輪詢備援 |
| 適用場景 | 指令碼、CI、筆電 | 伺服器、正式環境後端 |
**建議:**後端使用 Webhook,並以輪詢作為備援;指令碼/筆電則直接使用輪詢。