anoman
MasukDapatkan API Key
Beranda Dokumentasi

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.

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.

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.

Contoh body

Setiap body webhook adalah satu objek JSON dengan key tingkat-atas berikut: event_id, event_type, occurred_at, data, meta.

POST {your-url} — batch.completed

{
  "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
  }
}

POST {your-url} — anomaly.detected

{
  "event_id": "evt_8mPq2rTb",
  "event_type": "anomaly.detected",
  "occurred_at": "2026-05-28T11:04:32.190Z",
  "data": {
    "anomaly_id": "anm_def456",
    "severity": "high",
    "rule": "token_spike",
    "api_key_id": "key_xyz789",
    "agent_id": "support-bot-v2",
    "message": "Token burn rate 4.2× rolling baseline over last 5 minutes",
    "z_score": 5.8,
    "dashboard_url": "https://app.anoman.io/dashboard/anomalies/anm_def456"
  },
  "meta": {
    "delivery_attempt": 1
  }
}

POST {your-url} — balance.threshold

{
  "event_id": "evt_2vRn7sKw",
  "event_type": "balance.threshold",
  "occurred_at": "2026-05-28T08:15:00.000Z",
  "data": {
    "threshold": "low",
    "current_balance_usd": "4.12",
    "trigger_threshold_usd": "5.00",
    "billing_account_id": "ba_ghi789"
  },
  "meta": {
    "delivery_attempt": 1
  }
}

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.

Python — verifikasi signature (FastAPI)

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}

TypeScript — verifikasi (Express)

import express from "express";
import crypto from "crypto";
 
const app = express();
const SECRET = process.env.ANOMAN_WEBHOOK_SECRET!;
 
// Important: use raw body — JSON parsing would re-encode and break the signature
app.post(
  "/webhooks/anoman",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.header("x-anoman-signature") ?? "";
    const ts = req.header("x-anoman-timestamp") ?? "";
 
    // Stale timestamp protection
    if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
      return res.status(400).send("stale_timestamp");
    }
 
    const signedPayload = Buffer.concat([
      Buffer.from(`${ts}.`),
      req.body,  // raw Buffer
    ]);
    const expected = crypto
      .createHmac("sha256", SECRET)
      .update(signedPayload)
      .digest("hex");
 
    if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
      return res.status(401).send("invalid_signature");
    }
 
    const event = JSON.parse(req.body.toString());
    // Process the event...
    res.json({ received: true });
  },
);
 
app.listen(3000);

Node — pengecekan signature minimal

// Pure Node, no framework — useful in Cloudflare Workers / Lambda
const crypto = require("crypto");
 
function verifyAnomanSignature(rawBody, sigHeader, tsHeader, secret) {
  // 1. Stale timestamp (>5 min) → reject
  if (Math.abs(Date.now() / 1000 - Number(tsHeader)) > 300) return false;
 
  // 2. Re-compute HMAC
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${tsHeader}.${rawBody}`)
    .digest("hex");
 
  // 3. Constant-time compare
  return crypto.timingSafeEqual(
    Buffer.from(sigHeader),
    Buffer.from(expected),
  );
}

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.

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:

Python — idempotensi via event_id

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"])

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.

On this page