Tugas dan webhook
Pembuatan media (video, gambar, musik) bersifat asinkron: Anda membuat tugas (job) dan mengambil hasilnya saat sudah siap. Ada dua cara untuk mengetahui bahwa tugas telah selesai: polling dan webhook (push). Berikut struktur tugas dan cara mengambil hasilnya secara andal.
Siklus hidup tugas
POST /media/generate → status: processing ──► done (result_url siap)
│ └─► failed (error, dana dikembalikan)
└── (opsional) callback_url → webhook push saat done/failed- Pembuatan.
POST /media/generatedengan model dan parameter — mengembalikaniddan tautan untuk polling:
{ "id": "beefb531-…", "model": "image/nano-banana-2", "status": "processing",
"poll": "https://nordrouter.com/media/job/beefb531-…" }Menunggu. Tugas beralih dari
processing→done(ataufailed). Gambar memerlukan beberapa detik, sedangkan video dan musik memerlukan puluhan detik hingga beberapa menit.Hasil. Tugas yang selesai memiliki
result_url(tautan kenordrouter.com); file disimpan untuk waktu terbatas (retention_daysdariGET /media/models) — unduh untuk menyimpannya.
Format respons GET /media/job/:id (dan isi webhook — lihat di bawah) sama:
{ "id": "beefb531-…", "status": "done",
"result_url": "https://nordrouter.com/media/file/beefb531-…",
"cost_usd": 0.052, "error": null }status—processing|done|failed.result_url— hanya terisi saatdone, selain itunull.result_url_2— trek kedua pada model musik (Suno), selain itunull.results— semua file tugas:[{ "index": 1, "name": "Результат 1", "ext": "png", "url": "…" }](untuk 3D — model dan pratinjau, untuk pemisahan trek — trek “Vokal”, “Instrumental”…).cost_usd— biaya aktual (dibebankan berdasarkan hasil; pembuatan yang gagal tidak dikenai biaya — dana yang dicadangkan dikembalikan).error— teks kesalahan saatfailed, selain itunull.
Cara 1. Polling
Cara paling sederhana adalah meminta GET /media/job/:id secara berkala hingga status menjadi done/failed:
curl https://nordrouter.com/media/job/beefb531-…Lakukan polling setiap 2–3 detik. Polling selalu berfungsi dan tidak memerlukan alamat publik — cocok untuk skrip, notebook, dan CI.
Metode 2. Webhook (push)
Agar tidak perlu melakukan polling, kirim callback_url ke POST /media/generate, lalu kami akan mengirim POST ke alamat Anda saat tugas selesai:
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": "string-apa-saja"
}'Jika callback_url diterima, respons pembuatan tugas berisi "webhook": "registered". callback_url divalidasi langsung saat tugas dibuat: jika tidak memenuhi syarat (bukan https, alamat privat/lokal), permintaan akan mengembalikan 400 dengan alasannya ({ "error": "callback_url ditolak: …", "code": "invalid_callback" }), dan tugas tidak dibuat.
Isi webhook
Saat tugas berstatus done/failed, POST dengan isi yang sama seperti GET /media/job/:id, ditambah field event, akan dikirim ke callback_url Anda:
{ "event": "media.completed", "id": "beefb531-…", "status": "done",
"result_url": "https://nordrouter.com/media/file/beefb531-…",
"cost_usd": 0.052, "error": null }event—media.completed(berhasil) ataumedia.failed(gagal).
Memverifikasi tanda tangan
Jika Anda menetapkan callback_secret, header X-NR-Signature berisi sha256=<hmac> — HMAC-SHA256 dari isi mentah permintaan, dengan kunci berupa rahasia Anda. Verifikasi tanda tangan ini untuk memastikan permintaan berasal dari kami, bukan pemalsuan:
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) # perbandingan dengan waktu konstanimport 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));
}Hitung HMAC dari byte persis seperti diterima, sebelum mem-parsing JSON. Jika tidak, tanda tangan tidak akan cocok.
Penerima siap pakai dalam 10 baris
Server yang berfungsi untuk menerima webhook, memverifikasi tanda tangan, dan merespons dengan 2xx. Rahasianya sama dengan yang Anda kirim di callback_secret.
Node.js / Express:
import express from "express";
import crypto from "node:crypto";
const SECRET = "string-apa-saja"; // = callback_secret Anda
const app = express();
app.post("/webhooks/nordrouter",
express.raw({ type: "application/json" }), // isi mentah — wajib untuk tanda tangan
(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); // tanda tangan tidak cocok — bukan permintaan kami
const job = JSON.parse(req.body);
if (job.event === "media.completed") console.log("selesai:", job.result_url, "$" + job.cost_usd);
res.sendStatus(200); // balas 2xx, jika tidak, permintaan akan dicoba ulang
});
app.listen(3000);Python / FastAPI:
import hmac, hashlib
from fastapi import FastAPI, Request, Response
SECRET = b"string-apa-saja" # = callback_secret Anda
app = FastAPI()
@app.post("/webhooks/nordrouter")
async def hook(request: Request):
raw = await request.body() # byte mentah — wajib untuk tanda tangan
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) # tanda tangan tidak cocok — bukan permintaan kami
job = await request.json()
if job["event"] == "media.completed":
print("selesai:", job["result_url"], "$", job["cost_usd"])
return Response(status_code=200) # balas 2xx, jika tidak, permintaan akan dicoba ulangMenguji secara lokal
callback_url harus berupa alamat https publik. Untuk pengembangan di laptop, buat tunnel, misalnya ngrok http 3000, lalu gunakan alamat https yang diberikan sebagai callback_url.
Pengiriman dan keandalan
- Maksimal 4 percobaan dengan jeda yang semakin lama (≈3 → 6 → 9 dtk), batas waktu setiap percobaan adalah 15 dtk. Balas dengan
2xx, jika tidak, permintaan akan dicoba ulang. - Pengalihan tidak diikuti — tanggapi langsung di
callback_url. - Pengiriman bersifat best-effort: jika semua 4 percobaan gagal (server Anda tidak aktif), webhook tidak akan dikirim ulang nanti. Karena itu:
Webhook tidak menggantikan polling
Webhook mempercepat proses, tetapi bukan jaminan. Siapkan GET /media/job/:id sebagai cadangan: jika push tidak diterima dalam waktu yang wajar, periksa statusnya sendiri. Idempotensi menjadi tanggung jawab Anda: tugas yang sama dapat diterima melalui webhook dan ditemukan lewat polling — gunakan id sebagai acuan.
Persyaratan callback_url
- Hanya
https://. - Hanya alamat publik. Alamat lokal dan privat (
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) ditolak — ini melindungi dari SSRF. Alamat diperiksa ulang sebelum setiap pengiriman untuk mencegah DNS rebinding.
Pilihan yang tepat
| Polling | Webhook | |
|---|---|---|
Alamat https publik | tidak diperlukan | diperlukan |
| Waktu penerimaan | hingga interval polling | hampir seketika |
| Jaminan | Anda yang mengontrol | best-effort + polling sebagai cadangan |
| Cocok untuk | skrip, CI, laptop | server, backend produksi |
Rekomendasi: untuk backend, gunakan webhook + polling sebagai cadangan; untuk skrip/laptop, cukup gunakan polling.