Webhooks and HMAC
5 min read
- View as Markdown Open this page as plain Markdown (opens in a new tab)
- Open in ChatGPT Ask questions about this page (opens in a new tab)
- Open in Claude Ask questions about this page (opens in a new tab)
- Open in Google AI Studio Ask questions about this page (Google account required) (opens in a new tab)
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.
This page is about the Send Webhook automation action. The global webhook endpoints under Settings → Integrations → Webhooks are a separate feature with their own per-endpoint secrets and payloads.
Webhook Action Configuration #
In the rule editor, choose Send Webhook as the action and put the receiver’s URL in the action value:
| Setting | Description |
|---|---|
| URL | Full URL of the receiver endpoint (the action value). Required; the rule won’t save without a valid URL. Use HTTPS, and use the final URL — redirects aren’t followed |
| Secret | Not set per rule. TrustLens generates one signing secret for your site the first time a webhook is sent |
| Timeout | Fixed at 10 seconds |
There are no custom-header or timeout fields. The secret isn’t shown in the admin: it’s stored in the trustlens_automation_webhook_secret WordPress option (read it with wp option get trustlens_automation_webhook_secret), and developers can supply their own — per rule, if needed — with the trustlens/automation/webhook_secret filter. Save it somewhere safe — you’ll need it on the receiver side.
Sending to Slack #
Don’t paste a Slack incoming-webhook URL into Send Webhook. Slack only accepts a body with a text (or blocks) field, and the TrustLens payload has neither, so Slack rejects every delivery and the rule logs an error. Point the action at Zapier, Make or a small endpoint of your own, and have that post a formatted message to Slack.
Request Format #
Each webhook is an HTTP POST with:
- Content-Type:
application/json - Header
X-TrustLens-Event: alwaystrustlens_automation(the trigger is in the body) - Header
X-TrustLens-Signature:sha256=followed by the hex HMAC-SHA256 of the timestamp, a dot, and the raw body - Header
X-TrustLens-Delivery: a new UUID for every attempt - Header
X-TrustLens-Rule-ID: the rule’s ID - Header
X-TrustLens-Timestamp: Unix timestamp at dispatch time
The User-Agent is WordPress’s default.
Payload Schema #
The body is a flat JSON object:
{
"event": "trustlens_automation",
"rule": "Auto-block on dispute",
"rule_id": "3f2b9c1e-…",
"trigger": "chargeback_filed",
"email_hash": "…",
"trust_score": 18,
"segment": "critical",
"fired_at": "2026-03-15 12:00:00",
"attempt": 1
}
rule_id is the rule’s UUID, trust_score and segment are read when the webhook is sent (null if the customer has no record), fired_at is the site’s local time at sending, and attempt counts 1 to 4. Rules on Refund Processed also include a refund_context object with the refund’s amount, ratio, type, classification, reason and the other refund fields. The payload carries no order, dispute or fingerprint data — use the email hash with the REST API if you need more.
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
timestamp = request.headers['x-trustlens-timestamp']
signature_header = request.headers['x-trustlens-signature']
expected = "sha256=" + hmac_sha256_hex(secret, timestamp + "." + body)
return constant_time_compare(signature_header, expected)
}
Key points:
- Sign the timestamp too. The signed string is the
X-TrustLens-Timestampvalue, a., then the body — not the body alone. - 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 timestamp = req.headers['x-trustlens-timestamp'] || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', SECRET)
.update(timestamp + '.' + req.rawBody)
.digest('hex');
if (signature.length !== expected.length || !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', '')
timestamp = request.headers.get('X-TrustLens-Timestamp', '')
signed = timestamp.encode() + b'.' + request.get_data()
expected = 'sha256=' + hmac.new(
SECRET, signed, 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');
$timestamp = $_SERVER['HTTP_X_TRUSTLENS_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_TRUSTLENS_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, SECRET);
if ( ! hash_equals( $expected, $signature ) ) {
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).
The X-TrustLens-Timestamp header is part of the signed string, so it can’t be altered without breaking the signature — reject any request whose timestamp is more than 5 minutes old to block replays.
Expected Response #
The receiver should respond with HTTP 2xx within 10 seconds. Any 2xx is treated as success and logged as OK; the response body is ignored.
Anything else — a 3xx redirect, 4xx, 5xx, a timeout or a connection error — triggers automatic retry: 60s / 120s / 240s backoff. After 3 failed retries, the delivery is logged as failed.
If your receiver does long-running processing, respond 200 immediately and process asynchronously. Don’t keep TrustLens waiting.
Idempotency #
TrustLens retries a delivery only when it didn’t get a 2xx — but a receiver that processed the request and then timed out will see it again. Each retry carries a new X-TrustLens-Delivery UUID, a new fired_at and a higher attempt, so there’s no single ID that stays the same across retries. If duplicates matter, dedupe on rule_id + email_hash + trigger within a short window, or treat attempt > 1 as a possible repeat.
Before sending, identical pending deliveries (same rule and customer) are collapsed into one, and the one-hour rule cooldown limits how often the same rule can queue a webhook for the same customer.
Secret Rotation #
There’s no “Regenerate Secret” button. To rotate the site secret:
- Delete the
trustlens_automation_webhook_secretoption (e.g.wp option delete trustlens_automation_webhook_secret) — or return a new secret from thetrustlens/automation/webhook_secretfilter - A new secret is used from the next delivery (a deleted one is regenerated automatically)
- Read the new value and update every receiver’s expected secret
There’s no graceful transition window — the old secret stops working as soon as the new one is in use, for every rule at once. For zero-downtime rotation, update the receiver first to accept both old and new secrets, then rotate, then remove the old secret.
Debugging #
The Rule Inspector shows each delivery’s final result:
- QUEUED while the delivery waits in Action Scheduler
- OK once a 2xx arrives
- ERR after the last retry, with the HTTP status (“Webhook returned HTTP 500 after 4 attempt(s).”) or the connection error (“Webhook delivery failed: …”)
Request and response bodies aren’t stored. To see exactly what TrustLens sends, point a rule at a tool like webhook.site or RequestBin 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. Pending and retrying deliveries are visible under WooCommerce → Status → Scheduled Actions (hook trustlens/automation/dispatch_webhook).
Updated on October 18, 2026