When a webhook has a secret, every request is signed with HMAC-SHA256 over the timestamp header and the raw body.
X-Mateflow-Signature = "sha256=" + hex(
HMAC-SHA256(secret, X-Mateflow-Timestamp + "." + raw_request_body)
)
- Use the raw bytes. Characters that are special in HTML can be escaped in the body, so parsing the JSON and serializing it again produces different bytes and the signature will not match.
- Take the timestamp from the header, not the body. A manual resend updates only the header timestamp and the signature.
- A timestamp check does not interfere with retries. Every automatic retry and manual resend is signed again, so a 5-minute window is enough.
const crypto = require('crypto');
// rawBody must be the untouched bytes, e.g. from express.raw({ type: 'application/json' }).
function verifyMateflowSignature(rawBody, headers, secret, toleranceSeconds = 300) {
const timestamp = headers['x-mateflow-timestamp'];
const received = headers['x-mateflow-signature'] || '';
if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) {
return false;
}
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest('hex');
const a = Buffer.from(received);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}import hashlib
import hmac
import time
def verify_mateflow_signature(raw_body: bytes, headers, secret: str, tolerance_seconds: int = 300) -> bool:
timestamp = headers.get("X-Mateflow-Timestamp", "")
received = headers.get("X-Mateflow-Signature", "")
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > tolerance_seconds:
return False
signed = timestamp.encode() + b"." + raw_body
expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(received.encode(), expected.encode())Test vector
Use these values to check your signature calculation. The timestamp has expired, so relax your time window while testing.
secret my_signing_secret
X-Mateflow-Timestamp 1793664000
raw body (UTF-8) {"event":"member.joined","data":{"username":"张三"}}
X-Mateflow-Signature sha256=3f4c83828ce0086150c9bd48c1654899357b7c05265c1d1b6c172225ee04b4b4Rotating a secret
A new secret takes effect as soon as it is saved, and queued retries are signed with it too. There is no period when Mateflow signs with both, so have your receiver accept both the old and the new secret during the switch.