GuidesUpdated 3 days ago
Webhook Security
Webhooks let Skribby notify your application in real time. Because webhooks are inbound HTTP requests, you should verify that each request is genuinely sent by Skribby and has not been tampered with. Skribby signs every webhook using HMAC-SHA256 so you can validate the payload before processing it.
Headers sent with every webhook
| Header | Value |
|---|---|
X-Skribby-Signature | sha256=<hex_hmac> |
X-Skribby-Timestamp | Unix timestamp |
X-Skribby-Webhook-Id | Unique UUID |
Getting your webhook secret
- Navigate to Settings -> Webhook Settings in the dashboard.
- The secret is masked by default; click to reveal it.
- Use the copy button for quick access.
- If the secret is compromised, regenerate it (this invalidates existing integrations).
Verification steps
- Extract the signature and timestamp headers.
- Check the timestamp is within a tolerance window (recommended: 5 minutes).
- Compute the expected signature:
HMAC-SHA256(timestamp + "." + raw_body, secret). - Compare signatures using constant-time comparison.
Code examples
PHP (Laravel)
$signature = $request->header('X-Skribby-Signature');
$timestamp = $request->header('X-Skribby-Timestamp');
$payload = $request->getContent();
// Reject old timestamps (replay protection)
if (abs(time() - $timestamp) > 300) {
abort(400, 'Timestamp too old');
}
// Compute expected signature
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $payload, $webhookSecret);
// Constant-time comparison
if (!hash_equals($expected, $signature)) {
abort(401, 'Invalid signature');
}
Node.js
const crypto = require('crypto');
function verifyWebhook(req, secret) {
const signature = req.headers['x-skribby-signature'];
const timestamp = req.headers['x-skribby-timestamp'];
const payload = req.rawBody; // Must be raw body, not parsed JSON
// Reject old timestamps
if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
throw new Error('Timestamp too old');
}
// Compute expected signature
const expected =
'sha256=' +
crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + payload)
.digest('hex');
// Constant-time comparison
if (
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
) {
throw new Error('Invalid signature');
}
return true;
}
Python
import hmac
import hashlib
import time
def verify_webhook(request, secret):
signature = request.headers.get('X-Skribby-Signature')
timestamp = request.headers.get('X-Skribby-Timestamp')
payload = request.data # Raw body bytes
# Reject old timestamps
if abs(time.time() - int(timestamp)) > 300:
raise ValueError('Timestamp too old')
# Compute expected signature
signed_payload = timestamp.encode() + b'.' + payload
expected = 'sha256=' + hmac.new(
secret.encode(),
signed_payload,
hashlib.sha256
).hexdigest()
# Constant-time comparison
if not hmac.compare_digest(expected, signature):
raise ValueError('Invalid signature')
return True
Common pitfalls
- Using parsed JSON instead of the raw request body
- Skipping constant-time comparison (timing attacks)
- Not checking the timestamp (replay attacks)
- Logging the webhook secret
Troubleshooting
- "Invalid signature": Ensure you're using the raw request body, not parsed JSON.
- "Timestamp too old": Check server clock sync or increase your tolerance window.
- Secret not working after regeneration: Update the secret in your application.