Webhook
XPlor3 memberi tahu server kamu saat job selesai, gagal, atau dibatalkan, jadi kamu tidak perlu membaca status berulang kali.
Membaca GET /v1/jobs/{id} tiap beberapa detik memang jalan, tapi untuk video yang butuh beberapa menit, itu puluhan request yang jawabannya sama. Dengan webhook, kamu cukup menyebut alamat saat memulai job. Begitu job berakhir, XPlor3 mengirim satu POST bertanda tangan ke alamat itu.
Ambil rahasia penanda tangan
Buka Kredensial di studio, pilih tab REST API, lalu tekan Tampilkan di bagian Webhook. Simpan nilainya (diawali whsec_) sebagai variabel lingkungan di server kamu, misalnya XPLOR3_WEBHOOK_SECRET.
Rahasia ini hanya bisa dilihat dan diganti dari studio, tidak lewat kunci API. Satu akun punya satu rahasia untuk semua webhook-nya.
Kirim webhookUrl saat memulai job
Tambahkan webhookUrl di samping modelSlug dan input:
curl https://api.xplor3.space/v1/jobs \
-H "Authorization: Bearer $XPLOR3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"modelSlug": "nano-banana-2",
"input": { "prompt": "Secangkir kopi di meja kayu dekat jendela, cahaya pagi" },
"webhookUrl": "https://contoh.id/hooks/xplor3"
}'Alamat yang tidak memenuhi syaratnya ditolak dengan 400 WEBHOOK_URL_INVALID sebelum kredit terpotong.
Terima pesannya, cek tanda tangan, balas 2xx
Cek tanda tangan dari body mentah, balas 2xx secepatnya, lalu kerjakan sisanya di latar. Contohnya ada di Memeriksa tanda tangan.
Isi pesan
Setiap pesan adalah POST dengan body JSON dan header berikut:
| Header | Isinya |
|---|---|
webhook-id | msg_ lalu id job. Sama di setiap percobaan ulang, jadi bisa dipakai untuk mengabaikan pesan ganda |
webhook-timestamp | Waktu pengiriman, dalam detik sejak 1 Januari 1970 (Unix) |
webhook-signature | v1, lalu tanda tangan base64 |
content-type | application/json |
user-agent | XPlor3-Webhooks/1.0 |
{
"type": "job.succeeded",
"timestamp": "2026-10-23T08:30:12.000Z",
"data": {
"id": "cm...",
"status": "SUCCEEDED",
"kind": "IMAGE",
"modelSlug": "nano-banana-2",
"creditCost": 9.6,
"assetIds": ["cm..."],
"failCode": null,
"failMessage": null,
"createdAt": "2026-10-23T08:29:51.000Z",
"finishedAt": "2026-10-23T08:30:12.000Z"
}
}type | Kapan |
|---|---|
job.succeeded | Job selesai. Kirim data.assetIds ke POST /v1/assets/urls untuk tautan unduhannya |
job.failed | Job gagal. Kreditnya sudah kembali; alasannya di failCode dan failMessage |
job.canceled | Job dibatalkan saat masih antre. Kreditnya sudah kembali |
Rincian lengkap job, termasuk input dan ukuran tiap berkas, tetap ada di GET /v1/jobs/{id}.
Memeriksa tanda tangan
Formatnya Standard Webhooks: HMAC-SHA256 atas webhook-id, titik, webhook-timestamp, titik, lalu body mentah, dengan kunci berupa rahasia tanpa awalan whsec_ yang di-decode dari base64. Library standardwebhooks untuk Node dan Python bisa dipakai langsung, atau tulis sendiri seperti di bawah.
Pakai body mentah
Tanda tangan dihitung dari byte yang dikirim. Body yang sudah di-parse lalu diubah jadi JSON lagi bisa berbeda urutan atau spasinya, dan tanda tangannya tidak akan cocok.
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const SECRET = process.env.XPLOR3_WEBHOOK_SECRET;
function verifyXplor3(rawBody, headers, secret) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signatures = headers["webhook-signature"] ?? "";
if (!id || !timestamp) return false;
// Tolak pesan yang lebih tua dari lima menit, supaya tidak bisa diputar ulang.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest("base64");
return signatures.split(" ").some((part) => {
const [version, signature] = part.split(",");
return (
version === "v1" &&
signature !== undefined &&
signature.length === expected.length &&
timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
);
});
}
const app = express();
app.post("/hooks/xplor3", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
if (!verifyXplor3(raw, req.headers, SECRET)) return res.status(401).end();
const event = JSON.parse(raw);
res.status(204).end();
// Sesudah membalas: simpan, unduh hasilnya, kabari penggunamu.
console.log(event.type, event.data.id, event.data.assetIds);
});
app.listen(3000);Pengiriman ulang
- Berhasil berarti dibalas
2xxdalam 10 detik. Status lain, waktu habis, atau koneksi yang gagal dihitung gagal. - Delapan kali percobaan: jeda 30 detik, lalu berlipat dua tiap kali gagal, sekitar satu jam semuanya. Setelah itu XPlor3 berhenti mencoba.
- Redirect tidak diikuti. Balasan
3xxdihitung gagal, jadi pakai alamat akhirnya langsung. - Pesan bisa datang lebih dari sekali, misalnya kalau server kamu sudah memproses tapi balasannya tidak sampai. Pakai
webhook-idataudata.iduntuk mengabaikan yang ganda. - Hasilnya tetap aman kalau semua percobaan gagal. Job tetap tersimpan di Riwayat dan bisa dibaca dengan
GET /v1/jobs/{id}kapan saja.
Cara pengirimannya terlihat di GET /v1/jobs/{id}, di field webhook:
{
"webhook": {
"url": "https://contoh.id/hooks/xplor3",
"attempts": 2,
"deliveredAt": "2026-10-23T08:31:05.000Z",
"lastError": null
}
}lastError menyimpan alasan percobaan terakhir yang gagal, misalnya HTTP_503, TIMEOUT, ECONNREFUSED, atau WEBHOOK_ADDRESS_BLOCKED. Untuk job tanpa webhookUrl, nilai webhook adalah null.
Syarat alamat
- HTTPS di port 443, tanpa nama pengguna atau kata sandi di URL. Query string boleh, misalnya token milikmu sendiri.
- Bisa dijangkau dari internet.
localhost, alamat jaringan lokal seperti10.x,172.16.xsampai172.31.x, dan192.168.x, serta alamat internal lain ditolak. Pemeriksaan ini diulang tepat sebelum pesan dikirim, termasuk ke alamat IP yang dituju nama domainnya. - Paling panjang 2.048 karakter.
Untuk mencoba dari laptop, pakai tunnel yang memberi alamat HTTPS publik, misalnya ngrok atau Cloudflare Tunnel.
Mengganti rahasia
Tekan Ganti rahasia di bagian Webhook halaman Kredensial. Rahasia lama langsung tidak berlaku, dan setiap pesan yang dikirim sesudahnya ditandatangani dengan rahasia baru, termasuk percobaan ulang untuk job yang sudah selesai sebelumnya. Perbarui variabel lingkungan di server kamu di waktu yang sama.
Tanpa server publik
Kalau kodemu tidak bisa menerima request dari internet, misalnya skrip di laptop:
GET /v1/jobs/{id}/eventsmengirim perubahan status lewat Server-Sent Events dan menutup koneksinya sendiri saat job selesai.- Di MCP, tool
wait_for_generationmenunggu di server sampai job selesai, jadi agen cukup memanggilnya sekali. Lihat Server MCP. - Polling
GET /v1/jobs/{id}tiap 3 sampai 5 detik tetap boleh, dan jauh di bawah batas request.

