anoman
Docs · Webhooks

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

  1. Buka dashboard → Settings → Webhooks.
  2. Tambahkan URL endpoint Anda (harus HTTPS).
  3. Pilih tipe event mana yang ingin dilanggan (atau semua).
  4. Salin webhook signing secret yang dihasilkan — disimpan sekali, ditampilkan sekali. Gunakan untuk memverifikasi signature.
  5. 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 eventKapan
batch.completedBatch job yang diantrekan selesai. Payload menyertakan biaya + penghematan.
batch.failedBatch job kehabisan percobaan retry dan tak akan pulih.
batch.escalatedJob dipromosikan dari batch ke realtime untuk memenuhi SLA. Biaya lebih tinggi.
anomaly.detectedZ-score atau model ML menandai perilaku tak biasa. Severity ∈ {low, medium, high}.
balance.thresholdSaldo prabayar melewati ambang saldo rendah yang dikonfigurasi.
key.budget_warningAnggaran per key melewati 80% / 90% / 100%.
guardrail.spikeLedakan 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}
Jangan parse sebelum memverifikasi. Mem-parse JSON lalu men-serialisasi ulang merusak keakuratan byte yang diandalkan HMAC. Verifikasi terhadap byte body mentah; parse hanya setelah signature lolos.

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 ngrok atau cloudflared tunnel untuk mengekspos localhost:3000 lewat HTTPS. Signing secret tetap sama.

Siapkan webhook pertama Anda.

Butuh 60 detik. Event uji gratis untuk memverifikasi koneksi.