Webhook

Dapatkan notifikasi begitu tugas pembuatan asinkron selesai — tanpa perlu polling

Ringkasan

Ketika Anda mengirimkan tugas pembuatan asinkron, Atlas Cloud memprosesnya di latar belakang dan hasilnya baru tersedia beberapa saat kemudian. Alih-alih memanggil endpoint Prediksi berulang kali sampai tugas selesai, Anda dapat meminta Atlas Cloud untuk memanggil balik (callback) Anda begitu tugas mencapai status akhir.

Caranya, sertakan webhook_url saat Anda mengirimkan tugas. Ketika tugas selesai — baik berhasil, gagal, maupun kehabisan waktu — Atlas Cloud mengirimkan satu POST bertanda tangan ke URL tersebut yang berisi hasil akhirnya.

Jenis tugas yang didukung

Webhook tersedia untuk pembuatan video, gambar, dan audio asinkron. Mesin pengirimannya tidak bergantung pada jenis tugas — event_type (dan header X-AtlasCloud-Webhook-Event) yang menentukan modalitasnya: video.task.terminal, image.task.terminal, atau audio.task.terminal.

Webhook melengkapi polling — bukan menggantikannya. Endpoint Prediksi tetap bekerja persis seperti sebelumnya, dan payload webhook membawa bentuk hasil yang sama dengan yang akan Anda peroleh lewat polling. Gunakan salah satu, atau keduanya.

Mulai cepat

Tambahkan field webhook_url ke permintaan submit yang sudah ada:

curl -X POST https://api.atlascloud.ai/api/v1/model/generateVideo \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
        "model": "bytedance/seedance-2.0/text-to-video",
        "prompt": "A calico kitten chasing a butterfly in a garden, cinematic",
        "duration": 5,
        "resolution": "1080p",
        "webhook_url": "https://your-app.example.com/hooks/atlascloud"
      }'
import requests

response = requests.post(
    "https://api.atlascloud.ai/api/v1/model/generateVideo",
    headers={
        "Authorization": "Bearer your-api-key",
        "Content-Type": "application/json",
    },
    json={
        "model": "bytedance/seedance-2.0/text-to-video",
        "prompt": "A calico kitten chasing a butterfly in a garden, cinematic",
        "duration": 5,
        "resolution": "1080p",
        "webhook_url": "https://your-app.example.com/hooks/atlascloud",
    },
)

print(response.json()["data"]["id"])  # id tugas (session_id)
const res = await fetch("https://api.atlascloud.ai/api/v1/model/generateVideo", {
  method: "POST",
  headers: {
    Authorization: "Bearer your-api-key",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "bytedance/seedance-2.0/text-to-video",
    prompt: "A calico kitten chasing a butterfly in a garden, cinematic",
    duration: 5,
    resolution: "1080p",
    webhook_url: "https://your-app.example.com/hooks/atlascloud",
  }),
});

const { data } = await res.json();
console.log(data.id); // id tugas (session_id)

Field webhook_url yang sama bekerja identik pada POST /api/v1/model/generateImage (gambar asinkron, event_type: "image.task.terminal") dan POST /api/v1/model/generateAudio (audio asinkron, event_type: "audio.task.terminal").

Respons submit tidak berubah — Anda tetap langsung menerima id tugas (yaitu session_id). webhook_url hanya digunakan oleh Atlas Cloud dan tidak pernah diteruskan ke penyedia model di hulu. Ketika tugas selesai, endpoint Anda menerima sebuah POST.

Persyaratan webhook_url

URL callback Anda divalidasi pada saat submit. Jika validasi gagal, permintaan submit ditolak dengan HTTP 400 dan tidak ada tugas yang dibuat (Anda tidak dikenakan biaya).

AturanDetail
SkemaHarus https://. http:// biasa ditolak.
HostHarus berupa alamat publik yang dapat dirutekan. Rentang private, loopback, link-local, dan CGNAT (100.64.0.0/10) ditolak.
PanjangMaksimum 1024 karakter.
KeterjangkauanHarus dapat dijangkau dari internet publik agar Atlas Cloud bisa mengirim POST ke sana.

