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

# Receiving webhooks

> Subscribe to Clausum events and verify signed deliveries.

Clausum notifies your systems of assess outcomes, fraud, cases, and disputes via signed HTTP `POST` requests. See [Webhook events](/concepts/webhook-events) for the full catalog of **11 events**.

## 1. Register an endpoint

**Dashboard (recommended)**

1. Open **Conexiones** → tab **Webhooks salientes** (sandbox or production).
2. Enter **HTTPS URL** and select events (all 11 types available).
3. Copy the **signing secret** shown once at creation.

**Production note:** creating a **production** webhook may require **approved live credentials** for your organization. Sandbox webhooks work immediately with `clm_*_sbx_*` keys.

**API (optional):** `GET/POST /api/v1/integrations/merchant-webhooks` with dashboard session JWT — same event list as the UI.

## 2. Sandbox vs production

| Environment | API keys                         | Webhook deliveries                      |
| ----------- | -------------------------------- | --------------------------------------- |
| Sandbox     | `clm_sk_sbx_*` / `clm_pub_sbx_*` | Separate URLs + secrets per environment |
| Production  | `clm_sk_*` / `clm_pub_*`         | Separate URLs + secrets                 |

Assess on sandbox keys triggers sandbox webhooks only.

## 3. Understand the delivery

| Header                | Example                   | Description    |
| --------------------- | ------------------------- | -------------- |
| `X-Clausum-Signature` | `t=1716998400,v1=8a9b...` | HMAC signature |
| `X-Clausum-Event`     | `transaction.created`     | Event type     |
| `X-Clausum-Timestamp` | `2026-07-05T18:30:00Z`    | Delivery time  |

Body:

```json theme={null}
{
  "event": "transaction.created",
  "timestamp": "2026-07-05T18:30:00.000Z",
  "data": {
    "session_id": "ps_...",
    "decision": "approve",
    "assess_response": { "decision": "approve", "session_id": "ps_..." }
  }
}
```

## 4. Verify the signature

`HMAC_SHA256(secret, "<timestamp>.<rawBody>")` → header `t=...,v1=<hex>`. **Verify the raw body** before JSON parse.

<CodeGroup>
  ```ts Node.js theme={null}
  import crypto from "crypto"

  export function verifyClausumSignature(rawBody: string, header: string, secret: string): boolean {
    const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")))
    const timestamp = parts["t"]
    const provided = parts["v1"]
    if (!timestamp || !provided) return false

    const expected = crypto
      .createHmac("sha256", secret)
      .update(`${timestamp}.${rawBody}`)
      .digest("hex")

    return crypto.timingSafeEqual(Buffer.from(provided), Buffer.from(expected))
  }
  ```

  ```python Python theme={null}
  import hmac, hashlib

  def verify_clausum_signature(raw_body: str, header: str, secret: str) -> bool:
      parts = dict(p.split("=", 1) for p in header.split(","))
      timestamp, provided = parts.get("t"), parts.get("v1")
      if not timestamp or not provided:
          return False
      expected = hmac.new(
          secret.encode(),
          f"{timestamp}.{raw_body}".encode(),
          hashlib.sha256,
      ).hexdigest()
      return hmac.compare_digest(provided, expected)
  ```
</CodeGroup>

## 5. Handle the event

```ts theme={null}
export async function POST(req: Request) {
  const raw = await req.text()
  const signature = req.headers.get("x-clausum-signature")

  if (!signature || !verifyClausumSignature(raw, signature, process.env.CLAUSUM_WEBHOOK_SECRET!)) {
    return new Response("Invalid signature", { status: 401 })
  }

  const event = JSON.parse(raw)

  switch (event.event) {
    case "transaction.created":
    case "transaction.blocked":
      await syncOrderFromAssess(event.data)
      break
    case "fraud.detected":
      await onFraudDetected(event.data)
      break
    case "dispute.created":
      await onDispute(event.data)
      break
  }

  return new Response("ok", { status: 200 })
}
```

## Monitor deliveries

**Conexiones → Monitor → Entregas webhook salientes** shows recent attempts, HTTP status, and retry state.

## Best practices

<AccordionGroup>
  <Accordion title="Verify against the raw body" icon="fingerprint">
    Read raw text first, verify, then parse JSON.
  </Accordion>

  <Accordion title="Respond quickly" icon="bolt">
    Return `2xx` within a few seconds; use a queue for heavy work.
  </Accordion>

  <Accordion title="Be idempotent" icon="rotate">
    Duplicates happen — key on `session_id` + `event` or `case_id`.
  </Accordion>

  <Accordion title="Subscribe narrowly" icon="filter">
    Start with `transaction.blocked`, `transaction.created`, and `fraud.detected`; add dispute events when your chargeback workflow is ready.
  </Accordion>
</AccordionGroup>
