XPlor3
REST API

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:

HeaderIsinya
webhook-idmsg_ lalu id job. Sama di setiap percobaan ulang, jadi bisa dipakai untuk mengabaikan pesan ganda
webhook-timestampWaktu pengiriman, dalam detik sejak 1 Januari 1970 (Unix)
webhook-signaturev1, lalu tanda tangan base64
content-typeapplication/json
user-agentXPlor3-Webhooks/1.0
Body
{
  "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"
  }
}
typeKapan
job.succeededJob selesai. Kirim data.assetIds ke POST /v1/assets/urls untuk tautan unduhannya
job.failedJob gagal. Kreditnya sudah kembali; alasannya di failCode dan failMessage
job.canceledJob 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.

xplor3-webhook.mjs
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 2xx dalam 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 3xx dihitung gagal, jadi pakai alamat akhirnya langsung.
  • Pesan bisa datang lebih dari sekali, misalnya kalau server kamu sudah memproses tapi balasannya tidak sampai. Pakai webhook-id atau data.id untuk 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 seperti 10.x, 172.16.x sampai 172.31.x, dan 192.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}/events mengirim perubahan status lewat Server-Sent Events dan menutup koneksinya sendiri saat job selesai.
  • Di MCP, tool wait_for_generation menunggu 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.

Di halaman ini