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

# Webhooks

> Alert notifications and signature verification

MeshQu can send real-time HTTP notifications when alerts are created. You subscribe to events by registering a webhook URL.

## Supported events

| Event           | Trigger                                               |
| --------------- | ----------------------------------------------------- |
| `alert.created` | A new alert is raised (e.g. critical policy failure). |

## Creating a subscription

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.meshqu.com/v1/alerts/subscriptions \
    -H "Authorization: Bearer mqu_YOUR_API_KEY" \
    -H "X-MeshQu-Tenant-Id: YOUR_TENANT_ID" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://your-service.com/webhooks/meshqu",
      "decision_types": ["trade_execution"],
      "severity_min": "high"
    }'
  ```

  ```typescript TypeScript theme={null}
  const sub = await client.createSubscription(
    'https://your-service.com/webhooks/meshqu',
    { decision_types: ['trade_execution'], severity_min: 'high' },
  );
  ```
</CodeGroup>

The create response includes a `secret` that is shown **only once**:

```json theme={null}
{
  "subscription": {
    "id": "660e8400-e29b-41d4-a716-446655440000",
    "tenant_id": "123e4567-e89b-12d3-a456-426614174000",
    "webhook_url": "https://your-service.com/webhooks/meshqu",
    "decision_types": ["trade_execution"],
    "severity_min": "high",
    "is_active": true,
    "has_secret": true,
    "created_at": "2026-01-01T10:00:00.000Z"
  },
  "secret": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"
}
```

<Warning>
  **The `secret` is returned exactly once.** Save it to your secrets manager immediately — it is encrypted at rest on our side (AES-256-GCM) and cannot be retrieved again. If you lose it, delete the subscription and create a new one to obtain a fresh secret.
</Warning>

### Filtering: which alerts you receive

A subscription delivers an alert only if it matches **both** filters.

`severity_min` sets the minimum severity (inclusive):

| `severity_min` | Delivers               |
| -------------- | ---------------------- |
| `low`          | all alerts             |
| `medium`       | medium, high, critical |
| `high`         | high, critical         |
| `critical`     | critical only          |

`decision_types` restricts delivery to alerts arising from specific decision types. Omit it (or pass an empty array) to receive alerts from every decision type. A critical-only, type-scoped subscription is the typical shape for paging on-call:

```json theme={null}
{
  "url": "https://your-service.com/webhooks/oncall",
  "decision_types": ["trade_execution", "fraud_check"],
  "severity_min": "critical"
}
```

<Info>
  **SSRF protection.** The webhook URL must be HTTPS in production and its host must resolve to a publicly-routable address. URLs that resolve to private, loopback, link-local, cloud-metadata, or otherwise reserved ranges are rejected — both when you create the subscription **and** again at delivery time. If a legitimate endpoint is being rejected, this is operator-only allowlist configuration, never self-service — [contact us](mailto:contact@meshqu.com).
</Info>

## Payload format

When an event fires, MeshQu sends a `POST` request to your URL:

```json theme={null}
{
  "event": "alert.created",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "alert": {
    "id": "alert-uuid",
    "alert_type": "policy_violation",
    "severity": "critical",
    "message": "Policy max-order-size returned DENY for decision dec-uuid.",
    "decision_id": "dec-uuid",
    "created_at": "2025-01-15T10:30:00.000Z"
  }
}
```

## Request headers

Every webhook request includes these headers:

| Header                 | Description                            |
| ---------------------- | -------------------------------------- |
| `X-MeshQu-Event`       | Event type (e.g. `alert.created`).     |
| `X-MeshQu-Delivery-Id` | Unique delivery UUID.                  |
| `X-MeshQu-Tenant-Id`   | Tenant UUID.                           |
| `X-MeshQu-Timestamp`   | Unix timestamp in milliseconds.        |
| `X-MeshQu-Attempt`     | Delivery attempt number (starts at 1). |
| `X-MeshQu-Signature`   | HMAC signature for verification.       |
| `Content-Type`         | `application/json`                     |

## Verifying signatures

Each delivery is signed with HMAC-SHA256. The signature header has the format:

```
X-MeshQu-Signature: sha256=<hex_digest>
```

To verify:

1. Concatenate the timestamp and payload: `{timestamp}.{json_body}`
2. Compute HMAC-SHA256 using your webhook secret.
3. Compare with the signature in the header.
4. Reject requests where the timestamp is more than 5 minutes old (replay protection).

```typescript theme={null}
import { createHmac, timingSafeEqual } from 'crypto';

