Skribby
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

HeaderValue
X-Skribby-Signaturesha256=<hex_hmac>
X-Skribby-TimestampUnix timestamp
X-Skribby-Webhook-IdUnique 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

  1. Extract the signature and timestamp headers.
  2. Check the timestamp is within a tolerance window (recommended: 5 minutes).
  3. Compute the expected signature: HMAC-SHA256(timestamp + "." + raw_body, secret).
  4. 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.