Event asinkron yang tak perlu Anda poll.
POST bertanda tangan HMAC untuk penyelesaian batch, deteksi anomali, dan ambang saldo. Pengiriman at-least-once dengan retry eksponensial.
Penyiapan
Daftarkan endpoint
- Buka dashboard → Settings → Webhooks.
- Tambahkan URL endpoint Anda (harus HTTPS).
- Pilih tipe event mana yang ingin dilanggan (atau semua).
- Salin webhook signing secret yang dihasilkan — disimpan sekali, ditampilkan sekali. Gunakan untuk memverifikasi signature.
- Anoman langsung mengirim event uji sehingga Anda bisa memastikan endpoint Anda dapat dijangkau.
Anda dapat mendaftarkan hingga 3 endpoint per pelanggan. Beberapa endpoint menerima event yang sama secara paralel — berguna untuk memisahkan produksi dari mirror staging.
Tipe event
Yang kami kirim saat ini
| Tipe event | Kapan |
|---|---|
| batch.completed | Batch job yang diantrekan selesai. Payload menyertakan biaya + penghematan. |
| batch.failed | Batch job kehabisan percobaan retry dan tak akan pulih. |
| batch.escalated | Job dipromosikan dari batch ke realtime untuk memenuhi SLA. Biaya lebih tinggi. |
| anomaly.detected | Z-score atau model ML menandai perilaku tak biasa. Severity ∈ {low, medium, high}. |
| balance.threshold | Saldo prabayar melewati ambang saldo rendah yang dikonfigurasi. |
| key.budget_warning | Anggaran per key melewati 80% / 90% / 100%. |
| guardrail.spike | Ledakan blokir melampaui baseline — kemungkinan serangan atau agent salah konfigurasi. |
Bentuk payload
Contoh body
Setiap body webhook adalah satu objek JSON dengan key tingkat-atas berikut: event_id, event_type, occurred_at, data, meta.
{
"event_id": "evt_5kJh4nQp",
"event_type": "batch.completed",
"occurred_at": "2026-05-28T14:23:17.412Z",
"data": {
"batch_job_id": "job_abc123",
"api_key_id": "key_xyz789",
"model": "deepseek-v3",
"status": "complete",
"prompt_tokens": 4892,
"completion_tokens": 1203,
"cost_usd": "0.0028",
"savings_usd": "0.0084",
"poll_url": "/anoman/v1/batch/job_abc123",
"completed_at": "2026-05-28T14:23:15.001Z"
},
"meta": {
"delivery_attempt": 1
}
}Verifikasi signature
Selalu verifikasi sebelum memproses
Setiap request webhook membawa dua header:
x-anoman-timestamp— timestamp Unix saat event dikirim.x-anoman-signature— HMAC-SHA256 terenkode hex dari{timestamp}.{raw_body}memakai signing secret Anda.
Selalu: (1) pastikan timestamp berada dalam 5 menit (proteksi replay), (2) hitung ulang HMAC atas body request mentah, (3) bandingkan secara constant-time.
import hmac
import hashlib
import os
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
SECRET = os.environ["ANOMAN_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/anoman")
async def receive_webhook(request: Request):
raw_body = await request.body()
sig_header = request.headers.get("x-anoman-signature", "")
timestamp = request.headers.get("x-anoman-timestamp", "")
# Reject events older than 5 minutes — limits replay window
import time
if abs(time.time() - int(timestamp)) > 300:
raise HTTPException(400, "stale_timestamp")
# Signature is HMAC-SHA256 over: timestamp + "." + raw_body
signed_payload = f"{timestamp}.".encode() + raw_body
expected = hmac.new(SECRET, signed_payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig_header, expected):
raise HTTPException(401, "invalid_signature")
event = await request.json()
# Process the event...
return {"received": True}Pengiriman + retry
At-least-once
Kami menganggap response 2xx apa pun sebagai sukses. Selain itu (termasuk timeout > 10s) memicu retry dengan jadwal ini:
- Percobaan 1: langsung
- Percobaan 2: 30 detik kemudian
- Percobaan 3: 5 menit kemudian
- Percobaan 4: 1 jam kemudian
- Percobaan 5: 6 jam kemudian (terakhir)
Setelah percobaan ke-5, event dipindahkan ke antrean failed-deliveries (terlihat di dashboard) dan notifikasi webhook.delivery_exhausted terpisah dikirim via email ke admin akun Anda.
Setiap percobaan pengiriman menambah meta.delivery_attempt di payload. Gunakan untuk logging + debug. Karena kami retry pada setiap non-2xx, endpoint Anda harus idempoten:
import sqlite3
from datetime import datetime
# Persist event_ids you've processed. Any DB works — Redis SETNX,
# Postgres UNIQUE constraint, DynamoDB conditional put, etc.
db = sqlite3.connect("webhook_events.db")
db.execute("""
CREATE TABLE IF NOT EXISTS processed_events (
event_id TEXT PRIMARY KEY,
received_at TEXT
)
""")
db.commit()
def process_event(event):
event_id = event["event_id"]
# Atomic: insert-or-fail. If already processed, return 200 OK.
try:
db.execute(
"INSERT INTO processed_events VALUES (?, ?)",
(event_id, datetime.utcnow().isoformat()),
)
db.commit()
except sqlite3.IntegrityError:
return # Already handled; idempotent no-op
# Now do the real work — guaranteed once-per-event
if event["event_type"] == "batch.completed":
notify_user(event["data"]["batch_job_id"])
elif event["event_type"] == "anomaly.detected":
alert_oncall(event["data"]["anomaly_id"])Pengujian
Replay + pengiriman manual
- Halaman webhook di dashboard punya tombol “Send test event” yang mengirim payload sintetis untuk tiap tipe event.
- Log pengiriman menampilkan setiap percobaan dengan kode status + body response. Replay percobaan individual mana pun dengan satu klik.
- Untuk pengembangan lokal, gunakan tunnel seperti
ngrokataucloudflared tunneluntuk mengeksposlocalhost:3000lewat HTTPS. Signing secret tetap sama.
Siapkan webhook pertama Anda.
Butuh 60 detik. Event uji gratis untuk memverifikasi koneksi.