Skip to main content
DecimalAI sends outbound HTTP webhooks when notable events happen in your organization — for example, when a regression is detected on a newly-shipped manifest, or when your usage is approaching plan limits. Configure a single webhook URL per organization. DecimalAI POSTs a JSON body to that URL for every enabled event.
Webhooks support HMAC signing (X-Decimal-Signature) for verifiable delivery and automatic retry with backoff on failure. Set a webhook secret and verify signatures so you can trust the payload. Delivery is still subject to a 5-second per-attempt timeout — acknowledge fast and process asynchronously.

Setup

In Settings → Notifications, set:
  • Webhook URL — the HTTPS endpoint that receives events.
  • Enabled events — which event types to send. Defaults to all events.
You can also configure via the API:
curl -X PATCH https://api.decimal.ai/api/v1/org/notifications \
  -H "Authorization: Bearer dai_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://example.com/decimalai/hook",
    "events": ["regression.detected", "manifest.changed"]
  }'

Event types

EventFires when
manifest.changedA new manifest version is registered for an agent (new tools, prompt sections, model, or skill set).
regression.detectedThe post-deploy bisect view marks a manifest version as a regression.
regression.resolvedA previously-flagged regression is marked resolved (rollback or fix shipped).
usage.warningYour org has consumed 80% of a plan limit (traces ingested, SFT rows).
usage.limit_reachedYour org has consumed 100% of a plan limit. Further requests return 402.
payment.failedStripe reports a failed payment. Grace period starts; service degrades after the grace window.
payment.confirmedStripe confirms a successful payment.

Payload format

Every webhook body has the same envelope. The data field carries event-specific fields.
{
  "event": "regression.detected",
  "timestamp": "2026-05-13T17:42:11.123456+00:00",
  "org_id": "org_abc123",
  "org_name": "Acme AI",
  "data": {
    "agent_name": "support-agent",
    "manifest_id": "mfst_xyz789",
    "version_label": "v26",
    "previous_version_label": "v25",
    "metric": "pass_rate",
    "delta": -0.18
  }
}
The data field for each event:
{
  "agent_name": "support-agent",
  "version_label": "v3",
  "previous_version_label": "v2",
  "manifest_id": "mfst_abc123",
  "traces_affected": 450
}
{
  "agent_name": "support-agent",
  "manifest_id": "mfst_xyz789",
  "version_label": "v26",
  "previous_version_label": "v25",
  "metric": "pass_rate",
  "delta": -0.18,
  "trace_sample_ids": ["trc_001", "trc_002", "trc_003"]
}
{
  "metric": "traces_ingested",
  "current": 4250,
  "limit": 5000,
  "plan": "free",
  "period_start": "2026-05-01T00:00:00Z",
  "period_end": "2026-06-01T00:00:00Z"
}
{
  "stripe_invoice_id": "in_1Abc...",
  "amount": 2900,
  "currency": "usd",
  "grace_period_ends": "2026-05-20T00:00:00Z"
}

Headers

Every webhook request includes:
HeaderValue
Content-Typeapplication/json
User-AgentDecimalAI-Webhooks/1.0
X-Decimal-EventThe event name (e.g., regression.detected)
X-Decimal-Event-IdUnique ID for this delivery — use it to deduplicate retries.
X-Decimal-Signaturesha256=<hex> HMAC over the raw body, present when a webhook secret is configured. See Verifying webhooks.

Verifying webhooks

When your org has a webhook secret configured, every webhook request carries an X-Decimal-Signature header of the form sha256=<hex_digest> — an HMAC-SHA256 over the raw request body bytes using your secret. Verify it before trusting the payload. Set or rotate the secret in Settings → Notifications.
import hmac, hashlib

def verify(body_bytes: bytes, signature_header: str, secret: str) -> bool:
    """Constant-time HMAC verification against the X-Decimal-Signature header."""
    expected = "sha256=" + hmac.new(
        secret.encode(), body_bytes, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)

# In your webhook handler — hash the RAW body, not the re-serialized JSON:
raw = await request.body()
sig = request.headers.get("X-Decimal-Signature", "")
if not verify(raw, sig, os.environ["DECIMAL_WEBHOOK_SECRET"]):
    return Response(status_code=401)
The signature covers the exact bytes DecimalAI sent. Verify against await request.body() (or your framework’s raw-body accessor) — re-serializing the parsed JSON can reorder keys and break the comparison.

Receiving — example handlers

from fastapi import FastAPI, Request

app = FastAPI()

@app.post("/decimalai/hook")
async def handle_webhook(request: Request):
    body = await request.json()
    event = body["event"]
    data = body["data"]

    if event == "regression.detected":
        # Alert your team — Slack, PagerDuty, email
        notify_slack(
            f"[ALERT] Regression on {data['agent_name']} {data['version_label']} "
            f"({data['metric']} dropped {data['delta']:+.2f})"
        )

    return {"received": True}

Retries and delivery

Each delivery attempt has a 5-second timeout. If your endpoint returns a non-2xx response or times out, the dispatch is recorded as failed and automatically retried — a background scheduler replays failed dispatches with backoff (and jitter), up to a few attempts, before giving up. Because retries can redeliver the same event, treat your handler as idempotent: deduplicate on the X-Decimal-Event-Id header so a replayed delivery is a no-op. If your webhook handler is slow or unreliable, return 200 immediately and process asynchronously. Anything longer than 5 seconds counts as a failed attempt and will be retried.

Disabling webhooks

Either:
  • Remove the webhook URL in Settings → Notifications, or
  • Set webhook_url to null via the API.
Notifications for events that have no configured channel are silently dropped (the events still happen and are visible in the dashboard).

Next Steps

Errors

HTTP status codes and how to handle them.

Roadmap

What’s shipped (HMAC signing, retry) and what’s still in flight.