NextJS Example
javascriptimport { NextRequest, NextResponse } from "next/server"; import crypto from "crypto"; const WEBHOOK_SECRET = "whsec_<YOUR_SECRET>" const SECRET_PREFIX = "whsec_"; export async function POST(req: NextRequest) { const sigHeader = req.headers.get("x-basta-signature"); if (!sigHeader) { return new NextResponse("Missing signature header", { status: 400 }); } // Parse "t=<timestamp>,v1=<signature>" const parts = Object.fromEntries( sigHeader.split(",").map((p) => p.trim().split("=")) ); const timestamp = parseInt(parts.t, 10); const signature = parts.v1; if (!timestamp || !signature) { return new NextResponse("Invalid signature header", { status: 400 }); } // Read raw body text const rawBody = await req.text(); // Remove "whsec_" prefix const secret = WEBHOOK_SECRET.startsWith(SECRET_PREFIX) ? WEBHOOK_SECRET.slice(SECRET_PREFIX.length) : WEBHOOK_SECRET; // Recompute expected signature: HMAC-SHA256("timestamp.payload") const signedContent = `${timestamp}.${rawBody}`; const expected = crypto .createHmac("sha256", secret) .update(signedContent) .digest("hex"); // Constant-time comparison const valid = expected.length === signature.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature)); if (!valid) { return new NextResponse("Invalid signature", { status: 401 }); } // Signature verified — now it's safe to parse JSON const json = JSON.parse(rawBody); return NextResponse.json({ received: true, data: json }); }
Ruby Example
Below is a simple example on how to authenticate the webhook payload using a secret key
ruby#!/usr/bin/env ruby require "webrick" require "openssl" # ------------------------------------------------------------------------------ # Minimal Ruby webhook receiver example. # - Listens on /webhooks # - Reads the raw request body # - Verifies HMAC-SHA256 signatures using a shared secret # ------------------------------------------------------------------------------ PORT = 3000 SECRET_ID = "whsec_<YOUR_SECRET>" SECRET = SECRET_ID.sub(/^whsec_/, "") def secure_compare(a, b) return false if a.nil? || b.nil? return false unless a.bytesize == b.bytesize OpenSSL.fixed_length_secure_compare(a, b) rescue NoMethodError a == b end server = WEBrick::HTTPServer.new(Port: PORT) trap("INT") { server.shutdown } server.mount_proc "/webhooks" do |req, res| begin signature_header = req.header["x-basta-signature"]&.first if signature_header.nil? res.status = 401 res.body = "Missing X-Basta-Signature header" puts "Missing signature header" next end # Expected format: "t=<timestamp>,v1=<signature>" parts = signature_header.split(",") if parts.size != 2 res.status = 401 res.body = "Invalid signature format" puts "Invalid signature format: #{signature_header}" next end timestamp_part, sig_part = parts.map(&:strip) timestamp = timestamp_part.split("=").last received_sig = sig_part.split("=").last if timestamp.nil? || received_sig.nil? || timestamp.empty? || received_sig.empty? res.status = 401 res.body = "Invalid signature components" puts "Invalid signature components: #{signature_header}" next end raw_body = req.body || "" payload_to_sign = "#{timestamp}.#{raw_body}" computed_sig = OpenSSL::HMAC.hexdigest("SHA256", SECRET, payload_to_sign) valid = secure_compare(received_sig, computed_sig) puts "---- Incoming webhook ----" puts "Timestamp: #{timestamp}" puts "Payload bytes: #{raw_body.bytesize}" puts "Computed signature: #{computed_sig}" puts "Received signature: #{received_sig}" puts "Signature valid? #{valid ? 'YES' : 'NO'}" puts "---------------------------" if valid res.status = 200 res.body = "ok" else res.status = 401 res.body = "Invalid signature" end rescue => e puts "Error verifying webhook: #{e.class}: #{e.message}" res.status = 500 res.body = "internal error" end end puts "Listening locally on http://localhost:#{PORT}/webhooks" server.start