Skip to main content

Verifying Webhook Signatures (Optional)

Each webhook is assigned a unique Signing Secret, visible in the Edit Webhook panel of the Portal. You can use this secret to verify that incoming requests were sent by our platform and have not been tampered with.

Every webhook request includes an X-Webhook-Signature header in the format:

X-Webhook-Signature: t=1738900000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

Where t is the Unix timestamp of when the request was sent, and v1 is the HMAC-SHA256 signature.

How Verification Works

  1. Extract the t (timestamp) and v1 (signature) values from the X-Webhook-Signature header.
  2. Build the signing payload by concatenating the timestamp, a period (.), and the raw request body: {t}.{body}
  3. Compute the HMAC-SHA256 of the signing payload using your webhook's Signing Secret.
  4. Compare your computed signature with the v1 value from the header.
  5. Optionally, check that the timestamp is within an acceptable window (e.g., 5 minutes) to prevent replay attacks.

Verification with Official SDKs

Our official SDKs provide a Webhook.verify() helper that handles header parsing, HMAC computation, constant-time comparison, and timestamp tolerance checking. This is the recommended approach.

PHP

use GeneratorLabs\Webhook;

$header = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');

try {
$payload = Webhook::verify($body, $header, $signing_secret);
// $payload contains the decoded event data
} catch (GeneratorLabs\Exception $e) {
http_response_code(403);
}

Python

from generatorlabs import Webhook

header = request.headers.get('X-Webhook-Signature', '')
body = request.get_data(as_text=True)

try:
payload = Webhook.verify(body, header, signing_secret)
# payload contains the decoded event data
except Exception:
return jsonify({'error': 'Invalid signature'}), 403

Node.js

import { Webhook } from 'generatorlabs';

const header = req.headers['x-webhook-signature'];
const body = req.body; // raw string body

try {
const payload = Webhook.verify(body, header, signingSecret);
// payload contains the decoded event data
} catch (error) {
res.status(403).json({ error: 'Invalid signature' });
}

Go

import generatorlabs "github.com/generator-labs/go-sdk"

header := r.Header.Get("X-Webhook-Signature")
body, _ := io.ReadAll(r.Body)

payload, err := generatorlabs.VerifyWebhook(string(body), header, signingSecret, generatorlabs.DefaultWebhookTolerance)
if err != nil {
http.Error(w, "Invalid signature", http.StatusForbidden)
return
}
// payload contains the decoded event data

Ruby

require 'generatorlabs'

header = request.env['HTTP_X_WEBHOOK_SIGNATURE'] || ''
body = request.body.read

begin
payload = GeneratorLabs::Webhook.verify(body, header, signing_secret)
# payload contains the decoded event data
rescue GeneratorLabs::Error
halt 403, { error: 'Invalid signature' }.to_json
end

All SDKs default to a 5-minute timestamp tolerance window. You can customize this (in seconds) via an optional parameter, or pass 0 to disable timestamp checking.

Each SDK repository includes a complete webhook verification example in the examples/ directory. See the SDK README for your language for details.

Manual Verification

If you prefer not to use an SDK, you can verify signatures manually:

  1. Parse the t and v1 values from the X-Webhook-Signature header.
  2. Compute: HMAC-SHA256(key=signing_secret, message="{t}.{raw_body}").
  3. Compare your computed hex digest with v1 using a constant-time comparison.
  4. Optionally check that t is within an acceptable time window.
$header = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');

// Parse the header
preg_match('/t=(\d+),v1=([a-f0-9]+)/', $header, $matches);
$timestamp = $matches[1] ?? '';
$signature = $matches[2] ?? '';

// Check timestamp tolerance (5 minutes)
if (abs(time() - intval($timestamp)) > 300) {
http_response_code(403);
exit;
}

// Compute and compare the signature
$expected = hash_hmac('sha256', $timestamp . '.' . $body, $signing_secret);

if (hash_equals($expected, $signature)) {
$payload = json_decode($body, true);
// Signature is valid, process the event
}

Regenerating a Signing Secret

You can regenerate a webhook's Signing Secret at any time from the Edit Webhook panel. After regeneration, the previous secret will no longer produce valid signatures: make sure to update your application with the new secret.