Pemeriksaan ini merupakan perlindungan terhadap SSRF. Untuk pengembangan lokal, gunakan tunnel publik (layanan pengujian webhook, ngrok, atau Cloudflare tunnel) alih-alih alamat private.

Permintaan callback

Ketika tugas mencapai status akhir, Atlas Cloud mengirimkan POST dengan Content-Type: application/json dan header berikut:

HeaderDeskripsi
X-AtlasCloud-Webhook-Idsession_id tugas — kunci korelasi & idempotensi Anda.
X-AtlasCloud-Webhook-EventJenis event, misalnya video.task.terminal, image.task.terminal, atau audio.task.terminal.
X-AtlasCloud-Webhook-TimestampWaktu Unix epoch dalam detik saat percobaan pengiriman dilakukan. Tercakup dalam tanda tangan Ed25519.
X-AtlasCloud-Webhook-SignatureHMAC-SHA256 berkode heksadesimal atas body permintaan mentah (skema HMAC). Pada skema Ed25519 murni, header ini justru membawa tanda tangan Ed25519 — lihat Memverifikasi tanda tangan.
X-AtlasCloud-Webhook-Signature-Ed25519Tanda tangan Ed25519 base64url atas <timestamp>.<raw_body> (dikirim selama masa migrasi HMAC→Ed25519).
X-AtlasCloud-Webhook-Key-Idkid dari kunci penanda tangan Ed25519 — cocok dengan salah satu kunci di JWKS.
User-AgentAtlasCloud-Webhook/1.0

Payload

{
  "session_id": "string",       // id tugas; kunci idempotensi Anda
  "event_type": "string",       // misalnya "video.task.terminal"
  "status": "OK" | "ERROR",     // hasil tingkat atas — bercabanglah berdasarkan ini
  "created_at": 1782295062952,  // waktu pembuatan tugas (epoch ms)
  "payload": {                  // hasilnya, bentuknya sama dengan API Prediksi
    "model": "string",
    "status": "completed" | "failed" | "timeout",
    "outputs": ["https://..."], // ada saat berhasil
    "error_code": 0             // ada saat gagal
  },
  "error": "string"             // hanya ada saat status == "ERROR"
}

Percabangkan handler Anda berdasarkan field status di tingkat atas: OK berarti hasil yang dapat dipakai ada di payload.outputs; ERROR berarti tugas tidak menghasilkan apa pun dan error menjelaskan alasannya.

Hasil transkripsi audio

Tugas text-to-speech dan tugas audio generatif lainnya mengembalikan URL file audionya di payload.outputs, persis seperti video dan gambar. Untuk model speech-to-text (transkripsi), payload yang selesai juga membawa objek terstruktur stt_result (teks lengkap, bahasa yang terdeteksi, dan timestamp tingkat kata) — field yang sama dengan yang dikembalikan endpoint Prediksi.

Contoh keberhasilan

Contoh pengiriman nyata untuk tugas bytedance/seedance-2.0/text-to-video yang selesai:

{
  "session_id": "6a0c02cdb4b147b7bc78881eb7229ece",
  "event_type": "video.task.terminal",
  "status": "OK",
  "created_at": 1782295062952,
  "payload": {
    "model": "bytedance/seedance-2.0/text-to-video",
    "status": "completed",
    "outputs": [
      "https://atlas-media.oss-us-west-1.aliyuncs.com/videos/cgt-20260624-0.mp4"
    ]
  }
}

Contoh kegagalan

{
  "session_id": "9b2f4e7a1c0d4f5e8a6b3c2d1e0f9a8b",
  "event_type": "video.task.terminal",
  "status": "ERROR",
  "created_at": 1782200000000,
  "payload": {
    "model": "bytedance/seedance-2.0/text-to-video",
    "status": "failed",
    "error_code": 1039
  },
  "error": "the input was rejected by content moderation"
}

Hasil timeout terlihat sama, dengan payload.status: "timeout" dan pesan error yang generik.

Memverifikasi tanda tangan

Selalu verifikasi tanda tangan sebelum memercayai sebuah webhook. Tanda tangan membuktikan bahwa permintaan itu berasal dari Atlas Cloud dan tidak dimanipulasi.

Dua skema selama masa migrasi

