Skip to content

وظایف و وب‌هوک‌ها ​

تولید رسانه (ویدئو، تصویر، موسیقی) ناهمگام است: شما یک وظیفه (job) ایجاد می‌کنید و پس از آماده‌شدن، نتیجه را دریافت می‌کنید. برای آگاهی از پایان وظیفه دو روش وجود دارد: پرس‌وجو (polling) و وب‌هوک (push). در ادامه، ساختار وظیفه و روش دریافت مطمئن نتیجه را توضیح می‌دهیم.

چرخهٔ عمر وظیفه ​

POST /media/generate  →  status: processing  ──►  done   (result_url آماده است)
        │                                     └─►  failed (error، هزینه بازگردانده می‌شود)
        └── (اختیاری) callback_url → وب‌هوک push هنگام done/failed
  1. ایجاد. درخواست POST /media/generate را با مدل و پارامترها ارسال کنید؛ پاسخ شامل id و پیوندی برای پرس‌وجو است:
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) مقداردهی می‌شود؛ فایل برای مدت محدودی نگه‌داری می‌شود (retention_days از GET /media/models)، بنابراین برای حفظ آن، فایل را دانلود کنید.

قالب پاسخ GET /media/job/:id (و بدنهٔ وب‌هوک، در ادامه ببینید) یکسان است:

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)؛ در غیر این صورت null.
  • results — همهٔ فایل‌های وظیفه: [{ "index": 1, "name": "نتیجهٔ ۱", "ext": "png", "url": "…" }] (در وظایف 3D شامل مدل و پیش‌نمایش است؛ در جداسازی آهنگ شامل قطعه‌ها، مانند «وکال» و «ساز»).
  • cost_usd — هزینهٔ واقعی (پس از انجام کار کسر می‌شود؛ تولیدهای ناموفق هزینه ندارند و مبلغ رزروشده بازگردانده می‌شود).
  • error — متن خطا در وضعیت failed؛ در غیر این صورت null.

روش ۱. پرس‌وجو (polling) ​

ساده‌ترین روش این است که به‌طور دوره‌ای GET /media/job/:id را درخواست کنید تا status به done یا failed تغییر کند:

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

هر ۲ تا ۳ ثانیه یک‌بار پرس‌وجو کنید. پرس‌وجو همیشه کار می‌کند و به نشانی عمومی نیاز ندارد؛ برای اسکریپت‌ها، نوت‌بوک‌ها و CI مناسب است.

روش ۲. وب‌هوک‌ها (push) ​

برای جلوگیری از polling، callback_url را در POST /media/generate ارسال کنید تا پس از تکمیل کار، خودمان یک درخواست 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 بلافاصله هنگام ایجاد کار بررسی می‌شود: اگر معتبر نباشد (فاقد https باشد یا به آدرس خصوصی/محلی اشاره کند)، درخواست با خطای 400 و دلیل آن برگردانده می‌شود ({ "error": "callback_url رد شد: …", "code": "invalid_callback" }) و کار ایجاد نمی‌شود.

بدنه وب‌هوک ​

وقتی وضعیت کار به done/failed برسد، یک درخواست POST با همان بدنه GET /media/job/:id به callback_url شما ارسال می‌شود؛ با این تفاوت که فیلد event نیز اضافه می‌شود:

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 بدنه خام درخواست با کلیدی برابر با secret شماست. آن را بررسی کنید تا مطمئن شوید درخواست از طرف ما ارسال شده و جعلی نیست:

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));
}

HMAC را از بایت‌هایی دقیقاً مطابق داده دریافتی محاسبه کنید و این کار را پیش از تجزیه JSON انجام دهید؛ در غیر این صورت امضا مطابقت نخواهد داشت.

گیرنده آماده در ۱۰ خط ​

یک سرور عملی که وب‌هوک را دریافت می‌کند، امضا را بررسی می‌کند و پاسخ 2xx می‌دهد. Secret همان مقداری است که در 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 باشد. برای توسعه روی لپ‌تاپ، یک تونل ایجاد کنید؛ برای مثال 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 کافی است.