Webhooks
Security & Signature Verification
Learn how to verify webhook signatures to guarantee authenticity and prevent tampering.
be-in signs every webhook to guarantee authenticity and prevent tampering.
headers
| header | example |
|---|---|
x-platform-timestamp | 1717089600123 |
x-platform-signature | 9f6c34837e… |
x-platform-signature is computed as:
signature = HMAC_SHA256(secret, timestamp + '.' + rawBody)where
secret– the per-endpoint secret returned when you created the endpointtimestamp– the value of thex-platform-timestampheaderrawBody– exact bytes received (no json parse)
example implementation
import crypto from 'node:crypto';
export function verifySignature({
rawBody,
timestamp,
signature,
secret,
}: {
rawBody: string;
timestamp: string;
signature: string;
secret: string;
}) {
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
throw new Error('invalid signature');
}
// optional: replay protection (5-minute window)
const age = Math.abs(Date.now() - Number(timestamp));
if (age > 300_000) throw new Error('timestamp too old');
}import hmac
import hashlib
import time
def verify_signature(raw_body: bytes, timestamp: str, signature: str, secret: str):
"""verify webhook signature"""
expected = hmac.new(
secret.encode('utf-8'),
f"{timestamp}.{raw_body.decode('utf-8')}".encode('utf-8'),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
raise ValueError("invalid signature")
# replay protection (5-minute window)
age = abs(time.time() * 1000 - int(timestamp))
if age > 300_000:
raise ValueError("timestamp too old")<?php
function verifySignature($rawBody, $timestamp, $signature, $secret) {
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
if (!hash_equals($expected, $signature)) {
throw new Exception('invalid signature');
}
// replay protection (5-minute window)
$age = abs(time() * 1000 - intval($timestamp));
if ($age > 300000) {
throw new Exception('timestamp too old');
}
}package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"math"
"strconv"
"time"
)
func verifySignature(rawBody, timestamp, signature, secret string) error {
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(timestamp + "." + rawBody))
expected := hex.EncodeToString(h.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(signature)) {
return errors.New("invalid signature")
}
// replay protection (5-minute window)
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return errors.New("invalid timestamp")
}
age := math.Abs(float64(time.Now().UnixMilli() - ts))
if age > 300000 {
return errors.New("timestamp too old")
}
return nil
}using System;
using System.Security.Cryptography;
using System.Text;
public class WebhookVerifier
{
public static void VerifySignature(string rawBody, string timestamp, string signature, string secret)
{
var expected = ComputeHmacSha256(secret, $"{timestamp}.{rawBody}");
if (!SecureCompare(expected, signature))
{
throw new ArgumentException("invalid signature");
}
// replay protection (5-minute window)
if (!long.TryParse(timestamp, out var timestampMs))
{
throw new ArgumentException("invalid timestamp");
}
var age = Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - timestampMs);
if (age > 300_000)
{
throw new ArgumentException("timestamp too old");
}
}
private static string ComputeHmacSha256(string secret, string message)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
return Convert.ToHexString(hash).ToLower();
}
private static bool SecureCompare(string expected, string actual)
{
if (expected.Length != actual.Length)
{
return false;
}
var result = 0;
for (var i = 0; i < expected.Length; i++)
{
result |= expected[i] ^ actual[i];
}
return result == 0;
}
}common pitfalls
- json re-stringify – use the raw request body, not a re-serialized object.
- timezone differences – timestamp is unix milliseconds; no timezone conversion.
- whitelisting ips – prefer signature verification; ips may change.
- case sensitivity – ensure header names match exactly (lowercase).
- encoding issues – use utf-8 encoding consistently across all implementations.
- timing attacks – always use constant-time comparison functions like
crypto.timingSafeEqual.