Atlas Cloud sedang memindahkan tanda tangan webhook dari shared secret HMAC ke Ed25519 dengan endpoint JWKS publik. Selama masa transisi, setiap pengiriman membawa keduanya: tanda tangan HMAC (X-AtlasCloud-Webhook-Signature) dan tanda tangan Ed25519 (X-AtlasCloud-Webhook-Signature-Ed25519). Utamakan Ed25519 — verifikasinya memakai kunci publik yang Anda ambil dari sebuah URL, tanpa shared secret yang perlu disimpan.

Ed25519 + JWKS (direkomendasikan)

Atlas Cloud menandatangani setiap pengiriman dengan kunci privat Ed25519 dan memublikasikan kunci publik pasangannya di endpoint JWKS. Anda memverifikasi terhadap kunci publik tersebut — tidak ada secret yang perlu disiapkan atau dirotasi di sisi Anda.

  • Kunci publik (JWKS): GET https://api.atlascloud.ai/api/v1/webhooks/jwks.json (tanpa autentikasi):
{
  "keys": [
    {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "Wn_rgZDBO6nv4-ka97PPf5z8WXbM25o75dR6YukTqqI",
      "use": "sig",
      "alg": "EdDSA",
      "kid": "cnut8IN2blKoB5zRJr9th0HoLzH-iBH3WYoUtkcJZQE"
    }
  ]
}
  • Pesan yang ditandatangani: "<timestamp>.<raw_body>" — nilai X-AtlasCloud-Webhook-Timestamp, sebuah . literal, lalu body permintaan mentah yang persis. Berbeda dengan HMAC, timestamp ikut tercakup dalam tanda tangan, sehingga Anda dapat menerapkan jendela replay.
  • Header tanda tangan: X-AtlasCloud-Webhook-Signature-Ed25519 (base64url). Setelah HMAC dipensiunkan, tanda tangan Ed25519 akan berpindah ke X-AtlasCloud-Webhook-Signature — jadi utamakan header -Ed25519 bila tersedia dan gunakan -Signature sebagai cadangan.
  • ID kunci: X-AtlasCloud-Webhook-Key-Id adalah kid dari JWK yang menandatangani pengiriman tersebut.

Langkah-langkah

  1. Baca X-AtlasCloud-Webhook-Timestamp (ts), X-AtlasCloud-Webhook-Key-Id (kid), dan header tanda tangan Ed25519.
  2. (Direkomendasikan) Tolak jika timestamp berselisih lebih dari ~5 menit dari jam Anda (perlindungan replay).
  3. Ambil JWKS dan pilih kunci yang kid-nya cocok. Cache JWKS tersebut; bila menemui kid yang tidak Anda kenali, ambil ulang satu kali (kunci penanda tangan mungkin sudah dirotasi).
  4. Decode x pada JWK (base64url) → kunci publik Ed25519 sepanjang 32 byte.
  5. Verifikasi tanda tangan hasil decode base64url atas ts + "." + raw_body.
const crypto = require("crypto");

const JWKS_URL = "https://api.atlascloud.ai/api/v1/webhooks/jwks.json";
let jwks = {}; // kid -> jwk

async function publicKey(kid) {
  if (!jwks[kid]) {
    const { keys } = await (await fetch(JWKS_URL)).json();
    jwks = Object.fromEntries(keys.map((k) => [k.kid, k]));
  }
  const jwk = jwks[kid];
  return jwk && crypto.createPublicKey({ key: jwk, format: "jwk" });
}

// req.body harus berupa Buffer body MENTAH (misalnya express.raw()).
async function verifyEd25519(req) {
  const ts = req.get("X-AtlasCloud-Webhook-Timestamp");
  const kid = req.get("X-AtlasCloud-Webhook-Key-Id");
  const sig =
    req.get("X-AtlasCloud-Webhook-Signature-Ed25519") ||
    req.get("X-AtlasCloud-Webhook-Signature");

  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // jendela replay

  const pub = await publicKey(kid);
  if (!pub) return false;
  const msg = Buffer.concat([Buffer.from(`${ts}.`), req.body]);
  return crypto.verify(null, msg, pub, Buffer.from(sig, "base64url"));
}
import time, json, base64, urllib.request
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.exceptions import InvalidSignature

