Verifying Signatures
Copy-paste HMAC-SHA256 verification snippets in TypeScript, Python, PHP, Ruby, and Go.
Every webhook delivery carries:
X-Brainerce-Signature: hex HMAC-SHA256 of the signed payloadX-Brainerce-Timestamp: Unix milliseconds the payload was signed at- a JSON body
The signed payload is built by concatenating the timestamp, a literal dot, and the raw body bytes:
signed = `${timestamp}.${raw_body}`
signature = HMAC_SHA256(your_subscription_secret, signed)Three rules to follow on the receiving side:
- Sign the raw bytes, not a re-serialised JSON. Different libraries reorder keys differently, so the bytes you sign must be exactly the bytes you received.
- Reject stale timestamps. We recommend a 5-minute window. Without this check, anyone who captures one delivery can replay it forever.
- Constant-time compare. Use
crypto.timingSafeEqual/hmac.compare_digest/hash_equals/ your language's equivalent. Plain==leaks timing.
The snippets below follow these three rules.
TypeScript / Node.js
Works with Express, Fastify, Next.js Route Handlers, and any framework that exposes the raw request body.
If you already depend on the brainerce SDK, verifyWebhook({ rawBody, signature, timestamp, secret }) is this exact check (timestamp-prefixed HMAC, constant-time compare, 5-minute window) and parseWebhookEvent(rawBody) types the envelope; see SDK webhooks. The hand-rolled version below is equivalent and has no dependency.
import * as crypto from 'crypto';
const WEBHOOK_SECRET = process.env.BRAINERCE_WEBHOOK_SECRET!; // whsec_...
const MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
export function verifyBrainerceWebhook(
rawBody: string | Buffer,
signatureHeader: string | undefined,
timestampHeader: string | undefined
): { valid: boolean; reason?: string } {
if (!signatureHeader || !timestampHeader) {
return { valid: false, reason: 'missing signature/timestamp headers' };
}
const timestamp = Number.parseInt(timestampHeader, 10);
if (Number.isNaN(timestamp)) {
return { valid: false, reason: 'malformed timestamp' };
}
const age = Date.now() - timestamp;
if (age > MAX_AGE_MS) return { valid: false, reason: 'timestamp too old' };
if (age < -MAX_AGE_MS) return { valid: false, reason: 'timestamp in the future' };
const bodyString = Buffer.isBuffer(rawBody) ? rawBody.toString('utf8') : rawBody;
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(`${timestamp}.${bodyString}`)
.digest('hex');
const expectedBuf = Buffer.from(expected, 'hex');
const givenBuf = Buffer.from(signatureHeader, 'hex');
if (expectedBuf.length !== givenBuf.length) {
return { valid: false, reason: 'signature length mismatch' };
}
const valid = crypto.timingSafeEqual(expectedBuf, givenBuf);
return valid ? { valid: true } : { valid: false, reason: 'signature mismatch' };
}Express example
import express from 'express';
const app = express();
// Use raw body — express.json() will re-serialise and break signing
app.post('/brainerce-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const result = verifyBrainerceWebhook(
req.body, // Buffer thanks to express.raw
req.header('X-Brainerce-Signature'),
req.header('X-Brainerce-Timestamp')
);
if (!result.valid) {
return res.status(401).json({ error: result.reason });
}
const event = JSON.parse(req.body.toString('utf8'));
// process event ...
res.status(202).json({ ok: true });
});Next.js App Router example
// app/api/brainerce-webhook/route.ts
import { NextResponse } from 'next/server';
export async function POST(req: Request) {
const rawBody = await req.text(); // raw string, not parsed JSON
const result = verifyBrainerceWebhook(
rawBody,
req.headers.get('X-Brainerce-Signature') ?? undefined,
req.headers.get('X-Brainerce-Timestamp') ?? undefined
);
if (!result.valid) {
return NextResponse.json({ error: result.reason }, { status: 401 });
}
const event = JSON.parse(rawBody);
// process event ...
return NextResponse.json({ ok: true }, { status: 202 });
}Python
Works with Flask, FastAPI, Django, and any WSGI/ASGI framework that exposes the raw body.
import hmac
import hashlib
import os
import time
WEBHOOK_SECRET = os.environ['BRAINERCE_WEBHOOK_SECRET'].encode() # whsec_...
MAX_AGE_MS = 5 * 60 * 1000
def verify_brainerce_webhook(
raw_body: bytes,
signature_header: str | None,
timestamp_header: str | None,
) -> tuple[bool, str | None]:
if not signature_header or not timestamp_header:
return False, 'missing signature/timestamp headers'
try:
timestamp = int(timestamp_header)
except ValueError:
return False, 'malformed timestamp'
now_ms = int(time.time() * 1000)
age = now_ms - timestamp
if age > MAX_AGE_MS:
return False, 'timestamp too old'
if age < -MAX_AGE_MS:
return False, 'timestamp in the future'
signed_payload = f'{timestamp}.'.encode() + raw_body
expected = hmac.new(WEBHOOK_SECRET, signed_payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature_header):
return False, 'signature mismatch'
return True, NoneFlask example
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.post('/brainerce-webhook')
def webhook():
raw_body = request.get_data() # raw bytes, not request.json
valid, reason = verify_brainerce_webhook(
raw_body,
request.headers.get('X-Brainerce-Signature'),
request.headers.get('X-Brainerce-Timestamp'),
)
if not valid:
return jsonify({'error': reason}), 401
event = request.get_json()
# process event ...
return jsonify({'ok': True}), 202FastAPI example
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
@app.post('/brainerce-webhook')
async def webhook(request: Request):
raw_body = await request.body() # raw bytes
valid, reason = verify_brainerce_webhook(
raw_body,
request.headers.get('X-Brainerce-Signature'),
request.headers.get('X-Brainerce-Timestamp'),
)
if not valid:
raise HTTPException(401, reason)
import json
event = json.loads(raw_body)
# process event ...
return {'ok': True}PHP
Works with Laravel, Symfony, and plain PHP.
<?php
const BRAINERCE_WEBHOOK_SECRET_ENV = 'BRAINERCE_WEBHOOK_SECRET';
const BRAINERCE_MAX_AGE_MS = 5 * 60 * 1000;
function verifyBrainerceWebhook(
string $rawBody,
?string $signatureHeader,
?string $timestampHeader
): array {
if (!$signatureHeader || !$timestampHeader) {
return ['valid' => false, 'reason' => 'missing signature/timestamp headers'];
}
$timestamp = filter_var($timestampHeader, FILTER_VALIDATE_INT);
if ($timestamp === false) {
return ['valid' => false, 'reason' => 'malformed timestamp'];
}
$nowMs = (int) (microtime(true) * 1000);
$age = $nowMs - $timestamp;
if ($age > BRAINERCE_MAX_AGE_MS) return ['valid' => false, 'reason' => 'timestamp too old'];
if ($age < -BRAINERCE_MAX_AGE_MS) return ['valid' => false, 'reason' => 'timestamp in the future'];
$secret = getenv(BRAINERCE_WEBHOOK_SECRET_ENV);
$signed = $timestamp . '.' . $rawBody;
$expected = hash_hmac('sha256', $signed, $secret);
if (!hash_equals($expected, $signatureHeader)) {
return ['valid' => false, 'reason' => 'signature mismatch'];
}
return ['valid' => true];
}
// Usage (plain PHP)
$rawBody = file_get_contents('php://input');
$result = verifyBrainerceWebhook(
$rawBody,
$_SERVER['HTTP_X_BRAINERCE_SIGNATURE'] ?? null,
$_SERVER['HTTP_X_BRAINERCE_TIMESTAMP'] ?? null
);
if (!$result['valid']) {
http_response_code(401);
echo json_encode(['error' => $result['reason']]);
exit;
}
$event = json_decode($rawBody, true);
// process event ...
http_response_code(202);
echo json_encode(['ok' => true]);Laravel example
// routes/web.php
Route::post('/brainerce-webhook', function (Request $request) {
$result = verifyBrainerceWebhook(
$request->getContent(), // raw body, not $request->all()
$request->header('X-Brainerce-Signature'),
$request->header('X-Brainerce-Timestamp')
);
if (!$result['valid']) {
return response()->json(['error' => $result['reason']], 401);
}
$event = $request->json()->all();
// process event ...
return response()->json(['ok' => true], 202);
});Ruby
Works with Rails, Sinatra, and Rack.
require 'openssl'
require 'rack/utils'
WEBHOOK_SECRET = ENV.fetch('BRAINERCE_WEBHOOK_SECRET') # whsec_...
MAX_AGE_MS = 5 * 60 * 1000
def verify_brainerce_webhook(raw_body, signature_header, timestamp_header)
return [false, 'missing signature/timestamp headers'] if signature_header.nil? || timestamp_header.nil?
timestamp = Integer(timestamp_header) rescue nil
return [false, 'malformed timestamp'] if timestamp.nil?
now_ms = (Time.now.to_f * 1000).to_i
age = now_ms - timestamp
return [false, 'timestamp too old'] if age > MAX_AGE_MS
return [false, 'timestamp in the future'] if age < -MAX_AGE_MS
signed_payload = "#{timestamp}.#{raw_body}"
expected = OpenSSL::HMAC.hexdigest('SHA256', WEBHOOK_SECRET, signed_payload)
return [false, 'signature mismatch'] unless Rack::Utils.secure_compare(expected, signature_header)
[true, nil]
endRails example
# config/routes.rb
post '/brainerce-webhook', to: 'webhooks#brainerce'
# app/controllers/webhooks_controller.rb
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def brainerce
raw_body = request.raw_post # raw string, not params
valid, reason = verify_brainerce_webhook(
raw_body,
request.headers['X-Brainerce-Signature'],
request.headers['X-Brainerce-Timestamp']
)
return render(json: { error: reason }, status: 401) unless valid
event = JSON.parse(raw_body)
# process event ...
render json: { ok: true }, status: 202
end
endGo
package webhooks
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"net/http"
"os"
"strconv"
"time"
)
const maxAgeMs = 5 * 60 * 1000
var (
ErrMissingHeaders = errors.New("missing signature/timestamp headers")
ErrMalformedTime = errors.New("malformed timestamp")
ErrTimestampStale = errors.New("timestamp too old")
ErrTimestampFuture = errors.New("timestamp in the future")
ErrSignatureMismatch = errors.New("signature mismatch")
)
func VerifyBrainerceWebhook(
rawBody []byte,
signatureHeader, timestampHeader string,
) error {
if signatureHeader == "" || timestampHeader == "" {
return ErrMissingHeaders
}
timestamp, err := strconv.ParseInt(timestampHeader, 10, 64)
if err != nil {
return ErrMalformedTime
}
nowMs := time.Now().UnixMilli()
age := nowMs - timestamp
if age > maxAgeMs {
return ErrTimestampStale
}
if age < -maxAgeMs {
return ErrTimestampFuture
}
secret := os.Getenv("BRAINERCE_WEBHOOK_SECRET")
signed := fmt.Sprintf("%d.%s", timestamp, rawBody)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signed))
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(signatureHeader)) {
return ErrSignatureMismatch
}
return nil
}
// net/http handler
func BrainerceWebhookHandler(w http.ResponseWriter, r *http.Request) {
rawBody := make([]byte, 0, r.ContentLength)
// io.ReadAll or your framework's raw-body helper
// ...
if err := VerifyBrainerceWebhook(
rawBody,
r.Header.Get("X-Brainerce-Signature"),
r.Header.Get("X-Brainerce-Timestamp"),
); err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
// process event ...
w.WriteHeader(http.StatusAccepted)
}Testing your verifier
Once you have the snippet pasted in, test it with a known-good payload:
# Run from a terminal in your dev environment
SECRET='whsec_REPLACE_ME'
BODY='{"id":"evt_test","type":"order.created","createdAt":"2026-05-18T14:00:00.000Z","data":{"orderId":"ord_test"}}'
TS=$(date +%s)000
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -binary | xxd -p -c 256)
curl -X POST http://localhost:3000/brainerce-webhook \
-H "Content-Type: application/json" \
-H "X-Brainerce-Signature: $SIG" \
-H "X-Brainerce-Timestamp: $TS" \
--data-raw "$BODY"You should get 202 Accepted back. If you get 401, the snippet returned a reason; check it against the rules at the top of this page.