Webhook Security
Every webhook delivery includes a cryptographic signature so you can verify that the request genuinely came from GamifyHost and hasn’t been tampered with.
Signature Headers
Section titled “Signature Headers”Each webhook request includes these headers:
| Header | Description | Example |
|---|---|---|
X-Webhook-Signature | HMAC-SHA256 signature of the request body | sha256=a1b2c3d4e5f6... |
X-Webhook-Event | The event type | game.played |
X-Webhook-Timestamp | When the event was dispatched (RFC 3339) | 2025-07-15T10:30:00Z |
How Signing Works
Section titled “How Signing Works”- GamifyHost serializes the payload to JSON
- Computes
HMAC-SHA256(secret, jsonBody)using your webhook secret - Sends the signature as
sha256=<hex>in theX-Webhook-Signatureheader
Verification Steps
Section titled “Verification Steps”On your server:
- Read the raw request body (do not parse and re-serialize — the byte-exact body must match)
- Compute
HMAC-SHA256(your_secret, raw_body) - Compare with the
X-Webhook-Signatureheader value using a constant-time comparison - Reject the request if the signatures don’t match
Verification Examples
Section titled “Verification Examples”import crypto from 'crypto';
function verifyWebhook(rawBody, signature, secret) { const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex');
return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) );}
// Usage in Express:app.post('/webhook', express.raw({ type: '*/*' }), (req, res) => { const sig = req.headers['x-webhook-signature']; if (!verifyWebhook(req.body, sig, WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } // Process event... res.sendStatus(200);});import hmacimport hashlib
def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool: expected = "sha256=" + hmac.new( secret.encode(), raw_body, hashlib.sha256, ).hexdigest() return hmac.compare_digest(signature, expected)
# Usage in Flask:@app.route("/webhook", methods=["POST"])def webhook(): sig = request.headers.get("X-Webhook-Signature", "") if not verify_webhook(request.get_data(), sig, WEBHOOK_SECRET): abort(401) # Process event... return "OK", 200import ( "crypto/hmac" "crypto/sha256" "encoding/hex")
func verifyWebhook(body []byte, signature, secret string) bool { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(body) expected := "sha256=" + hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(signature), []byte(expected))}import javax.crypto.Mac;import javax.crypto.spec.SecretKeySpec;import java.security.MessageDigest;import java.util.HexFormat;
public static boolean verifyWebhook( byte[] body, String signature, String secret) throws Exception { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec( secret.getBytes(), "HmacSHA256" )); String expected = "sha256=" + HexFormat.of().formatHex(mac.doFinal(body)); return MessageDigest.isEqual( signature.getBytes(), expected.getBytes() );}function verifyWebhook( string $rawBody, string $signature, string $secret): bool { $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret); return hash_equals($expected, $signature);}
// Usage:$payload = file_get_contents('php://input');$sig = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';if (!verifyWebhook($payload, $sig, $secret)) { http_response_code(401); exit('Invalid signature');}require 'openssl'
def verify_webhook(raw_body, signature, secret) expected = 'sha256=' + OpenSSL::HMAC.hexdigest( 'sha256', secret, raw_body ) Rack::Utils.secure_compare(signature, expected)endusing System.Security.Cryptography;using System.Text;
bool VerifyWebhook(string rawBody, string signature, string secret){ using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(rawBody)); var expected = "sha256=" + Convert.ToHexString(hash).ToLower(); return CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(signature), Encoding.UTF8.GetBytes(expected) );}Best Practices
Section titled “Best Practices”- Always verify signatures — Never process unverified webhook payloads
- Use constant-time comparison — Prevents timing attacks (
crypto.timingSafeEqual,hmac.compare_digest,hash_equals, etc.) - Read the raw body — Parse after verification. Re-serializing JSON may change field order or whitespace, breaking the signature
- Respond quickly — Return
200immediately and process the event asynchronously. GamifyHost times out after 10 seconds - Handle retries idempotently — The same event may be delivered multiple times (up to 5 retries). Use the payload’s unique IDs (
ledgerId,playId) to deduplicate - Store your secret securely — Use environment variables or a secrets manager. The secret is only shown once at webhook creation