Webhooks and HMAC
3 min read
Webhook actions let your TrustLens rules push events to external systems — CRMs, helpdesks, analytics, custom internal tools. Every webhook is signed with HMAC-SHA256 so the receiver can verify the request came from your TrustLens instance and wasn’t tampered with. This page covers the webhook payload format, the signature scheme, and how to implement receivers correctly.
Webhook Action Configuration #
In a rule’s action editor, choose Fire Webhook and configure:
| Field | Description |
|---|---|
| URL | Full HTTPS URL of the receiver endpoint |
| Secret | HMAC signing key. Auto-generated if blank; can be regenerated |
| Custom headers | Optional key-value pairs added to the request |
| Timeout | Defaults to 10 seconds |
The secret is shown once when generated, then masked. Save it somewhere safe — you’ll need it on the receiver side.
Request Format #
Each webhook is an HTTP POST with:
- Content-Type:
application/json - Header
X-TrustLens-Event: trigger event ID (e.g.chargeback_filed) - Header
X-TrustLens-Signature: HMAC-SHA256 hex digest of the raw body, prefixed withsha256= - Header
X-TrustLens-Delivery: unique delivery ID for deduplication - Header
X-TrustLens-Timestamp: Unix timestamp at dispatch time - Header
User-Agent:TrustLens/{version}
Payload Schema #
The body is a JSON object with these top-level fields:
{
"event": "chargeback_filed",
"delivery_id": "uuid-...",
"timestamp": 1710000000,
"rule": {
"id": 42,
"name": "Auto-block on dispute"
},
"data": {
"customer": { ... },
"order": { ... },
"dispute": { ... }
}
}
The data object’s shape depends on the trigger. Each trigger documents which sub-objects (customer, order, dispute, fingerprint, etc.) are included.
Signature Verification #
On the receiver side, verify the signature before trusting the payload. Reference implementation in pseudocode:
function verify(request) {
body = request.raw_body // exact bytes
signature_header = request.headers['x-trustlens-signature']
expected = "sha256=" + hmac_sha256_hex(secret, body)
return constant_time_compare(signature_header, expected)
}
Key points:
- Use the raw body bytes, not a re-serialized JSON. JSON serialization can vary by language; the signed bytes are whatever TrustLens sent.
- Use constant-time comparison to prevent timing attacks. Most languages have this built in (Node:
crypto.timingSafeEqual; Python:hmac.compare_digest; PHP:hash_equals). - Reject requests with missing or wrong signature — return 401, log the attempt
Implementation Examples #
Node.js / Express #
const crypto = require('crypto');
app.post('/trustlens-webhook', (req, res) => {
const signature = req.headers['x-trustlens-signature'];
const expected = 'sha256=' + crypto
.createHmac('sha256', SECRET)
.update(req.rawBody)
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.rawBody.toString());
// ... handle event
res.status(200).send('ok');
});
Python / Flask #
import hmac, hashlib
from flask import request
@app.post('/trustlens-webhook')
def webhook():
signature = request.headers.get('X-TrustLens-Signature', '')
expected = 'sha256=' + hmac.new(
SECRET, request.get_data(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
return ('Invalid signature', 401)
event = request.get_json()
# ... handle event
return ('ok', 200)
PHP #
$body = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_TRUSTLENS_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $body, SECRET);
if ( ! hash_equals( $signature, $expected ) ) {
http_response_code( 401 );
exit( 'Invalid signature' );
}
$event = json_decode( $body, true );
// ... handle event
http_response_code( 200 );
echo 'ok';
Replay Protection #
TrustLens includes a X-TrustLens-Delivery header with a unique UUID per dispatch attempt (different for retries). Receivers can dedupe on this if needed.
The X-TrustLens-Timestamp header lets receivers reject very old requests — useful protection against replay attacks. Convention: reject any request with a timestamp more than 5 minutes old.
Expected Response #
The receiver should respond with HTTP 2xx within 10 seconds. Any 2xx is treated as success; the action is marked complete.
Non-2xx responses (or timeouts) trigger automatic retry per the standard retry policy: 60s / 120s / 240s backoff. After 3 failed retries, the action is logged as failed.
If your receiver does long-running processing, respond 200 immediately and process asynchronously. Don’t keep TrustLens waiting.
Idempotency #
TrustLens may retry the same delivery on transient failures. Receivers should be idempotent — process the same delivery_id once, even if it arrives multiple times. The delivery_id stays the same across retries; only the timestamp changes.
Secret Rotation #
To rotate the secret:
- In the rule’s action editor, click “Regenerate Secret”
- Copy the new secret
- Update the receiver’s expected secret
- Save the rule
There’s no graceful transition window — the old secret stops working immediately when the new one is generated. For zero-downtime rotation, update the receiver first to accept both old and new secrets, then rotate, then remove the old secret.
Debugging #
The Automation Log shows each webhook attempt with:
- URL
- Request body (truncated if large)
- Response status
- Response body (truncated)
- Time to first byte
For debugging signature issues, the log shows the body bytes used for signing — handy if you suspect serialization differences.
For new webhook endpoints, use a tool like webhook.site or RequestBin to inspect incoming requests before connecting your real receiver. TrustLens’s payload format is predictable; verifying it against a test endpoint is faster than debugging via real downstream effects.