Building a Resolution-Risk Dashboard with SettleRisk Webhooks
Executive Summary
The fastest way to keep a desk's view of resolution risk current is to subscribe to webhooks. SettleRisk publishes seven typed event families covering score changes, rule changes, dispute lifecycle, and URL health. This post walks through HMAC verification, idempotency on event_id, the seven event types, and a working Node.js consumer ready to drop into an Express server.
Core Concept
The seven webhook event families:
| Event family | Trigger |
|--------------|---------|
| score.tier_changed | Risk score crosses a tier boundary |
| score.driver_added | A new driver appeared on re-extraction |
| score.driver_removed | A driver dropped off on re-extraction |
| rules.changed | Rules text changed on the venue |
| dispute.opened | Dispute was filed against the market |
| dispute.resolved | Dispute closed (with or without outcome change) |
| url.health_changed | Source URL crossed a health threshold |
Every payload is signed with HMAC-SHA256 over the raw body using the webhook's secret. Replay-safe consumers dedupe on the event_id and timestamp.
header: X-Vellox-Webhook-Signature
secret: <webhook_secret>
sig = HMAC-SHA256(secret, timestamp + "\n" + raw_body_bytes)
Max acceptable clock skew is 300 seconds.
Worked Example
A Node.js consumer using Express:
import express from "express";
import crypto from "node:crypto";
const app = express();
// Important: capture raw body for HMAC verification
app.use(
express.json({
verify: (req: any, _res, buf) => {
req.rawBody = buf;
},
})
);
const WEBHOOK_SECRET = process.env.SETTLERISK_WEBHOOK_SECRET!;
function verify(req: any): boolean {
const sigHeader = req.header("X-Vellox-Webhook-Signature") ?? "";
const tsHeader = req.header("X-Vellox-Webhook-Timestamp") ?? "";
if (!sigHeader || !tsHeader) return false;
const skew = Math.abs(Date.now() / 1000 - parseInt(tsHeader, 10));
if (skew > 300) return false;
const signingInput = Buffer.concat([
Buffer.from(tsHeader + "\n", "utf-8"),
req.rawBody,
]);
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(signingInput)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(sigHeader, "utf-8"),
Buffer.from(expected, "utf-8")
);
}
const seenEventIds = new Set<string>();
app.post("/webhooks/settlerisk", (req, res) => {
if (!verify(req)) return res.status(401).send("invalid signature");
const { event_id, event_type, data } = req.body;
if (seenEventIds.has(event_id)) return res.status(200).send("duplicate");
seenEventIds.add(event_id);
switch (event_type) {
case "score.tier_changed":
console.log(`${data.market_id}: ${data.old_tier} -> ${data.new_tier}`);
// Trigger re-pricing in your engine
break;
case "rules.changed":
console.log(`Rules changed on ${data.market_id}`);
// Invalidate cached score
break;
case "dispute.opened":
console.log(`Dispute opened on ${data.market_id}`);
// Lock the position; do not trade
break;
default:
console.log(`Unhandled event: ${event_type}`);
}
res.status(200).send("ok");
});
app.listen(3000);
Python equivalent for a Flask consumer:
import os, hmac, hashlib, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["SETTLERISK_WEBHOOK_SECRET"].encode()
SEEN_EVENTS = set()
@app.post("/webhooks/settlerisk")
def receive():
sig = request.headers.get("X-Vellox-Webhook-Signature", "")
ts = request.headers.get("X-Vellox-Webhook-Timestamp", "0")
if abs(time.time() - int(ts)) > 300:
abort(401, "clock skew")
expected = hmac.new(SECRET, f"{ts}\n".encode() + request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401, "bad signature")
payload = request.get_json()
event_id = payload["event_id"]
if event_id in SEEN_EVENTS:
return "duplicate", 200
SEEN_EVENTS.add(event_id)
if payload["event_type"] == "score.tier_changed":
# Trigger reprice
pass
return "ok", 200
Implementation Notes
Always verify HMAC. A consumer that doesn't verify can be poisoned by anyone who guesses your endpoint URL. Use crypto.timingSafeEqual (Node) or hmac.compare_digest (Python) to avoid timing attacks.
Dedupe on event_id, not on payload hash. SettleRisk delivery is at-least-once. The same event_id can arrive twice; the same payload can be a legitimate second event with a different event_id. Trust the ID.
Use exponential backoff on retry. If your consumer returns non-2xx, the webhook delivery engine retries with exponential backoff up to 24 hours. Don't accidentally NACK a valid event.
Persist event_id with TTL. In-memory Set works for low volume; high-volume consumers should use Redis with a 7-day TTL on each event_id.
| Concern | Recommendation |
|---------|----------------|
| Signature verification | HMAC-SHA256, timing-safe compare |
| Replay protection | Dedupe on event_id, 7-day TTL |
| Clock skew | Reject if > 300s |
| Retry behavior | 200 = ack, 4xx = no retry, 5xx = retry |
| Async processing | Return 200 immediately, queue heavy work |
Failure Modes
1. Verifying against parsed JSON. The signature is over the raw bytes. JSON.parse + JSON.stringify changes whitespace and order and breaks signature verification. Capture the raw body.
2. Holding the worker thread during processing. Webhook handlers must return 200 quickly. Push real work to a queue; do not block the HTTP response.
3. Re-processing on duplicate. Even with at-least-once delivery, repeated score.tier_changed events for the same market should not double-trigger a reprice.
4. Ignoring health events. url.health_changed is the early-warning signal for score.tier_changed on oracle-dependent markets. Subscribing to both gives you minutes of lead time.
5. Not handling replay endpoints. Operators replay webhooks during incident recovery. A replayed event has a new delivery_id but the same event_id. Dedupe correctly so replay doesn't double-process.
Checklist
- [ ] HMAC-SHA256 verification with timing-safe compare
- [ ] Capture raw body before JSON parse
- [ ] Dedupe on
event_idwith 7-day TTL - [ ] Return 200 in <500ms; queue heavy work
- [ ] Subscribe to both
score.tier_changedandurl.health_changed - [ ] Test replay handling end-to-end
Sources + Further Reading
- SettleRisk docs — webhook event schemas and delivery guarantees
- Webhook event reference — full payload examples
- Idempotency in Distributed Systems — Stripe Engineering blog (2017)
- RFC 6234 — HMAC-SHA256 specification
Try webhooks free on the Builder tier: /pricing for the breakdown.
Get weekly risk analysis in your inbox
Market risk scores, emerging dispute patterns, and settlement delay trends — delivered every Monday.