{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-gensail-analytics/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":[]},"type":"markdown"},"seo":{"title":"Webhook Authentication","llmstxt":{"hide":false,"sections":[{"title":"Table of contents","includeFiles":["**/*"],"excludeFiles":[]}],"excludeFiles":[]}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"webhook-authentication","__idx":0},"children":["Webhook Authentication"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Gensail uses HMAC signatures to secure webhook deliveries. This allows you to verify that webhook payloads genuinely originate from Gensail and haven't been tampered with."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"overview","__idx":1},"children":["Overview"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When webhook authentication is enabled, every webhook request includes an ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Signature"]}," header containing a Stripe-style HMAC signature. Your server should verify this signature before processing the webhook payload."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"signature-format","__idx":2},"children":["Signature Format"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Signature"]}," header uses this format:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"X-Signature: t=1734789600,v1=a3b2c1d4e5f6789abc123def456...\n"},"children":[]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Component"},"children":["Component"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Description"},"children":["Description"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["t"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Unix timestamp (seconds) when the signature was generated"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["HMAC-SHA256 signature in hexadecimal format"]}]}]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"how-signatures-are-computed","__idx":3},"children":["How Signatures Are Computed"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Gensail computes the signature using:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Signed Payload"]},": ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{timestamp}.{json_body}"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Algorithm"]},": HMAC-SHA256"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Key"]},": Your webhook secret (provided by Gensail)"]}]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"signature = HMAC-SHA256(\n    key = webhook_secret,\n    message = \"{timestamp}.{json_body}\"\n)\n"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"verification-steps","__idx":4},"children":["Verification Steps"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["To verify a webhook signature:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Extract"]}," the timestamp (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["t"]},") and signature (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1"]},") from the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Signature"]}," header"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Reconstruct"]}," the signed payload: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{timestamp}.{raw_request_body}"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Compute"]}," your own HMAC-SHA256 using your webhook secret"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Compare"]}," your computed signature with the received ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1"]}," value"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Check"]}," that the timestamp is within tolerance (recommended: 5 minutes)"]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"code-examples","__idx":5},"children":["Code Examples"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"python","__idx":6},"children":["Python"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"import hmac\nimport hashlib\nimport time\n\ndef verify_webhook(payload_bytes: bytes, signature_header: str, secret: str) -> bool:\n    \"\"\"\n    Verify Gensail webhook signature.\n\n    Args:\n        payload_bytes: Raw request body (bytes)\n        signature_header: X-Signature header value\n        secret: Your webhook secret\n\n    Returns:\n        True if signature is valid, False otherwise\n    \"\"\"\n    try:\n        # Parse header: \"t=123,v1=abc...\"\n        parts = dict(p.split(\"=\", 1) for p in signature_header.split(\",\"))\n        timestamp = int(parts[\"t\"])\n        received_sig = parts[\"v1\"]\n    except (ValueError, KeyError):\n        return False  # Invalid header format\n\n    # Check timestamp (5 minute tolerance)\n    if abs(time.time() - timestamp) > 300:\n        return False  # Signature too old (replay protection)\n\n    # Compute expected signature\n    signed_payload = f\"{timestamp}.{payload_bytes.decode('utf-8')}\"\n    expected_sig = hmac.new(\n        secret.encode(),\n        signed_payload.encode(),\n        hashlib.sha256\n    ).hexdigest()\n\n    # Constant-time comparison (prevents timing attacks)\n    return hmac.compare_digest(received_sig, expected_sig)\n\n\n# Flask example\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\nWEBHOOK_SECRET = \"your_webhook_secret_here\"\n\n@app.route('/webhook', methods=['POST'])\ndef handle_webhook():\n    signature = request.headers.get('X-Signature', '')\n    payload = request.get_data()\n\n    if not verify_webhook(payload, signature, WEBHOOK_SECRET):\n        return jsonify({'error': 'Invalid signature'}), 401\n\n    # Process the verified webhook\n    data = request.get_json()\n    # ... your processing logic ...\n\n    return jsonify({'status': 'received'}), 200\n","lang":"python"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"nodejs","__idx":7},"children":["Node.js"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"const crypto = require('crypto');\n\nfunction verifyWebhook(payloadString, signatureHeader, secret) {\n    try {\n        // Parse header: \"t=123,v1=abc...\"\n        const parts = Object.fromEntries(\n            signatureHeader.split(',').map(p => p.split('='))\n        );\n        const timestamp = parseInt(parts.t);\n        const receivedSig = parts.v1;\n\n        // Check timestamp (5 minute tolerance)\n        const now = Math.floor(Date.now() / 1000);\n        if (Math.abs(now - timestamp) > 300) {\n            return false; // Signature too old\n        }\n\n        // Compute expected signature\n        const signedPayload = `${timestamp}.${payloadString}`;\n        const expectedSig = crypto\n            .createHmac('sha256', secret)\n            .update(signedPayload)\n            .digest('hex');\n\n        // Constant-time comparison\n        return crypto.timingSafeEqual(\n            Buffer.from(receivedSig),\n            Buffer.from(expectedSig)\n        );\n    } catch (e) {\n        return false;\n    }\n}\n\n// Express example\nconst express = require('express');\nconst app = express();\n\nconst WEBHOOK_SECRET = 'your_webhook_secret_here';\n\napp.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {\n    const signature = req.headers['x-signature'] || '';\n    const payload = req.body.toString();\n\n    if (!verifyWebhook(payload, signature, WEBHOOK_SECRET)) {\n        return res.status(401).json({ error: 'Invalid signature' });\n    }\n\n    // Process the verified webhook\n    const data = JSON.parse(payload);\n    // ... your processing logic ...\n\n    res.json({ status: 'received' });\n});\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"ruby","__idx":8},"children":["Ruby"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"ruby","header":{"controls":{"copy":{}}},"source":"require 'openssl'\nrequire 'json'\n\ndef verify_webhook(payload, signature_header, secret)\n  begin\n    # Parse header: \"t=123,v1=abc...\"\n    parts = signature_header.split(',').map { |p| p.split('=', 2) }.to_h\n    timestamp = parts['t'].to_i\n    received_sig = parts['v1']\n\n    # Check timestamp (5 minute tolerance)\n    return false if (Time.now.to_i - timestamp).abs > 300\n\n    # Compute expected signature\n    signed_payload = \"#{timestamp}.#{payload}\"\n    expected_sig = OpenSSL::HMAC.hexdigest('sha256', secret, signed_payload)\n\n    # Constant-time comparison\n    ActiveSupport::SecurityUtils.secure_compare(received_sig, expected_sig)\n  rescue\n    false\n  end\nend\n\n# Rails controller example\nclass WebhooksController < ApplicationController\n  skip_before_action :verify_authenticity_token\n\n  WEBHOOK_SECRET = ENV['WEBHOOK_SECRET']\n\n  def receive\n    signature = request.headers['X-Signature']\n    payload = request.raw_post\n\n    unless verify_webhook(payload, signature, WEBHOOK_SECRET)\n      render json: { error: 'Invalid signature' }, status: :unauthorized\n      return\n    end\n\n    # Process the verified webhook\n    data = JSON.parse(payload)\n    # ... your processing logic ...\n\n    render json: { status: 'received' }\n  end\nend\n","lang":"ruby"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"php","__idx":9},"children":["PHP"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"php","header":{"controls":{"copy":{}}},"source":"<?php\n\nfunction verifyWebhook(string $payload, string $signatureHeader, string $secret): bool {\n    // Parse header: \"t=123,v1=abc...\"\n    $parts = [];\n    foreach (explode(',', $signatureHeader) as $part) {\n        [$key, $value] = explode('=', $part, 2);\n        $parts[$key] = $value;\n    }\n\n    $timestamp = (int)($parts['t'] ?? 0);\n    $receivedSig = $parts['v1'] ?? '';\n\n    // Check timestamp (5 minute tolerance)\n    if (abs(time() - $timestamp) > 300) {\n        return false;\n    }\n\n    // Compute expected signature\n    $signedPayload = \"{$timestamp}.{$payload}\";\n    $expectedSig = hash_hmac('sha256', $signedPayload, $secret);\n\n    // Constant-time comparison\n    return hash_equals($receivedSig, $expectedSig);\n}\n\n// Usage example\n$payload = file_get_contents('php://input');\n$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';\n$secret = getenv('WEBHOOK_SECRET');\n\nif (!verifyWebhook($payload, $signature, $secret)) {\n    http_response_code(401);\n    echo json_encode(['error' => 'Invalid signature']);\n    exit;\n}\n\n// Process the verified webhook\n$data = json_decode($payload, true);\n// ... your processing logic ...\n\necho json_encode(['status' => 'received']);\n","lang":"php"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"security-best-practices","__idx":10},"children":["Security Best Practices"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"1-always-verify-signatures","__idx":11},"children":["1. Always Verify Signatures"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Never process webhook payloads without verifying the signature first. This protects against:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Spoofing"]},": Attackers sending fake webhooks"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Tampering"]},": Modified payloads in transit"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Replay attacks"]},": Re-sending old webhooks (use timestamp validation)"]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"2-use-constant-time-comparison","__idx":12},"children":["2. Use Constant-Time Comparison"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Always use constant-time string comparison functions to prevent timing attacks:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Python: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hmac.compare_digest()"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Node.js: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["crypto.timingSafeEqual()"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Ruby: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ActiveSupport::SecurityUtils.secure_compare()"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["PHP: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hash_equals()"]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"3-validate-timestamps","__idx":13},"children":["3. Validate Timestamps"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Reject signatures older than 5 minutes to prevent replay attacks. This is especially important if an attacker captures a valid webhook."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"4-store-secrets-securely","__idx":14},"children":["4. Store Secrets Securely"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Use environment variables, not hardcoded values"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Never commit secrets to version control"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Rotate secrets periodically"]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"5-use-https","__idx":15},"children":["5. Use HTTPS"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Always use HTTPS for your webhook endpoint to encrypt data in transit."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"troubleshooting","__idx":16},"children":["Troubleshooting"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"signature-mismatch-error","__idx":17},"children":["\"Signature mismatch\" Error"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Check the secret"]},": Ensure you're using the correct webhook secret"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Raw body"]},": Use the raw request body, not parsed JSON"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Encoding"]},": Ensure UTF-8 encoding for both payload and secret"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["No modifications"]},": Don't modify the payload before verification"]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"signature-expired-error","__idx":18},"children":["\"Signature expired\" Error"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Server time"]},": Ensure your server's clock is synchronized (use NTP)"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Tolerance"]},": Consider increasing tolerance if network latency is high"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Timestamp format"]},": The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["t"]}," value is Unix seconds, not milliseconds"]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"no-x-signature-header","__idx":19},"children":["No X-Signature Header"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Configuration"]},": Verify webhook authentication is enabled for your workspace"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Contact support"]},": Ensure your webhook secret is properly configured"]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"getting-your-webhook-secret","__idx":20},"children":["Getting Your Webhook Secret"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Your webhook secret is provided when your workspace is configured. Contact your Gensail administrator or check your workspace settings in the Gensail platform."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If you need to rotate your webhook secret, contact ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"mailto:support@gensail.com"},"children":["support@gensail.com"]},"."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"testing","__idx":21},"children":["Testing"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["To test your webhook verification locally:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"# Generate a test signature\nSECRET=\"your_webhook_secret\"\nTIMESTAMP=$(date +%s)\nPAYLOAD='{\"test\": \"data\"}'\nSIGNATURE=$(echo -n \"${TIMESTAMP}.${PAYLOAD}\" | openssl dgst -sha256 -hmac \"$SECRET\" | cut -d' ' -f2)\n\n# Send test webhook\ncurl -X POST http://localhost:3000/webhook \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-Signature: t=${TIMESTAMP},v1=${SIGNATURE}\" \\\n  -d \"${PAYLOAD}\"\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"support","__idx":22},"children":["Support"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["For questions about webhook authentication, contact ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"mailto:support@gensail.com"},"children":["support@gensail.com"]},"."]}]},"headings":[{"value":"Webhook Authentication","id":"webhook-authentication","depth":1},{"value":"Overview","id":"overview","depth":2},{"value":"Signature Format","id":"signature-format","depth":2},{"value":"How Signatures Are Computed","id":"how-signatures-are-computed","depth":2},{"value":"Verification Steps","id":"verification-steps","depth":2},{"value":"Code Examples","id":"code-examples","depth":2},{"value":"Python","id":"python","depth":3},{"value":"Node.js","id":"nodejs","depth":3},{"value":"Ruby","id":"ruby","depth":3},{"value":"PHP","id":"php","depth":3},{"value":"Security Best Practices","id":"security-best-practices","depth":2},{"value":"1. Always Verify Signatures","id":"1-always-verify-signatures","depth":3},{"value":"2. Use Constant-Time Comparison","id":"2-use-constant-time-comparison","depth":3},{"value":"3. Validate Timestamps","id":"3-validate-timestamps","depth":3},{"value":"4. Store Secrets Securely","id":"4-store-secrets-securely","depth":3},{"value":"5. Use HTTPS","id":"5-use-https","depth":3},{"value":"Troubleshooting","id":"troubleshooting","depth":2},{"value":"\"Signature mismatch\" Error","id":"signature-mismatch-error","depth":3},{"value":"\"Signature expired\" Error","id":"signature-expired-error","depth":3},{"value":"No X-Signature Header","id":"no-x-signature-header","depth":3},{"value":"Getting Your Webhook Secret","id":"getting-your-webhook-secret","depth":2},{"value":"Testing","id":"testing","depth":2},{"value":"Support","id":"support","depth":2}],"frontmatter":{"seo":{"title":"Webhook Authentication"}},"lastModified":"2026-03-27T16:51:42.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/gensail-analytics/guides/webhook-authentication","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}