Alerts to a webhook

On this page

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:

HeaderValue
X-BxLens-Eventfiring, reminder, resolved or report
X-BxLens-TimestampSeconds since 1970, when the request was made
X-BxLens-Signaturesha256= 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:

{
  "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

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

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 for how alerts are routed.