JWKS_URL = "https://api.atlascloud.ai/api/v1/webhooks/jwks.json"
_jwks = {}  # kid -> jwk

def _b64u(s: str) -> bytes:
    return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))

def _public_key(kid: str):
    if kid not in _jwks:
        keys = json.load(urllib.request.urlopen(JWKS_URL, timeout=5))["keys"]
        _jwks.clear()
        _jwks.update({k["kid"]: k for k in keys})
    jwk = _jwks.get(kid)
    return Ed25519PublicKey.from_public_bytes(_b64u(jwk["x"])) if jwk else None

def verify_ed25519(headers, raw_body: bytes) -> bool:
    ts = headers["X-AtlasCloud-Webhook-Timestamp"]
    kid = headers["X-AtlasCloud-Webhook-Key-Id"]
    sig = headers.get("X-AtlasCloud-Webhook-Signature-Ed25519") \
        or headers["X-AtlasCloud-Webhook-Signature"]

    if abs(time.time() - int(ts)) > 300:  # jendela replay
        return False
    pub = _public_key(kid)
    if pub is None:
        return False
    try:
        pub.verify(_b64u(sig), ts.encode() + b"." + raw_body)
        return True
    except InvalidSignature:
        return False
const jwksURL = "https://api.atlascloud.ai/api/v1/webhooks/jwks.json"

// fetchKey mengembalikan kunci publik base64url untuk kid. Pada kode nyata,
// tambahkan caching + satu kali pengambilan ulang saat miss (rotasi kunci).
func fetchKey(kid string) (string, bool) {
    resp, err := http.Get(jwksURL)
    if err != nil {
        return "", false
    }
    defer resp.Body.Close()
    var set struct {
        Keys []struct{ Kid, X string } `json:"keys"`
    }
    if json.NewDecoder(resp.Body).Decode(&set) != nil {
        return "", false
    }
    for _, k := range set.Keys {
        if k.Kid == kid {
            return k.X, true
        }
    }
    return "", false
}

func verifyEd25519(h http.Header, body []byte) bool {
    ts := h.Get("X-AtlasCloud-Webhook-Timestamp")
    sigB64 := h.Get("X-AtlasCloud-Webhook-Signature-Ed25519")
    if sigB64 == "" {
        sigB64 = h.Get("X-AtlasCloud-Webhook-Signature")
    }
    t, _ := strconv.ParseInt(ts, 10, 64)
    if math.Abs(float64(time.Now().Unix()-t)) > 300 { // jendela replay
        return false
    }
    xB64, ok := fetchKey(h.Get("X-AtlasCloud-Webhook-Key-Id"))
    if !ok {
        return false
    }
    pub, e1 := base64.RawURLEncoding.DecodeString(xB64)
    sig, e2 := base64.RawURLEncoding.DecodeString(sigB64)
    if e1 != nil || e2 != nil || len(pub) != ed25519.PublicKeySize {
        return false
    }
    return ed25519.Verify(ed25519.PublicKey(pub), append([]byte(ts+"."), body...), sig)
}

Tanda tangan membuktikan bahwa callback berasal dari Atlas Cloud — bukan menunjukkan akun mana yang memiliki tugas tersebut. Karena webhook_url diberikan per permintaan, korelasikan juga session_id dengan tugas yang benar-benar Anda buat sebelum menindaklanjuti hasilnya.

HMAC (lawas)

Penandatanganan HMAC sedang ditinggalkan (deprecated) dan digantikan oleh Ed25519/JWKS di atas; integrasi baru sebaiknya memakai Ed25519. Header HMAC X-AtlasCloud-Webhook-Signature tetap dikirim selama jendela migrasi.

Tanda tangan dihitung sebagai:

HMAC-SHA256( signing_secret, raw_request_body )   →   lowercase hex
  • Kuncinya adalah shared signing secret Anda (disediakan untuk akun Anda).
  • Pesannya adalah byte mentah persis dari body permintaan — verifikasi sebelum ada re-serialisasi JSON apa pun, karena itu dapat mengubah urutan byte atau spasi.
  • Bandingkan hasilnya dengan header X-AtlasCloud-Webhook-Signature menggunakan perbandingan waktu-konstan (constant-time).

