Skip to content

Tác vụ và webhook ​

Việc tạo media (video, hình ảnh, nhạc) là bất đồng bộ: bạn tạo một tác vụ (job) rồi lấy kết quả khi tác vụ hoàn tất. Có hai cách để biết tác vụ đã hoàn tất: thăm dò (polling) và webhook (push). Dưới đây là cách tác vụ hoạt động và cách nhận kết quả một cách đáng tin cậy.

Vòng đời của tác vụ ​

POST /media/generate  →  status: processing  ──►  done   (result_url đã sẵn sàng)
        │                                     └─►  failed (error, tiền được hoàn lại)
        └── (tùy chọn) callback_url → webhook push khi done/failed
  1. Tạo tác vụ. POST /media/generate với model và các tham số — trả về id và liên kết để thăm dò:
json
{ "id": "beefb531-…", "model": "image/nano-banana-2", "status": "processing",
  "poll": "https://nordrouter.com/media/job/beefb531-…" }
  1. Chờ xử lý. Tác vụ chuyển từ processing → done (hoặc failed). Hình ảnh mất vài giây, video và nhạc mất từ vài chục giây đến vài phút.

  2. Kết quả. Tác vụ đã hoàn tất sẽ có result_url (liên kết đến nordrouter.com); tệp được lưu trữ trong thời gian giới hạn (retention_days từ GET /media/models) — hãy tải xuống để lưu lại.

Định dạng phản hồi của GET /media/job/:id (và nội dung webhook — xem bên dưới) giống nhau:

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 — chỉ được điền khi done, nếu không sẽ là null.
  • result_url_2 — bản nhạc thứ hai đối với các model nhạc (Suno), nếu không sẽ là null.
  • results — tất cả tệp của tác vụ: [{ "index": 1, "name": "Kết quả 1", "ext": "png", "url": "…" }] (đối với 3D là model và bản xem trước, đối với tách bản nhạc là các track «Giọng hát», «Nhạc cụ»…).
  • cost_usd — chi phí thực tế (được tính theo thực tế; các lần tạo không thành công không bị tính phí — khoản tiền giữ chỗ sẽ được hoàn lại).
  • error — nội dung lỗi khi failed, nếu không sẽ là null.

Cách 1. Thăm dò (polling) ​

Cách đơn giản nhất là định kỳ gửi yêu cầu GET /media/job/:id cho đến khi status trở thành done/failed:

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

Hãy thăm dò sau mỗi 2–3 giây. Thăm dò luôn hoạt động và không yêu cầu địa chỉ công khai — phù hợp với script, notebook và CI.

Phương thức 2. Webhook (push) ​

Để không phải polling, hãy truyền callback_url trong POST /media/generate. Khi tác vụ hoàn tất, chúng tôi sẽ gửi POST đến địa chỉ của bạn:

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": "любая-ваша-строка"
  }'

Nếu callback_url được chấp nhận, phản hồi khi tạo tác vụ sẽ có "webhook": "registered". callback_url được kiểm tra ngay khi tạo tác vụ: nếu URL không hợp lệ (không dùng https, là địa chỉ riêng tư/cục bộ), yêu cầu sẽ trả về 400 kèm lý do ({ "error": "callback_url отклонён: …", "code": "invalid_callback" }), và tác vụ sẽ không được tạo.

Nội dung webhook ​

Khi tác vụ chuyển sang trạng thái done/failed, callback_url của bạn sẽ nhận được yêu cầu POST với nội dung giống như GET /media/job/:id, cùng với trường event:

json
{ "event": "media.completed", "id": "beefb531-…", "status": "done",
  "result_url": "https://nordrouter.com/media/file/beefb531-…",
  "cost_usd": 0.052, "error": null }
  • event là media.completed (thành công) hoặc media.failed (lỗi).

Xác minh chữ ký ​

Nếu bạn đã đặt callback_secret, header X-NR-Signature sẽ chứa sha256=<hmac> — HMAC-SHA256 của nội dung thô của yêu cầu, với khóa là bí mật của bạn. Hãy xác minh chữ ký để đảm bảo yêu cầu đến từ chúng tôi, không phải giả mạo:

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)  # so sánh trong thời gian hằng định
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));
}

Tính HMAC từ đúng các byte nhận được, trước khi phân tích JSON, nếu không chữ ký sẽ không khớp.

Bộ nhận webhook sẵn dùng chỉ với 10 dòng ​

Máy chủ hoạt động, nhận webhook, xác minh chữ ký và trả về 2xx. Bí mật phải giống với giá trị bạn đã truyền trong 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, иначе будет повтор

Kiểm tra cục bộ

callback_url phải là địa chỉ https công khai. Để phát triển trên máy tính cá nhân, hãy thiết lập một tunnel, chẳng hạn ngrok http 3000, rồi truyền địa chỉ https được cấp làm callback_url.

Gửi webhook và độ tin cậy ​

  • Tối đa 4 lần thử với khoảng chờ tăng dần (≈3 → 6 → 9 giây), mỗi lần timeout sau 15 giây. Hãy trả về 2xx, nếu không webhook sẽ được gửi lại.
  • Không theo redirect — hãy phản hồi trực tiếp tại callback_url.
  • Việc gửi webhook theo nguyên tắc best-effort: nếu cả 4 lần thử đều thất bại (máy chủ của bạn không hoạt động), webhook sẽ không được gửi lại sau đó. Vì vậy:

Webhook không thay thế polling

Webhook giúp nhận thông báo nhanh hơn, nhưng không đảm bảo chắc chắn. Hãy giữ GET /media/job/:id làm phương án dự phòng: nếu không nhận được push sau một khoảng thời gian hợp lý, hãy tự polling trạng thái. Bạn cần xử lý tính idempotent ở phía mình: cùng một tác vụ có thể được thông báo qua webhook và được phát hiện khi polling — hãy dựa vào id.

Yêu cầu đối với callback_url ​

  • Chỉ chấp nhận https://.
  • Chỉ chấp nhận địa chỉ công khai. Các địa chỉ cục bộ và riêng tư (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) bị từ chối để bảo vệ khỏi SSRF. Địa chỉ được kiểm tra lại trước mỗi lần gửi để phòng chống DNS rebinding.

Nên chọn phương thức nào ​

PollingWebhook
Địa chỉ https công khaikhông cầncần
Độ trễ nhận kết quảtối đa bằng chu kỳ pollinggần như tức thì
Mức đảm bảobạn kiểm soátbest-effort + polling dự phòng
Phù hợp vớiscript, CI, máy tính cá nhânmáy chủ, backend production

Khuyến nghị: với backend, dùng webhook + polling dự phòng; với script/máy tính cá nhân, chỉ cần polling.