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

# Webhooks

> Inbound webhooks (third-party systems → Causeloop, HMAC-verified) and outbound webhooks (Causeloop → your systems, signed). Includes signature verification code examples.

Causeloop has two directions of webhook traffic:

* **Inbound** — a third-party system posts events to Causeloop. Causeloop verifies the HMAC signature and ingests the payload.
* **Outbound** — Causeloop posts events to your endpoint when something happens (sync completed, issue ingested, pattern detected). Your endpoint verifies the signature.

## Inbound webhooks

Inbound webhooks let any system push events directly into Causeloop without polling. The endpoint authenticates callers via HMAC-SHA256 — there is no Bearer token. The endpoint **fails closed**: a missing or invalid signature always returns `401`.

### Endpoint

```
POST /v1/webhooks/inbound/{connector_id}
```

This endpoint is public (no Bearer auth). The `connector_id` identifies which connector's signing secret to use for verification.

### Required headers

| Header                  | Description                                                |
| ----------------------- | ---------------------------------------------------------- |
| `x-causeloop-signature` | HMAC-SHA256 hex digest of the raw request body. Preferred. |
| `x-hub-signature-256`   | Alternative: `sha256=<hex>` format (GitHub-compatible).    |

Provide one of the two headers. If you use `x-hub-signature-256`, prefix the digest with `sha256=`.

### How signature verification works

<Steps>
  <Step title="Get the signing secret">
    When you configure an inbound webhook for a connector, Causeloop generates a `signing_secret` (e.g. `whsec_a1b2c3...`). Store it securely in your source system.
  </Step>

  <Step title="Sign the raw body">
    Before sending, compute `HMAC-SHA256(signing_secret, raw_body)` and include the hex digest in the `x-causeloop-signature` header.
  </Step>

  <Step title="Causeloop verifies">
    Causeloop buffers the raw request bytes (before any JSON parsing), recomputes the HMAC using the stored secret, and compares with a constant-time comparison. A mismatch, missing signature, or missing secret all return `401`.
  </Step>
</Steps>

<Warning>
  The HMAC is computed over the **raw bytes** of the request body. Do not parse, pretty-print, or re-serialize the JSON before signing — the byte sequence must be identical to what Causeloop receives.
</Warning>

### Signing an outgoing payload (sender side)

<CodeGroup>
  ```python Python theme={null}
  import hashlib
  import hmac
  import json
  import httpx

  SIGNING_SECRET = "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
  CONNECTOR_ID   = "con_01hx4k2m9p"

  payload = {"event_type": "alert.fired", "alert_id": "ALT-9999", "severity": "critical"}
  body = json.dumps(payload, separators=(",", ":")).encode()

  signature = hmac.new(
      SIGNING_SECRET.encode("utf-8"),
      body,
      hashlib.sha256,
  ).hexdigest()

  resp = httpx.post(
      f"https://api.causeloop.ai/v1/webhooks/inbound/{CONNECTOR_ID}",
      content=body,
      headers={
          "Content-Type": "application/json",
          "x-causeloop-signature": signature,
      },
  )
  print(resp.status_code)  # 202
  ```

  ```javascript JavaScript theme={null}
  import crypto from 'crypto';

  const SIGNING_SECRET = 'whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6';
  const CONNECTOR_ID   = 'con_01hx4k2m9p';

  const payload = { event_type: 'alert.fired', alert_id: 'ALT-9999', severity: 'critical' };
  const body = JSON.stringify(payload);

  const signature = crypto
    .createHmac('sha256', SIGNING_SECRET)
    .update(body)
    .digest('hex');

  const resp = await fetch(
    `https://api.causeloop.ai/v1/webhooks/inbound/${CONNECTOR_ID}`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-causeloop-signature': signature,
      },
      body,
    }
  );
  console.log(resp.status); // 202
  ```
</CodeGroup>

### Inbound webhook response

**202 Accepted** — payload received, signature valid, delivery recorded.

```json theme={null}
{
  "received": true,
  "delivery_id": "del_01hx5d4e5f",
  "connector_id": "con_01hx4k2m9p"
}
```

**401 Unauthorized** — missing or invalid signature, or no signing secret configured for this connector.

### Registering an inbound webhook

To enable inbound delivery for a connector, create an outbound webhook record that references the connector. Causeloop auto-generates the signing secret:

```bash theme={null}
curl -X POST https://api.causeloop.ai/v1/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.causeloop.ai/v1/webhooks/inbound/con_01hx4k2m9p",
    "connector_id": "con_01hx4k2m9p",
    "description": "PagerDuty inbound events"
  }'
```

The response includes `signing_secret` — copy it now. Causeloop does not show it again (use the rotate endpoint to retrieve a new one).

***

## Outbound webhooks

Outbound webhooks let Causeloop notify your systems when events occur. Causeloop signs every delivery with `x-causeloop-signature`. You verify the signature on your end before processing.

### Create an outbound webhook

```bash theme={null}
curl -X POST https://api.causeloop.ai/v1/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.example.com/hooks/causeloop",
    "event_types": ["connector.sync.failed", "connector.sync.succeeded", "issue.ingested"],
    "description": "Alert on sync failures"
  }'
