---
title: Alerts to a webhook
order: 9
description: Send BxLens alerts and reports to your own endpoint as signed JSON, with the payload and how to verify the signature in BoxLang and Python.
icon: lucide:webhook
---

# Alerts to a webhook

A webhook channel posts a JSON document to an https address of yours for every alert and report. Use it to feed a pager, a ticket system or your own code. It needs **BoxLang+**.

## Add it

1. Open **Settings, Notifications, Channels**, press **+ Add channel** and choose **Webhook**.
2. Paste the address. It must be https and must resolve to a public address. It is stored encrypted and never shown again; the list shows its host.
3. Enter a **signing key** (a long random string, 8 to 200 characters). The **Generate** button makes one; copy it before you save, because it is never shown again. The same key goes into your receiver. Without a key the requests are not signed, and the list says `unsigned`.
4. Choose the alerts, the lowest severity and the reports, press **Save**, then **Send test**.

## The request

`POST` with `Content-Type: application/json` and these headers:

| Header | Value |
|---|---|
| `X-BxLens-Event` | `firing`, `reminder`, `resolved` or `report` |
| `X-BxLens-Timestamp` | Seconds since 1970, when the request was made |
| `X-BxLens-Signature` | `sha256=` and the lower case hex of the HMAC-SHA256 of the **exact body bytes** with the signing key (only when the channel has a key) |

The body:

```json
{
  "version": 1,
  "kind": "firing",
  "rule": "datasource-pool",
  "severity": "critical",
  "title": "Datasource pool exhausted",
  "summary": "Datasource orders has 20 of 20 connections in use, 7 requests are waiting.",
  "server": "web-02",
  "since": "2026-10-20T14:32:00Z",
  "facts": [ { "label": "About", "value": "orders" } ],
  "link": "https://lens.acme.com/~bxlens"
}
```

- `severity` is `critical`, `warning` or `info` (a report).
- A report has `"kind": "report"` and `"period": "daily"` (or `weekly`, `monthly`) instead of `rule`.
- `link` is there only when `console.publicUrl` is set. A test message has `"test": true`.
- The body is at most 64 KB. Text is cut with three dots.

Answer with any `2xx` status. A `429` or `5xx`, a timeout or a connection failure is tried again after 5 minutes. Another status (a `404`, a `401`) is not retried until you fix the channel, and the list shows the answer. BxLens does not follow redirects, waits 5 seconds to connect and 10 seconds for the answer, and reads at most 64 KB of it.

## Verify the signature

Compute the HMAC of the raw body, compare it in constant time, and check the timestamp if you want to refuse old requests.

### BoxLang

```boxlang
// The raw body, exactly as it arrived
body     = getHTTPRequestData().content
sent     = getHTTPRequestData().headers[ "X-BxLens-Signature" ] ?: ""
expected = "sha256=" & lCase( hmac( body, application.bxlensKey, "HmacSHA256", "UTF-8" ) )

// A constant time comparison
valid = createObject( "java", "java.security.MessageDigest" ).isEqual(
	expected.getBytes( "UTF-8" ),
	sent.getBytes( "UTF-8" )
)

if ( !valid ) {
	bx:header statusCode="401" statusText="Invalid signature";
	bx:abort;
}
event = jsonDeserialize( body )
```

### Python

```python
import hashlib, hmac, os
from flask import Flask, request, abort

app = Flask(__name__)
KEY = os.environ["BXLENS_KEY"].encode()

@app.post("/bxlens")
def bxlens():
    body = request.get_data()                       # the raw bytes
    expected = "sha256=" + hmac.new(KEY, body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.headers.get("X-BxLens-Signature", "")):
        abort(401)
    event = request.get_json()
    return "", 204
```

A known test vector: the key `key` and the body `The quick brown fox jumps over the lazy dog` give `sha256=f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8`.

The signature covers the body only. To refuse a request that someone recorded and sent again, also compare `X-BxLens-Timestamp` with your clock and refuse anything older than a few minutes.

See [Notifications](../console/notifications.md#channels) for how alerts are routed.