Header X-AtlasCloud-Webhook-Timestamp bersifat informatif (berguna untuk pemeriksaan jendela replay yang opsional). Header ini bukan bagian dari konten yang ditandatangani — hanya body mentah yang ditandatangani.

const crypto = require("crypto");
const express = require("express");
const app = express();

const SIGNING_SECRET = process.env.ATLASCLOUD_WEBHOOK_SECRET;

// Tangkap body MENTAH — jangan biarkan parser JSON berjalan lebih dulu.
app.use("/hooks/atlascloud", express.raw({ type: "*/*" }));

app.post("/hooks/atlascloud", (req, res) => {
  const sig = req.get("X-AtlasCloud-Webhook-Signature") || "";
  const expected = crypto
    .createHmac("sha256", SIGNING_SECRET)
    .update(req.body)               // req.body adalah Buffer (byte mentah)
    .digest("hex");

  // Bandingkan sebagai Buffer dan periksa panjang BYTE: timingSafeEqual melempar
  // error pada input yang panjangnya berbeda, dan header multibyte yang cacat bisa
  // cocok pada panjang string JS namun berbeda pada panjang byte.
  const sigBuf = Buffer.from(sig);
  const expBuf = Buffer.from(expected);
  const ok =
    sigBuf.length === expBuf.length &&
    crypto.timingSafeEqual(sigBuf, expBuf);
  if (!ok) return res.status(401).send("invalid signature");

  const event = JSON.parse(req.body.toString("utf8"));
  // ... masukkan ke antrean berdasarkan event.session_id, lalu balas dengan cepat ...
  res.status(200).send("ok");
});
import hmac, hashlib, os
from flask import Flask, request, abort

SIGNING_SECRET = os.environ["ATLASCLOUD_WEBHOOK_SECRET"].encode()
app = Flask(__name__)

@app.post("/hooks/atlascloud")
def atlascloud_webhook():
    raw = request.get_data()  # byte mentah, sebelum parsing JSON
    expected = hmac.new(SIGNING_SECRET, raw, hashlib.sha256).hexdigest()
    received = request.headers.get("X-AtlasCloud-Webhook-Signature", "")
    # Bandingkan sebagai bytes: compare_digest pada str melempar error untuk input non-ASCII.
    if not hmac.compare_digest(expected.encode(), received.encode()):
        abort(401)

    event = request.get_json()
    # ... masukkan ke antrean berdasarkan event["session_id"], lalu balas 200 dengan cepat ...
    return "ok", 200
func verify(secret string, body []byte, sigHeader string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(sigHeader))
}

Semantik pengiriman

Mengonfirmasi sebuah pengiriman

Balas dengan kode status 2xx apa pun untuk mengonfirmasi penerimaan. Kode status lain — atau timeout koneksi — dianggap sebagai kegagalan dan pengiriman akan diulang.

Balaslah dengan cepat (jauh di bawah beberapa detik). Kerjakan proses sesungguhnya secara asinkron: verifikasi tanda tangan, masukkan event ke antrean dengan kunci session_id, lalu kembalikan 200 seketika. Respons yang lambat berisiko timeout dan memicu pengulangan yang tidak perlu.

Pengulangan dan backoff

Jika sebuah pengiriman tidak dikonfirmasi, Atlas Cloud mengulanginya dengan exponential backoff (kira-kira 10s → 20s → 40s → …, dibatasi sekitar 30 menit), hingga maksimal ~10 percobaan. Setelah percobaan habis, pengiriman ditandai sebagai tidak terkirim dan tidak diulang lagi.

At-least-once — deduplikasi berdasarkan session_id

Pengiriman bersifat at-least-once. Dalam kasus yang jarang terjadi, Anda mungkin menerima webhook yang sama lebih dari satu kali. Buat handler Anda idempoten dan lakukan deduplikasi berdasarkan session_id.

session_id tetap sama di seluruh pengulangan. Perlakukan webhook dengan session_id yang sudah Anda proses tuntas sebagai no-op dan kembalikan 200.

