Why verify
Webhook endpoints are public URLs. Without verification, anyone who discovers your endpoint URL could send fake events. Signature verification proves that a delivery genuinely came from Bluvo.How it works
Every webhook delivery is signed with your endpoint’s secret using HMAC-SHA256:- Bluvo concatenates the timestamp and JSON payload:
${timestamp}\n${payload} - Computes the HMAC-SHA256 of that string using your webhook secret
- Base64-encodes the result and sends it in the
X-Webhook-Signatureheader
Headers
| Header | Example | Description |
|---|---|---|
X-Webhook-Signature | K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols= | Base64-encoded HMAC-SHA256 signature |
X-Webhook-Timestamp | 1713700800000 | Unix timestamp in milliseconds when the delivery was sent |
Verification example
Timestamp validation
Bluvo does not enforce a maximum age on deliveries. We recommend your receiver rejects any payload whereX-Webhook-Timestamp is more than 5 minutes from the current time. This prevents replay attacks where an attacker captures a valid delivery and resends it later.
Secret rotation
Bluvo supports zero-downtime secret rotation with a 3-phase lifecycle:| Phase | Description |
|---|---|
| Pending | New secret created but not yet active. Deliveries are still signed with the active secret. The pending secret expires after 24 hours if not activated. |
| Active | The current signing secret. All new deliveries are signed with this secret. |
| Expired | The previous active secret. Kept for a grace period so in-flight deliveries can still be verified. The last 2 expired secrets are retained. |
- Navigate to Settings > Webhooks and select your endpoint
- Click Rotate Secret — this creates a new pending secret
- Update your server with the new secret
- Click Activate Secret to promote it to active
Common mistakes
Parsed body instead of raw body
Parsed body instead of raw body
If you parse the JSON body before computing the signature, the re-serialized string may differ from the original (key ordering, whitespace). Always use the raw request body bytes.
Wrong encoding
Wrong encoding
The signature is Base64-encoded, not hex. Make sure your HMAC output is encoded as Base64 before comparing.
String comparison instead of constant-time
String comparison instead of constant-time
Using
=== instead of timingSafeEqual (Node.js) or hmac.compare_digest (Python) leaks timing information that can be exploited to forge signatures.