وظایف و وبهوکها
تولید رسانه (ویدئو، تصویر، موسیقی) ناهمگام است: شما یک وظیفه (job) ایجاد میکنید و پس از آمادهشدن، نتیجه را دریافت میکنید. برای آگاهی از پایان وظیفه دو روش وجود دارد: پرسوجو (polling) و وبهوک (push). در ادامه، ساختار وظیفه و روش دریافت مطمئن نتیجه را توضیح میدهیم.
چرخهٔ عمر وظیفه
POST /media/generate → status: processing ──► done (result_url آماده است)
│ └─► failed (error، هزینه بازگردانده میشود)
└── (اختیاری) callback_url → وبهوک push هنگام done/failed- ایجاد. درخواست
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) مقداردهی میشود؛ فایل برای مدت محدودی نگهداری میشود (retention_daysازGET /media/models)، بنابراین برای حفظ آن، فایل را دانلود کنید.
قالب پاسخ GET /media/job/:id (و بدنهٔ وبهوک، در ادامه ببینید) یکسان است:
{ "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": "نتیجهٔ ۱", "ext": "png", "url": "…" }](در وظایف 3D شامل مدل و پیشنمایش است؛ در جداسازی آهنگ شامل قطعهها، مانند «وکال» و «ساز»).cost_usd— هزینهٔ واقعی (پس از انجام کار کسر میشود؛ تولیدهای ناموفق هزینه ندارند و مبلغ رزروشده بازگردانده میشود).error— متن خطا در وضعیتfailed؛ در غیر این صورتnull.
روش ۱. پرسوجو (polling)
سادهترین روش این است که بهطور دورهای GET /media/job/:id را درخواست کنید تا status به done یا failed تغییر کند:
curl https://nordrouter.com/media/job/beefb531-…هر ۲ تا ۳ ثانیه یکبار پرسوجو کنید. پرسوجو همیشه کار میکند و به نشانی عمومی نیاز ندارد؛ برای اسکریپتها، نوتبوکها و CI مناسب است.
روش ۲. وبهوکها (push)
برای جلوگیری از polling، callback_url را در POST /media/generate ارسال کنید تا پس از تکمیل کار، خودمان یک درخواست 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" }) و کار ایجاد نمیشود.
بدنه وبهوک
وقتی وضعیت کار به done/failed برسد، یک درخواست POST با همان بدنه GET /media/job/:id به callback_url شما ارسال میشود؛ با این تفاوت که فیلد 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 بدنه خام درخواست با کلیدی برابر با secret شماست. آن را بررسی کنید تا مطمئن شوید درخواست از طرف ما ارسال شده و جعلی نیست:
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));
}HMAC را از بایتهایی دقیقاً مطابق داده دریافتی محاسبه کنید و این کار را پیش از تجزیه JSON انجام دهید؛ در غیر این صورت امضا مطابقت نخواهد داشت.
گیرنده آماده در ۱۰ خط
یک سرور عملی که وبهوک را دریافت میکند، امضا را بررسی میکند و پاسخ 2xx میدهد. Secret همان مقداری است که در 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 ارسال کنید.
ارسال و قابلیت اطمینان
- حداکثر ۴ تلاش با فاصلههای افزایشی (حدوداً ۳ → ۶ → ۹ ثانیه)، مهلت زمانی هر تلاش ۱۵ ثانیه است. پاسخ
2xxبدهید؛ در غیر این صورت ارسال تکرار میشود. - Redirectها دنبال نمیشوند؛ مستقیماً در
callback_urlپاسخ دهید. - ارسال بهصورت best-effort انجام میشود: اگر هر ۴ تلاش ناموفق باشند (مثلاً سرور شما از دسترس خارج باشد)، وبهوک بعداً دوباره ارسال نمیشود. بنابراین:
وبهوکها جایگزین polling نیستند
وبهوک باعث تسریع دریافت نتیجه میشود، اما تضمینی ارائه نمیکند. GET /media/job/:id را بهعنوان مسیر پشتیبان نگه دارید: اگر push در مدت زمان معقولی دریافت نشد، وضعیت را خودتان بررسی کنید. ایدمپوتنسی بر عهده شماست: ممکن است یک کار هم بهصورت وبهوک دریافت شود و هم در polling مشاهده شود؛ از id استفاده کنید.
الزامات callback_url
- فقط
https://. - فقط آدرس عمومی. آدرسهای محلی و خصوصی (
localhost،*.local،10.x،172.16–31.x،192.168.x،127.x،169.254.x،100.64–127.x، loopback/ULA/link-local در IPv6) رد میشوند؛ این کار برای محافظت در برابر SSRF است. آدرس پیش از هر ارسال نیز دوباره بررسی میشود تا از DNS rebinding جلوگیری شود.
کدام گزینه را انتخاب کنیم
| polling | وبهوک | |
|---|---|---|
آدرس عمومی https | لازم نیست | لازم است |
| تأخیر دریافت | حداکثر بهاندازه فاصله polling | تقریباً فوری |
| تضمین | تحت کنترل شماست | best-effort + polling بهعنوان مسیر پشتیبان |
| مناسب برای | اسکریپتها، CI، لپتاپ | سرورها، بکاندهای production |
توصیه: در بکاند، وبهوک را همراه با polling بهعنوان پشتیبان استفاده کنید؛ در اسکریپت یا لپتاپ، فقط polling کافی است.