Webhook signing and verification
Every outbound alert delivered to your endpoint carries an HMAC-SHA256 signature derived from the raw request body and your webhook secret. Verify the signature on every request; reject any request whose signature does not match.
The request
Breachtide sends a single POST with two request headers that carry the signing material:
X-Breachtide-Signaturestring
The value sha256=<hex digest>, where the digest is HMAC-SHA256(secret, raw_body).
Content-Typestring
Always application/json.
Payload shape
A representative JSON body:
POST /your-endpointContent-Type: application/json
{
"event": "alert.created",
"severity": "critical",
"address": "[email protected]",
"source": "combolist",
"summary": "new plaintext password in circulation",
"delta": {
"previous_count": 3,
"new_count": 4,
"new_source_dbs": ["ComboList2026Q2"],
"newly_exposed_fields": ["password"],
"has_new_plaintext": true,
"is_first_scan": false
},
"observed_at": "2026-05-07T14:22:08Z",
"dashboard_url": "https://breachtide.com/dashboard/emails/me_0193..."
}Top-level fields
eventenum
The event name. Reserved for future event types; today only one value is sent.alert.created
severityenum
How urgent the change is. Critical means rotate now.criticalwarninginfo
addressstring
The verified email address whose state changed.
sourceenum
Which feed produced the change.archivecombolistindexenriched
summarystring
A short human-readable description of the change.
deltaobject
Structured detail of what changed since the prior sweep. See the next table.
observed_atstring
ISO-8601 UTC timestamp of when the change was recorded.
dashboard_urlstring
Link to the affected email address in your Breachtide dashboard.
Delta fields
previous_countinteger
Number of distinct source databases on the prior sweep.
new_countinteger
Number of distinct source databases on the current sweep.
new_source_dbsstring[]
The named breaches or combolists that appeared since the previous sweep.
newly_exposed_fieldsenum[]
Newly exposed value categories.passwordhashusernamenamedobaddressphoneip
has_new_plaintextboolean
True when this delta introduced a plaintext password that was not present before.
is_first_scanboolean
True when this is the first sweep for the address; counts represent the initial state.
Verification
Compute HMAC-SHA256(secret, raw_body), prefix the hex digest with sha256=, and compare against the header in constant time. Constant-time comparison defends against timing-side-channel attacks.
Node.js (Express)
verify-webhook.jsNode 20+
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.BREACHTIDE_WEBHOOK_SECRET;
app.post(
'/breachtide-webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const received = req.header('x-breachtide-signature') ?? '';
const expected =
'sha256=' +
crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
const a = Buffer.from(received);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
handleAlert(event);
res.status(204).end();
},
);Python (Flask)
verify_webhook.pyPython 3.10+
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["BREACHTIDE_WEBHOOK_SECRET"].encode()
@app.post("/breachtide-webhook")
def webhook():
received = request.headers.get("X-Breachtide-Signature", "")
expected = "sha256=" + hmac.new(SECRET, request.data, hashlib.sha256).hexdigest()
if not hmac.compare_digest(received, expected):
abort(401)
handle_alert(request.get_json())
return "", 204Go
main.goGo 1.21+
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
)
var secret = []byte(os.Getenv("BREACHTIDE_WEBHOOK_SECRET"))
func webhook(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
mac := hmac.New(sha256.New, secret)
mac.Write(body)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
received := r.Header.Get("X-Breachtide-Signature")
if !hmac.Equal([]byte(received), []byte(expected)) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusNoContent)
}Operational notes
- Sign the raw request body, byte-for-byte. Re-serializing the parsed JSON will produce a different digest and the comparison will fail.
- Breachtide retries on 408, 429, and 5xx, up to two additional attempts. Treat your handler as idempotent; the same alert may arrive twice if your endpoint times out.
- Request timeout is 10 seconds. If your handler needs to do heavy work, return 204 immediately and process out of band.
- Rotate the secret from /dashboard/settings. The old secret is invalidated as soon as the new one is generated; deploy the replacement before rotating.
- Webhook delivery is gated on the Pro and Business plans. Free and Personal subscribers receive email alerts only.
Webhook delivery is part of Pro.
Set a URL, generate a secret, and verify with the snippets above.
See pricing