> ## Documentation Index
> Fetch the complete documentation index at: https://docs.decimal.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive HTTP notifications when manifests change, regressions are detected, or usage crosses thresholds.

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.

<Note>
  Webhooks support **HMAC signing** (`X-Decimal-Signature`) for verifiable delivery and **automatic retry with backoff** on failure. When a signing secret is configured for your org, deliveries are signed so you can [verify them](#verifying-webhooks); to enable signing, email [support@decimal.ai](mailto:support@decimal.ai) — there is no self-serve secret setting yet. Delivery is still subject to a 5-second per-attempt timeout — acknowledge fast and process asynchronously.
</Note>

## Setup

Configure webhooks with the `PATCH /api/v1/org/notifications` API (admin role). There is no dashboard UI for this yet.

* **Webhook URL** — the HTTPS endpoint that receives events (must be `https://`).
* **Enabled events** — which event types to send. Defaults to all events.

```bash theme={null}
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

| Event                 | Fires when                                                                                           |
| --------------------- | ---------------------------------------------------------------------------------------------------- |
| `manifest.changed`    | A new manifest version is registered for an agent (new tools, prompt sections, model, or skill set). |
| `regression.detected` | The [post-deploy bisect](/guides/post-deploy-bisect) view marks a manifest version as a regression.  |
| `regression.resolved` | A previously-flagged regression is marked resolved (rollback or fix shipped).                        |
| `usage.warning`       | Your org has consumed 80% of a plan limit (traces ingested, SFT rows).                               |
| `usage.limit_reached` | Your org has consumed 100% of a plan limit. Further requests return `402`.                           |
| `payment.failed`      | Stripe reports a failed payment. Grace period starts; service degrades after the grace window.       |
| `payment.confirmed`   | Stripe confirms a successful payment.                                                                |

## Payload format

Every webhook body has the same envelope. The `data` field carries event-specific fields.

```json theme={null}
{
  "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:

<AccordionGroup>
  <Accordion title="manifest.changed">
    ```json theme={null}
    {
      "agent_name": "support-agent",
      "version_label": "v3",
      "previous_version_label": "v2",
      "manifest_id": "mfst_abc123",
      "traces_affected": 450
    }
    ```
  </Accordion>

  <Accordion title="regression.detected / regression.resolved">
    ```json theme={null}
    {
      "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"]
    }
    ```
  </Accordion>

  <Accordion title="usage.warning / usage.limit_reached">
    ```json theme={null}
    {
      "metric": "traces_ingested",
      "current": 4250,
      "limit": 5000,
      "plan": "free",
      "period_start": "2026-05-01T00:00:00Z",
      "period_end": "2026-06-01T00:00:00Z"
    }
    ```
  </Accordion>

  <Accordion title="payment.failed / payment.confirmed">
    ```json theme={null}
    {
      "stripe_invoice_id": "in_1Abc...",
      "amount": 2900,
      "currency": "usd",
      "grace_period_ends": "2026-05-20T00:00:00Z"
    }
    ```
  </Accordion>
</AccordionGroup>

## Headers

Every webhook request includes:

| Header                | Value                                                                                                                              |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `Content-Type`        | `application/json`                                                                                                                 |
| `User-Agent`          | `DecimalAI-Webhooks/1.0`                                                                                                           |
| `X-Decimal-Event`     | The event name (e.g., `regression.detected`)                                                                                       |
| `X-Decimal-Event-Id`  | Unique ID for this delivery — use it to deduplicate retries.                                                                       |
| `X-Decimal-Signature` | `sha256=<hex>` HMAC over the raw body, present when a webhook secret is configured. See [Verifying webhooks](#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. To set or rotate the signing secret, email [support@decimal.ai](mailto:support@decimal.ai).

<CodeGroup>
  ```python Python theme={null}
  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)
  ```

  ```js Node theme={null}
  import crypto from "crypto";

  function verify(bodyBytes, signatureHeader, secret) {
    // Constant-time HMAC verification against the X-Decimal-Signature header.
    const expected =
      "sha256=" +
      crypto.createHmac("sha256", secret).update(bodyBytes).digest("hex");
    const a = Buffer.from(expected);
    const b = Buffer.from(signatureHeader || "");
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  }

  // In your webhook handler — hash the RAW body, not the re-serialized JSON.
  // Use express.raw() so req.body is the original bytes:
  const sig = req.headers["x-decimal-signature"] || "";
  if (!verify(req.body, sig, process.env.DECIMAL_WEBHOOK_SECRET)) {
    return res.status(401).end();
  }
  ```
</CodeGroup>

<Note>
  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.
</Note>

## Receiving — example handlers

<CodeGroup>
  ```python Python (FastAPI) theme={null}
  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}
  ```

  ```js Node (Express) theme={null}
  import express from "express";
  const app = express();
  app.use(express.json());

  app.post("/decimalai/hook", (req, res) => {
    const { event, data } = req.body;

    if (event === "regression.detected") {
      notifySlack(
        `[ALERT] Regression on ${data.agent_name} ${data.version_label} ` +
        `(${data.metric} dropped ${data.delta.toFixed(2)})`
      );
    }

    res.json({ received: true });
  });
  ```
</CodeGroup>

## 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

Set `webhook_url` to `null` via the `PATCH /api/v1/org/notifications` API.

Notifications for events that have no configured channel are silently dropped (the events still happen and are visible in the dashboard).

## Next Steps

<CardGroup cols={2}>
  <Card title="Errors" icon="circle-question" href="/api-reference/errors">
    HTTP status codes and how to handle them.
  </Card>

  <Card title="Roadmap" icon="flag-checkered" href="/roadmap">
    What's shipped (HMAC signing, retry) and what's still in flight.
  </Card>
</CardGroup>
