Skip to content

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.

HeaderDescription
X-Gateway-SignatureHMAC-SHA256 hex digest of the signed payload
X-Gateway-TimestampUnix timestamp when the delivery was signed
HMAC-SHA256("{timestamp}.{raw_body}", delivery_secret)

Where:

  • timestamp is the value of X-Gateway-Timestamp
  • raw_body is the raw HTTP request body (not parsed JSON)
  • delivery_secret is the delivery_secret (or project_delivery_secret) configured on the account

The result is a lowercase hex-encoded string.

  1. Extract X-Gateway-Signature and X-Gateway-Timestamp from the request headers
  2. Read the raw request body (before JSON parsing)
  3. Compute HMAC-SHA256("{timestamp}.{body}", secret) and hex-encode the result
  4. Compare the computed signature with the received signature using constant-time comparison
  5. Optionally, check that the timestamp is within an acceptable window (e.g., 5 minutes) to prevent replay attacks
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
end
end
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);
}
import hmac
import 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)
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)
);
}