Ketepatan waktu

Sebagian besar webhook terkirim dalam hitungan detik setelah tugas selesai. Jaring pengaman rekonsiliasi bawaan menjamin pengiriman tetap terjadi meskipun jalur cepat terlewat (misalnya saat deployment layanan), dengan konsekuensi keterlambatan hingga ~30 menit pada kasus langka tersebut. Rancang sistem Anda untuk pengiriman eventual dan at-least-once, bukan seketika dan exactly-once.

Praktik terbaik

  • Sajikan endpoint callback melalui HTTPS pada host yang dapat dijangkau publik.
  • Verifikasi tanda tangan webhook sebelum memercayai event — sebaiknya dengan Ed25519/JWKS (tidak ada secret yang perlu disimpan; verifikasi terhadap kunci publik yang diambil berdasarkan kid). HMAC adalah cadangan lawas selama jendela migrasi.
  • Saat memverifikasi Ed25519, cache JWKS dan ambil ulang bila menemui kid yang tidak dikenal; periksa tanda tangan atas "<timestamp>.<raw_body>" dan terapkan jendela replay.
  • Balas 2xx dengan cepat; pindahkan pemrosesan sesungguhnya ke antrean latar belakang.
  • Deduplikasi berdasarkan session_id — handler harus idempoten.
  • Bercabanglah berdasarkan status di tingkat atas (OK vs ERROR); baca hasilnya dari payload.outputs.
  • Jangan berasumsi ada urutan atau exactly-once; rancang untuk at-least-once.
  • Pertahankan endpoint Prediksi sebagai jalur cadangan / rekonsiliasi.
  • Khusus HMAC lawas: jaga kerahasiaan signing secret Anda dan rotasikan bila bocor. (Ed25519/JWKS tidak menyimpan secret di sisi Anda.)

Pemecahan masalah

GejalaKemungkinan penyebab / tindakan
Submit mengembalikan 400 webhook_url ... is not a routable public addressHost-nya private/loopback/CGNAT. Gunakan URL HTTPS publik atau sebuah tunnel.
Submit mengembalikan 400 webhook_url must use httpsUbah skemanya menjadi https://.
Submit mengembalikan 400 webhook_url exceeds the 1024-character limitPerpendek URL-nya (pindahkan state ke penyimpanan Anda sendiri dengan kunci session_id).
Tidak ada webhook yang diterimaPastikan endpoint dapat dijangkau publik dan mengembalikan 2xx; verifikasi bahwa tugas benar-benar mencapai status akhir melalui endpoint Prediksi.
Tanda tangan tidak cocok (Ed25519)Tandatangani atas "<timestamp>.<raw_body>" (bukan body saja), decode tanda tangan dengan base64url, dan cari kuncinya berdasarkan kid di JWKS.
Tanda tangan tidak cocok (HMAC)Pastikan Anda meng-HMAC byte body mentah (bukan objek JSON yang diserialisasi ulang) dan memakai signing secret yang benar.
kid tidak ada di JWKSKunci penanda tangan telah dirotasi — ambil ulang JWKS (jangan cache satu kunci selamanya).
Menerima event yang sama dua kaliWajar pada pengiriman at-least-once — lakukan deduplikasi berdasarkan session_id.

Referensi

  • Submit (dengan webhook): POST /api/v1/model/generateVideo, POST /api/v1/model/generateImage, atau POST /api/v1/model/generateAudio — tambahkan webhook_url.
  • Jenis event: video.task.terminal, image.task.terminal, audio.task.terminal.
  • JWKS (kunci publik): GET /api/v1/webhooks/jwks.json.
  • Tanda tangan (Ed25519, direkomendasikan): Ed25519 base64url atas "<timestamp>.<raw_body>", di X-AtlasCloud-Webhook-Signature-Ed25519 (id kunci di X-AtlasCloud-Webhook-Key-Id).
  • Tanda tangan (HMAC, lawas): HMAC-SHA256(signing_secret, raw_body), heksadesimal, di X-AtlasCloud-Webhook-Signature.
  • Kunci idempotensi: session_id (juga ada di X-AtlasCloud-Webhook-Id).
  • Alternatif polling: Prediksi.