```

Response (201 Created):

```json theme={null}
{
  "id": "wh_01hx5c3d4e",
  "url": "https://your-app.example.com/hooks/causeloop",
  "event_types": ["connector.sync.failed", "connector.sync.succeeded", "issue.ingested"],
  "enabled": true,
  "signing_secret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  "created_at": "2026-06-14T10:00:00Z"
}
```

<Warning>
  The `signing_secret` is only returned at creation and after rotation. Store it securely immediately.
</Warning>

### Webhook event types

| Event                        | Fired when                                                |
| ---------------------------- | --------------------------------------------------------- |
| `connector.sync.succeeded`   | An incremental or full sync completed successfully        |
| `connector.sync.failed`      | A sync run failed                                         |
| `connector.inbound.received` | An inbound webhook payload was verified and accepted      |
| `issue.ingested`             | A single issue was ingested (API push or inbound webhook) |
| `issue.batch_ingested`       | A batch ingest job completed                              |
| `webhook.created`            | A new webhook was registered                              |
| `webhook.secret.rotated`     | A webhook signing secret was rotated                      |

Leave `event_types` empty or omit it to receive all events.

### Verifying outbound webhook signatures

Every outbound delivery includes `x-causeloop-signature: <hex>` computed as `HMAC-SHA256(signing_secret, raw_body)`.

<CodeGroup>
  ```python Python theme={null}
  import hashlib
  import hmac
  import json
  from fastapi import Request, HTTPException

  async def verify_causeloop_webhook(request: Request, secret: str) -> dict:
      raw_body = await request.body()
      provided_sig = request.headers.get("x-causeloop-signature", "")

      expected_sig = hmac.new(
          secret.encode("utf-8"),
          raw_body,
          hashlib.sha256,
      ).hexdigest()

      if not hmac.compare_digest(expected_sig, provided_sig):
          raise HTTPException(status_code=401, detail="Invalid signature")

      return json.loads(raw_body)
  ```

  ```javascript JavaScript (Express) theme={null}
  import crypto from 'crypto';

  function verifyCauseloopWebhook(req, res, next) {
    const secret = process.env.CAUSELOOP_WEBHOOK_SECRET;
    const provided = req.headers['x-causeloop-signature'] ?? '';

    // req.rawBody must be populated — use express.raw() on this route
    const expected = crypto
      .createHmac('sha256', secret)
      .update(req.rawBody)
      .digest('hex');

    if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided))) {
      return res.status(401).json({ error: 'Invalid signature' });
    }
    next();
  }
  ```

  ```typescript TypeScript (Next.js App Router) theme={null}
  import { headers } from 'next/headers';
  import crypto from 'crypto';

  export async function POST(request: Request) {
    const secret = process.env.CAUSELOOP_WEBHOOK_SECRET!;
    const rawBody = await request.text();
    const provided = (await headers()).get('x-causeloop-signature') ?? '';

    const expected = crypto
      .createHmac('sha256', secret)
      .update(rawBody)
      .digest('hex');

    if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided))) {
      return Response.json({ error: 'Invalid signature' }, { status: 401 });
    }

    const event = JSON.parse(rawBody);
    // handle event...
    return Response.json({ received: true });
  }
  ```
</CodeGroup>

<Warning>
  Always use a **constant-time comparison** (`hmac.compare_digest` in Python, `crypto.timingSafeEqual` in Node). Standard string equality is vulnerable to timing attacks.
</Warning>

### Rotating the signing secret

```bash theme={null}
curl -X POST https://api.causeloop.ai/v1/webhooks/wh_01hx5c3d4e/rotate \
  -H "Authorization: Bearer $TOKEN"
```

Response:

```json theme={null}
{
  "signing_secret": "whsec_newSecretHere...",
  "rotated_at": "2026-06-14T11:00:00Z"
}
```

Update your endpoint's secret immediately — the old secret stops working once the new one is issued.

### Viewing delivery history

```bash theme={null}
curl https://api.causeloop.ai/v1/webhooks/wh_01hx5c3d4e/deliveries \
  -H "Authorization: Bearer $TOKEN"
```

Each delivery record includes:

| Field             | Description                              |
| ----------------- | ---------------------------------------- |
| `id`              | Unique delivery ID                       |
| `event_type`      | The event that triggered the delivery    |
| `status_code`     | HTTP response code from your endpoint    |
| `delivered_at`    | ISO 8601 delivery timestamp              |
| `duration_ms`     | Round-trip time in milliseconds          |
| `payload_preview` | First 200 bytes of the delivered payload |

### Managing webhooks

```bash theme={null}
# List all webhooks
GET /v1/webhooks

# Update a webhook
PATCH /v1/webhooks/{id}   # fields: url, event_types, enabled, description

# Delete a webhook
DELETE /v1/webhooks/{id}
```

Required scopes: `webhooks:read` for GET endpoints, `webhooks:admin` for POST/PATCH/DELETE.
