Security & Payload Signing
Every SocialHook payload is signed with HMAC-SHA256 using your secret key. You must verify this signature on your server before processing any message — this protects you against spoofed payloads.
How signing works
When SocialHook delivers a payload to your webhook URL, it computes an HMAC-SHA256 hash of the raw request body using your secret key. This hash is sent in the X-SocialHook-Signature HTTP header in the format sha256=<hex_digest>.
On your server, you compute the same hash using the same secret key and raw body — and compare the two values. If they match, the payload is authentic.
Never skip signature verification. Without it, anyone who knows your webhook URL can send fake payloads to your server.
Verification — Node.js
Node.js / Express
const crypto = require('crypto')
function verifySocialHookSignature(req, secret) {
const header = req.headers['x-socialhook-signature']
if (!header) return false
const hash = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(req.rawBody) // must be raw Buffer
.digest('hex')
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(hash),
Buffer.from(header)
)
}
// In your Express middleware:
app.use(express.json({
verify: (req, _, buf) => { req.rawBody = buf }
}))
app.post('/webhook', (req, res) => {
if (!verifySocialHookSignature(req, process.env.SOCIALHOOK_SECRET)) {
return res.status(401).send('Invalid signature')
}
// Safe to process
res.sendStatus(200)
})Verification — Python
Python / Flask
import hmac
import hashlib
from flask import Flask, request, abort
import os
app = Flask(__name__)
def verify_signature(payload_body, signature_header, secret):
if not signature_header:
return False
expected = 'sha256=' + hmac.new(
secret.encode(),
payload_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)
@app.route('/webhook', methods=['POST'])
def webhook():
sig = request.headers.get('X-SocialHook-Signature')
if not verify_signature(request.data, sig, os.environ['SOCIALHOOK_SECRET']):
abort(401)
# Safe to process request.json
return '', 200Verification — PHP
PHP
<?php
function verifySocialHookSignature($rawBody, $signatureHeader, $secret): bool {
if (empty($signatureHeader)) return false;
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signatureHeader);
}
$rawBody = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_SOCIALHOOK_SIGNATURE'] ?? '';
if (!verifySocialHookSignature($rawBody, $sig, getenv('SOCIALHOOK_SECRET'))) {
http_response_code(401);
exit('Invalid signature');
}
$payload = json_decode($rawBody, true);
// Safe to process $payload
http_response_code(200);Security best practices
Always use timingSafeEqual — a simple string comparison is vulnerable to timing attacks. Use
crypto.timingSafeEqual() (Node), hmac.compare_digest() (Python), or hash_equals() (PHP).Sign the raw body, not parsed JSON — compute the HMAC over the raw request body bytes before any JSON parsing. Different JSON serializers may produce different byte sequences.
Store your secret key as an environment variable — never hardcode it in source code or commit it to a repository.
Return 200 before processing — SocialHook waits up to 10 seconds for your response. Return 200 immediately and process the payload asynchronously.
Use HTTPS only — SocialHook will only deliver to HTTPS endpoints. Plain HTTP endpoints are rejected at configuration time.
Implement idempotency — In rare cases SocialHook may deliver the same event twice. Store
delivery_id values and skip duplicates.Rotate your secret key periodically — Generate a new secret key from your dashboard settings and update your server environment variable.
Found a security vulnerability? Please report it responsibly to security@socialhook.io. Do not disclose publicly before we've had a chance to address it. We respond within 72 hours.