Be-In Docs
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

headerexample
x-platform-timestamp1717089600123
x-platform-signature9f6c34837e…

x-platform-signature is computed as:

signature = HMAC_SHA256(secret, timestamp + '.' + rawBody)

where

  • secret – the per-endpoint secret returned when you created the endpoint
  • timestamp – the value of the x-platform-timestamp header
  • rawBody – exact bytes received (no json parse)

example implementation

verify.ts
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');
}
verify.py
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")
verify.php
<?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');
    }
}
verify.go
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
}
WebhookVerifier.cs
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.
© 2025 Be-In GmbH. Impressum