Verify Webhook Signatures
Every request from XRNotify is signed with your webhook secret. Verifying this signature protects your endpoint from spoofed or tampered requests.
How signatures work
When XRNotify delivers an event, it computes an HMAC-SHA256 digest of the raw request body using your webhook's signing secret. The result is included in the X-XRNotify-Signature request header, prefixed with sha256=.
To verify, you recompute the HMAC using the same secret and compare your result to the header value using a constant-time comparison function to prevent timing attacks.
Webhook request headers
| Header | Description |
|---|---|
X-XRNotify-Signature | HMAC-SHA256 of the raw body, formatted as sha256=<hex_digest> |
X-XRNotify-Timestamp | Unix timestamp (seconds) of when the delivery was initiated |
X-XRNotify-Delivery-Id | Unique delivery ID — use for idempotency checks in your database |
X-XRNotify-Webhook-Id | ID of the webhook this event was delivered to |
Security warning: Always verify the signature before processing any event data. Never trust the payload contents without first confirming the request originated from XRNotify.
Verification examples
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expected = `sha256=${crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex')}`;
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express middleware
app.use('/webhooks', express.raw({ type: 'application/json' }));
app.post('/webhooks/xrpl', (req, res) => {
const valid = verifySignature(
req.body,
req.headers['x-xrnotify-signature'],
process.env.WEBHOOK_SECRET
);
if (!valid) return res.status(401).json({ error: 'Invalid signature' });
const event = JSON.parse(req.body);
console.log('Verified event:', event.event_type);
// process event...
res.sendStatus(200);
});Common mistakes
Parsing JSON before verifying
The signature covers the raw bytes of the request body. If you parse the JSON first and re-serialize, even a single whitespace difference will cause verification to fail. Always use the raw body buffer.
Using == for string comparison
Standard equality checks are vulnerable to timing attacks where an attacker can infer the correct signature byte-by-byte by measuring response times. Always use a constant-time comparison function like crypto.timingSafeEqual (Node.js), hmac.compare_digest (Python), or hmac.Equal (Go).
Forgetting the sha256= prefix
The header value is sha256=<hex_digest>, not just the hex digest. Your expected value must include the prefix, otherwise the comparison will always fail.
Security best practices
- ✓
Rotate secrets if compromised
Use the rotate-secret endpoint (POST /v1/webhooks/:id/rotate-secret) to immediately invalidate the old secret and generate a new one.
- ✓
Reject stale events
Check the X-XRNotify-Timestamp header and reject events older than 5 minutes. This prevents replay attacks where an attacker re-sends a valid old request.
- ✓
Deduplicate with delivery IDs
Store X-XRNotify-Delivery-Id values in your database to ensure idempotent processing. XRNotify may retry failed deliveries, so your handler should be safe to call multiple times.
- ✓
Store secrets in environment variables
Never hardcode webhook secrets in your source code. Use environment variables or a secrets manager like AWS Secrets Manager or HashiCorp Vault.