Skip to content

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
  1. Pembuatan. POST /media/generate dengan model dan parameter — mengembalikan id dan tautan untuk polling:
json
{ "id": "beefb531-…", "model": "image/nano-banana-2", "status": "processing",
  "poll": "https://nordrouter.com/media/job/beefb531-…" }
  1. Menunggu. Tugas beralih dari processing → done (atau failed). Gambar memerlukan beberapa detik, sedangkan video dan musik memerlukan puluhan detik hingga beberapa menit.

  2. Hasil. Tugas yang selesai memiliki result_url (tautan ke nordrouter.com); file disimpan untuk waktu terbatas (retention_days dari GET /media/models) — unduh untuk menyimpannya.

Format respons GET /media/job/:id (dan isi webhook — lihat di bawah) sama:

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 — hanya terisi saat done, selain itu null.
  • result_url_2 — trek kedua pada model musik (Suno), selain itu null.
  • 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 saat failed, selain itu null.

Cara 1. Polling ​

Cara paling sederhana adalah meminta GET /media/job/:id secara berkala hingga status menjadi done/failed:

bash
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:

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": "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:

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 (berhasil) atau media.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:

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)  # perbandingan dengan waktu konstan
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));
}

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:

javascript
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:

python
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 ulang

Menguji 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 ​

PollingWebhook
Alamat https publiktidak diperlukandiperlukan
Waktu penerimaanhingga interval pollinghampir seketika
JaminanAnda yang mengontrolbest-effort + polling sebagai cadangan
Cocok untukskrip, CI, laptopserver, backend produksi

Rekomendasi: untuk backend, gunakan webhook + polling sebagai cadangan; untuk skrip/laptop, cukup gunakan polling.