function verifyWebhook(
  body: string,
  signature: string,
  timestamp: string,
  secret: string,
): boolean {
  const signatureBase = `${timestamp}.${body}`;
  const expected = createHmac('sha256', secret)
    .update(signatureBase)
    .digest('hex');

  const received = signature.replace('sha256=', '');

  // Timing-safe comparison
  return timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(received, 'hex'),
  );
}
```

The same verification in Python:

```python theme={null}
import hmac
import hashlib
import time

def verify_webhook(body: str, signature: str, timestamp: str, secret: str,
                   max_age_ms: int = 5 * 60 * 1000) -> bool:
    # 1. Replay protection: reject stale timestamps.
    try:
        ts_ms = int(timestamp)
    except ValueError:
        return False
    if (time.time() * 1000 - ts_ms) > max_age_ms:
        return False

    # 2. Recompute and compare.
    signature_base = f"{timestamp}.{body}"
    expected = hmac.new(secret.encode(), signature_base.encode(), hashlib.sha256).hexdigest()
    received = signature.replace("sha256=", "")
    return hmac.compare_digest(expected, received)
```

<Warning>
  Verify against the **raw request body**, byte for byte — not a re-serialized parse of it. Re-encoding JSON can reorder keys or change whitespace and will break the signature. Read and verify the raw body first, then parse it.
</Warning>

## Retry behaviour

If your endpoint returns a non-2xx status or times out, MeshQu retries with exponential backoff:

| Attempt | Delay        |
| ------- | ------------ |
| 1       | Immediate    |
| 2       | \~1 second   |
| 3       | \~2 seconds  |
| 4       | \~4 seconds  |
| 5       | \~8 seconds  |
| 6       | \~16 seconds |

After the maximum attempts are exhausted, the delivery moves to a dead-letter queue and the failure reason is recorded on the delivery record.

## Inspecting and replaying deliveries

Every delivery attempt is persisted, so you can monitor and debug without trawling logs. Each delivery has one of these statuses:

| Status      | Meaning                                              |
| ----------- | ---------------------------------------------------- |
| `pending`   | Awaiting first delivery or a scheduled retry.        |
| `delivered` | Successfully delivered (your endpoint returned 2xx). |
| `failed`    | Last attempt failed; a retry is scheduled.           |
| `dead`      | Permanently failed after the maximum attempts.       |

```bash theme={null}
# List deliveries (filter by status or subscription_id)
GET /v1/alerts/deliveries

# Aggregate delivery statistics
GET /v1/alerts/deliveries/stats

# Inspect a single delivery
GET /v1/alerts/deliveries/:id
```

If a delivery is `failed` or `dead` — for example after you fixed an outage on your side — you can force an immediate retry. This resets the attempt count and schedules delivery right away:

```bash theme={null}
curl -X POST https://api.meshqu.com/v1/alerts/deliveries/DELIVERY_ID/retry \
  -H "Authorization: Bearer mqu_YOUR_API_KEY" \
  -H "X-MeshQu-Tenant-Id: YOUR_TENANT_ID"
```

Retrying a delivery that is already `delivered` returns `400 ALREADY_DELIVERED`; retrying one still `pending` returns `400 STILL_PENDING`.

## Timeout

Your endpoint must respond within **10 seconds**. Longer processing should be handled asynchronously (accept the webhook, enqueue, process later).

## Handling a webhook end to end

A robust handler verifies the signature against the raw body, responds fast, and processes out of band:

```typescript theme={null}
import express from 'express';

const app = express();

// Capture the RAW body — verification must run against the exact bytes sent.
app.post(
  '/webhooks/meshqu',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const body = req.body.toString();
    const signature = req.header('X-MeshQu-Signature') ?? '';
    const timestamp = req.header('X-MeshQu-Timestamp') ?? '';

    if (!verifyWebhook(body, signature, timestamp, process.env.MESHQU_WEBHOOK_SECRET!)) {
      return res.status(401).send('Invalid signature');
    }

    // Respond immediately, then process asynchronously.
    res.status(200).send('OK');

    const { alert } = JSON.parse(body);
    enqueueForProcessing(alert); // your own queue / worker
  },
);
```

## Managing subscriptions

```bash theme={null}
# List
GET /v1/alerts/subscriptions

# Delete
DELETE /v1/alerts/subscriptions/:id
```

To change a subscription's filters, delete it and create a new one. Deleting a subscription stops further deliveries to that endpoint.

## Best practices

* **Return 200 quickly.** Acknowledge receipt and process asynchronously.
* **Verify signatures.** Always validate `X-MeshQu-Signature` before trusting the payload.
* **Handle duplicates.** Use `X-MeshQu-Delivery-Id` to deduplicate if your handler is not idempotent.
* **Monitor delivery stats.** Check the `/deliveries/stats` endpoint periodically to catch failures early.
