Verifying Signatures
Every webhook delivered by Transyt includes cryptographic signature headers. You should always verify these to ensure the request originated from Transyt and hasn’t been tampered with.
Headers
Section titled “Headers”| Header | Description |
|---|---|
X-Gateway-Signature | HMAC-SHA256 hex digest of the signed payload |
X-Gateway-Timestamp | Unix timestamp when the delivery was signed |
Signature Formula
Section titled “Signature Formula”HMAC-SHA256("{timestamp}.{raw_body}", delivery_secret)Where:
timestampis the value ofX-Gateway-Timestampraw_bodyis the raw HTTP request body (not parsed JSON)delivery_secretis thedelivery_secret(orproject_delivery_secret) configured on the account
The result is a lowercase hex-encoded string.
Verification Steps
Section titled “Verification Steps”- Extract
X-Gateway-SignatureandX-Gateway-Timestampfrom the request headers - Read the raw request body (before JSON parsing)
- Compute
HMAC-SHA256("{timestamp}.{body}", secret)and hex-encode the result - Compare the computed signature with the received signature using constant-time comparison
- Optionally, check that the timestamp is within an acceptable window (e.g., 5 minutes) to prevent replay attacks
Ruby (Rails)
Section titled “Ruby (Rails)”class WebhooksController < ApplicationController skip_before_action :verify_authenticity_token
def create timestamp = request.headers["X-Gateway-Timestamp"] signature = request.headers["X-Gateway-Signature"] body = request.raw_post
expected = OpenSSL::HMAC.hexdigest( "SHA256", ENV.fetch("GATEWAY_DELIVERY_SECRET"), "#{timestamp}.#{body}" )
unless ActiveSupport::SecurityUtils.secure_compare(signature.to_s, expected) head :unauthorized return end
# Process the webhook... head :ok endendPHP (Laravel)
Section titled “PHP (Laravel)”public function handleWebhook(Request $request){ $timestamp = $request->header('X-Gateway-Timestamp'); $signature = $request->header('X-Gateway-Signature'); $body = $request->getContent(); $secret = config('services.transyt.delivery_secret');
$expected = hash_hmac('sha256', "{$timestamp}.{$body}", $secret);
if (!hash_equals($expected, $signature ?? '')) { abort(401, 'Invalid signature'); }
// Process the webhook... return response('OK', 200);}Python
Section titled “Python”import hmacimport hashlib
def verify_signature(timestamp, body, signature, secret): expected = hmac.new( secret.encode(), f"{timestamp}.{body}".encode(), hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature)Node.js
Section titled “Node.js”const crypto = require('crypto');
function verifySignature(timestamp, body, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(`${timestamp}.${body}`) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) );}