> ## Documentation Index
> Fetch the complete documentation index at: https://docs.autosend.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify Webhook Requests

> Learn how to verify that webhook requests are genuinely from AutoSend using HMAC-SHA256 signature verification.

export const APP_PATHS = {
  home: '/',
  quickstart: '/quickstart',
  domainConfiguration: '/domain',
  apiReference: '/api-reference',
  sendEmail: '/api-reference/mails/send',
  bulkSendEmail: '/api-reference/mails/bulk',
  upsertContactApiRef: '/api-reference/contacts/upsert-contact',
  transactional: '/transactional-emails',
  emailActivity: '/transactional-emails/email-activity',
  emailTemplates: '/transactional-emails/email-templates',
  sendingEmail: '/quickstart/email-using-api',
  transactionalTroubleshooting: '/transactional-emails/troubleshooting',
  marketing: '/marketing-emails',
  campaigns: '/marketing-emails/campaigns',
  contacts: '/marketing-emails/contacts',
  contactsIntroduction: '/marketing-emails/contacts/introduction',
  contactsImportCsv: '/marketing-emails/contacts/import-csv',
  contactsLists: '/marketing-emails/contacts/lists',
  contactsSegments: '/marketing-emails/contacts/segments',
  contactsCustomFields: '/marketing-emails/contacts/custom-fields',
  sender: '/marketing-emails/sender',
  unsubscribeGroups: '/others/unsubscribe-groups',
  webhookIntroduction: '/others/webhooks/introduction',
  webhookEventType: '/others/webhooks/event-type',
  webhookRetries: '/others/webhooks/retries',
  webhookVerifyRequests: '/others/webhooks/verify-requests',
  dynamicTemplates: '/dynamic-templates',
  guides: '/guides',
  sitemap: '/sitemap.xml',
  team: '/others/team',
  automations: '/automations',
  smtpIntroduction: '/quickstart/smtp',
  betterAuth: '/guides/better-auth',
  templateVariables: '/transactional-emails/variables',
  suppressions: '/others/suppressions',
  rateLimit: '/api-reference/rate-limit',
  nodejsSdk: '/sdk/nodejs',
  smtpIntegrationGuides: '/guides/smtp',
  apiKeys: '/api-keys',
  apiReferenceIntroduction: '/api-reference/introduction',
  lovableGuide: '/ai/integrations/lovable',
  aiIntroduction: '/ai/introduction',
  aiSkills: '/ai/skills',
  aiMcpServer: '/ai/mcp-server',
  aiLovable: '/ai/integrations/lovable',
  aiBolt: '/ai/integrations/bolt',
  aiV0: '/ai/integrations/v0',
  aiReplit: '/ai/integrations/replit',
  mcpClaude: '/ai/mcp-clients/claude',
  mcpCursor: '/ai/mcp-clients/cursor',
  mcpCopilot: '/ai/mcp-clients/copilot',
  mcpWindsurf: '/ai/mcp-clients/windsurf',
  mcpCodex: '/ai/mcp-clients/codex',
  mcpAntigravity: '/ai/mcp-clients/antigravity',
  mcpChatgpt: '/ai/mcp-clients/chatgpt',
  mcpRaycast: '/ai/mcp-clients/raycast',
  domainWarmup: '/marketing-emails/domain-warmup',
  projects: '/projects',
  createAutomationApi: '/api-reference/automations/create-automation'
};

export const AUTOSEND_PATHS = {
  dashboard: 'https://autosend.com/dashboard',
  apiKey: 'https://autosend.com/account/api-key',
  faqs: 'https://autosend.com/faq',
  marketingEmails: 'https://autosend.com/marketing-emails',
  webhooks: 'https://autosend.com/webhooks',
  composeByAutoSend: 'https://autosend.com/compose',
  emailActivity: 'https://autosend.com/email-activities',
  team: 'https://autosend.com/settings/team',
  pricing: 'https://autosend.com/pricing',
  verifyEmail: 'https://autosend.com/compose/email-builder?template=verify-email',
  welcomeEmail: 'https://autosend.com/compose/email-builder?template=welcome-email',
  productUpdate: 'https://autosend.com/compose/email-builder?template=product-update',
  newsletter: 'https://autosend.com/compose/email-builder?template=newsletter',
  automations: 'https://autosend.com/automations',
  globalSuppressions: 'https://autosend.com/suppressions/global',
  signup: 'https://autosend.com/signup',
  domains: 'https://autosend.com/settings/domains',
  logoKit: 'https://asend.email/logo',
  contactsPage: 'https://autosend.com/contacts/list-and-segments'
};

## Why Verify Webhooks?

**Security is critical.** Anyone can send a POST request to your webhook endpoint. Without verification, malicious actors could:

* Send fake events to corrupt your data
* Trigger unwanted actions in your application
* Cause your system to process fraudulent information
* Launch denial-of-service attacks

**Always verify webhook signatures** to ensure requests are genuinely from AutoSend.

***

## How AutoSend Signs Webhooks

Every webhook request from AutoSend includes an `X-Webhook-Signature` header containing an HMAC-SHA256 signature.

### Signature Generation

AutoSend generates the signature using this process:

1. **Format the webhook payload** with the event type, timestamp, and event data
2. **Convert the payload to JSON string** (the raw request body)
3. **Compute HMAC-SHA256** using your webhook secret as the key
4. **Convert to hexadecimal** format
5. **Add as header**: `X-Webhook-Signature: <signature>`

```javascript theme={null}
// How AutoSend generates signatures
const payload = {
  type: event,
  createdAt: new Date().toISOString(),
  data: eventData,
};

const payloadString = JSON.stringify(payload);
const signature = crypto
  .createHmac('sha256', webhookSecret)
  .update(payloadString)
  .digest('hex');
```

***

## Webhook Request Headers

Every webhook request includes these headers:

<ParamField path="X-Webhook-Signature" type="string" required>
  HMAC-SHA256 signature of the request body in hexadecimal format

  Example: `"a1b2c3d4e5f6..."`
</ParamField>

<ParamField path="X-Webhook-Event" type="string" required>
  The event type

  Example: `"email.opened"`
</ParamField>

<ParamField path="X-Webhook-Delivery-Id" type="string" required>
  Unique delivery identifier (job ID from the queue system)

  Example: `"delivery-123..."`
</ParamField>

<ParamField path="X-Webhook-Timestamp" type="string" required>
  Unix timestamp in milliseconds when the webhook was sent

  Example: `"1699790400000"`
</ParamField>

<ParamField path="Content-Type" type="string" required>
  Always `application/json`

  Example: `"application/json"`
</ParamField>

<ParamField path="User-Agent" type="string">
  AutoSend user agent (if set)

  Example: `"AutoSend-Webhooks/1.0"`
</ParamField>

***

## Steps to Verify Signatures

<Steps>
  <Step title="Extract the signature from the X-Webhook-Signature header" titleSize="h3" />

  <Step title="Get the raw request body as a string (before parsing)" titleSize="h3" />

  <Step title="Compute the expected signature using your webhook secret" titleSize="h3" />

  <Step title="Compare signatures using a constant-time comparison function" titleSize="h3" />
</Steps>

<CodeGroup>
  ```javascript Node.js expandable theme={null}
  const express = require("express");
  const crypto = require("crypto");

  const app = express();

  // Important: Store raw body for signature verification
  app.use(
  express.json({
  verify: (req, res, buf) => {
  req.rawBody = buf.toString("utf8");
  },
  })
  );

  function verifyWebhookSignature(req, webhookSecret) {
  const receivedSignature = req.headers["x-webhook-signature"];

  if (!receivedSignature) {
  return false;
  }

  // Compute expected signature using raw body
  const expectedSignature = crypto
  .createHmac("sha256", webhookSecret)
  .update(req.rawBody)
  .digest("hex");

  // Use constant-time comparison to prevent timing attacks
  try {
  return crypto.timingSafeEqual(
  Buffer.from(receivedSignature),
  Buffer.from(expectedSignature)
  );
  } catch (error) {
  // Buffer lengths don't match
  return false;
  }
  }

  app.post("/webhooks/autosend", (req, res) => {
  const webhookSecret = process.env.WEBHOOK_SECRET;

  // Verify signature
  if (!verifyWebhookSignature(req, webhookSecret)) {
  console.error("Invalid webhook signature");
  return res.status(401).json({ error: "Invalid signature" });
  }

  // Process webhook
  const { type, data } = req.body;
  console.log(`Verified webhook: ${type}`);

  res.status(200).json({ received: true });
  });

  ```

  ```python Python expandable theme={null}
  from flask import Flask, request, jsonify
  import hmac
  import hashlib
  import os

  app = Flask(__name__)
  WEBHOOK_SECRET = os.getenv('WEBHOOK_SECRET')

  def verify_webhook_signature(payload, signature, secret):
      """Verify webhook signature using HMAC-SHA256"""
      if not signature:
          return False

      # Compute expected signature
      expected = hmac.new(
          secret.encode('utf-8'),
          payload.encode('utf-8'),
          hashlib.sha256
      ).hexdigest()

      # Constant-time comparison
      return hmac.compare_digest(expected, signature)

  @app.route('/webhooks/autosend', methods=['POST'])
  def webhook():
      # Get signature from header
      signature = request.headers.get('X-Webhook-Signature')

      # Get raw payload
      payload = request.get_data(as_text=True)

      # Verify signature
      if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
          return jsonify({'error': 'Invalid signature'}), 401

      # Parse and process webhook
      data = request.json
      event_type = data.get('type')
      print(f'Verified webhook: {event_type}')

      return jsonify({'received': True}), 200

  if __name__ == '__main__':
      app.run(port=3000)
  ```

  ```php PHP expandable theme={null}
  <?php
  function verifyWebhookSignature($payload, $receivedSignature, $secret) {
      if (empty($receivedSignature)) {
          return false;
      }

      // Compute expected signature
      $expectedSignature = hash_hmac('sha256', $payload, $secret);

      // Constant-time comparison
      return hash_equals($expectedSignature, $receivedSignature);
  }

  // Get raw POST body
  $payload = file_get_contents('php://input');

  // Get signature from header
  $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';

  // Get secret from environment
  $webhookSecret = getenv('WEBHOOK_SECRET');

  // Verify signature
  if (!verifyWebhookSignature($payload, $signature, $webhookSecret)) {
      http_response_code(401);
      echo json_encode(['error' => 'Invalid signature']);
      exit;
  }

  // Parse and process webhook
  $data = json_decode($payload, true);
  $eventType = $data['type'];

  error_log("Verified webhook: $eventType");

  http_response_code(200);
  echo json_encode(['received' => true]);
  ?>
  ```

  ```ruby Ruby expandable theme={null}
  require 'sinatra'
  require 'json'
  require 'openssl'

  WEBHOOK_SECRET = ENV['WEBHOOK_SECRET']

  def verify_webhook_signature(payload, signature, secret)
    return false if signature.nil? || signature.empty?

    # Compute expected signature
    expected = OpenSSL::HMAC.hexdigest('sha256', secret, payload)

    # Constant-time comparison
    Rack::Utils.secure_compare(expected, signature)
  end

  post '/webhooks/autosend' do
    # Get raw body and signature
    payload = request.body.read
    signature = request.env['HTTP_X_WEBHOOK_SIGNATURE']

    # Verify signature
    unless verify_webhook_signature(payload, signature, WEBHOOK_SECRET)
      status 401
      return { error: 'Invalid signature' }.to_json
    end

    # Parse and process webhook
    data = JSON.parse(payload)
    event_type = data['type']

    puts "Verified webhook: #{event_type}"

    status 200
    { received: true }.to_json
  end
  ```

  ```go Go expandable theme={null}
  package main

  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
      "encoding/json"
      "io/ioutil"
      "net/http"
      "os"
  )

  func verifyWebhookSignature(payload []byte, signature string, secret string) bool {
      if signature == "" {
          return false
      }

      // Compute expected signature
      mac := hmac.New(sha256.New, []byte(secret))
      mac.Write(payload)
      expected := hex.EncodeToString(mac.Sum(nil))

      // Constant-time comparison
      return hmac.Equal([]byte(expected), []byte(signature))
  }

  func webhookHandler(w http.ResponseWriter, r *http.Request) {
      // Read raw body
      payload, err := ioutil.ReadAll(r.Body)
      if err != nil {
          http.Error(w, "Error reading body", http.StatusBadRequest)
          return
      }

      // Get signature from header
      signature := r.Header.Get("X-Webhook-Signature")

      // Get secret from environment
      webhookSecret := os.Getenv("WEBHOOK_SECRET")

      // Verify signature
      if !verifyWebhookSignature(payload, signature, webhookSecret) {
          http.Error(w, "Invalid signature", http.StatusUnauthorized)
          return
      }

      // Parse webhook
      var data map[string]interface{}
      json.Unmarshal(payload, &data)

      // Process webhook
      eventType := data["type"].(string)
      println("Verified webhook:", eventType)

      w.WriteHeader(http.StatusOK)
      json.NewEncoder(w).Encode(map[string]bool{"received": true})
  }

  func main() {
      http.HandleFunc("/webhooks/autosend", webhookHandler)
      http.ListenAndServe(":3000", nil)
  }
  ```
</CodeGroup>

***

## Retrieving Your Webhook Secret

Your webhook secret is shown only once when you create the webhook. If you've lost it, you can retrieve it:

<Steps>
  <Step title="Navigate to Webhooks from the AutoSend sidebar" titleSize="h3" />

  <Step title="Click on the webhook you want to manage" titleSize="h3" />

  <Step title="Click the Reveal Secret button" titleSize="h3" />

  <Step title="Copy the secret and store it securely in your environment variables" titleSize="h3" />
</Steps>

<Warning>
  Store your webhook secret securely. Never commit it to version control or
  expose it in client-side code.
</Warning>

***

## Complete Production Example

Here's a complete, production-ready webhook endpoint with signature verification, timestamp validation, and error handling:

<CodeGroup>
  ```javascript Node.js expandable theme={null}
  const express = require("express");
  const crypto = require("crypto");
  const app = express();

  // Store raw body for signature verification
  app.use(
  express.json({
  verify: (req, res, buf) => {
  req.rawBody = buf.toString("utf8");
  },
  })
  );

  // Webhook secret from environment
  const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

  if (!WEBHOOK_SECRET) {
  throw new Error("WEBHOOK_SECRET environment variable is required");
  }

  // Verify webhook signature
  function verifyWebhookSignature(req) {
  const receivedSignature = req.headers["x-webhook-signature"];

  if (!receivedSignature) {
  return false;
  }

  const expectedSignature = crypto
  .createHmac("sha256", WEBHOOK_SECRET)
  .update(req.rawBody)
  .digest("hex");

  try {
  return crypto.timingSafeEqual(
  Buffer.from(receivedSignature),
  Buffer.from(expectedSignature)
  );
  } catch (error) {
  // Buffer lengths don't match
  return false;
  }
  }

  // Validate timestamp (optional but recommended)
  function isTimestampValid(timestamp, maxAgeSeconds = 300) {
  if (!timestamp) {
  return false;
  }

  const now = Date.now();
  const age = now - parseInt(timestamp);

  // Reject if older than 5 minutes or more than 1 minute in the future
  return age < maxAgeSeconds \* 1000 && age > -60000;
  }

  // Webhook endpoint
  app.post("/webhooks/autosend", async (req, res) => {
  const deliveryId = req.headers["x-webhook-delivery-id"];
  const timestamp = req.headers["x-webhook-timestamp"];
  const event = req.headers["x-webhook-event"];

  // Validate timestamp
  if (!isTimestampValid(timestamp)) {
  console.error("Invalid or expired timestamp", { deliveryId, timestamp });
  return res.status(401).json({ error: "Invalid timestamp" });
  }

  // Verify signature
  if (!verifyWebhookSignature(req)) {
  console.error("Invalid signature", { deliveryId, event });
  return res.status(401).json({ error: "Invalid signature" });
  }

  // Process webhook
  const { type, data, createdAt } = req.body;

  console.log("Webhook received and verified", {
  deliveryId,
  type,
  event,
  createdAt,
  });

  try {
  // Queue for background processing to respond quickly
  await processWebhookAsync(type, data);

      // Respond with 200 to acknowledge receipt
      res.status(200).json({ received: true });

  } catch (error) {
  console.error("Error processing webhook", { deliveryId, error });
  // Still return 200 to avoid retries for processing errors
  // Log the error for investigation
  res.status(200).json({ received: true, warning: "Processing queued" });
  }
  });

  async function processWebhookAsync(type, data) {
  // Implement your webhook processing logic here
  // This should be non-blocking and ideally queued
  console.log(`Processing ${type} event:`, data);
  }

  app.listen(3000, () => {
  console.log("Webhook server listening on port 3000");
  });

  ```

  ```python Python expandable theme={null}
  from flask import Flask, request, jsonify
  import hmac
  import hashlib
  import os
  import time
  import json
  import logging

  app = Flask(__name__)
  logging.basicConfig(level=logging.INFO)
  logger = logging.getLogger(__name__)

  WEBHOOK_SECRET = os.getenv('WEBHOOK_SECRET')

  if not WEBHOOK_SECRET:
      raise ValueError("WEBHOOK_SECRET environment variable is required")

  def verify_webhook_signature(payload, signature, secret):
      """Verify webhook signature using HMAC-SHA256"""
      if not signature:
          return False

      expected = hmac.new(
          secret.encode('utf-8'),
          payload.encode('utf-8'),
          hashlib.sha256
      ).hexdigest()

      return hmac.compare_digest(expected, signature)

  def is_timestamp_valid(timestamp, max_age_seconds=300):
      """Validate webhook timestamp"""
      if not timestamp:
          return False

      try:
          now = int(time.time() * 1000)
          ts = int(timestamp)
          age = now - ts

          # Reject if older than 5 minutes or more than 1 minute in the future
          return age < max_age_seconds * 1000 and age > -60000
      except (ValueError, TypeError):
          return False

  @app.route('/webhooks/autosend', methods=['POST'])
  def webhook():
      # Get headers
      delivery_id = request.headers.get('X-Webhook-Delivery-Id')
      timestamp = request.headers.get('X-Webhook-Timestamp')
      event = request.headers.get('X-Webhook-Event')
      signature = request.headers.get('X-Webhook-Signature')

      # Get raw payload
      payload = request.get_data(as_text=True)

      # Validate timestamp
      if not is_timestamp_valid(timestamp):
          logger.error(f"Invalid timestamp: {delivery_id}, {timestamp}")
          return jsonify({'error': 'Invalid timestamp'}), 401

      # Verify signature
      if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
          logger.error(f"Invalid signature: {delivery_id}, {event}")
          return jsonify({'error': 'Invalid signature'}), 401

      # Parse and process webhook
      data = request.json
      event_type = data.get('type')
      event_data = data.get('data')
      created_at = data.get('createdAt')

      logger.info(f"Webhook received and verified: {delivery_id}, {event_type}")

      try:
          # Process webhook asynchronously
          process_webhook_async(event_type, event_data)
          return jsonify({'received': True}), 200
      except Exception as error:
          logger.error(f"Error processing webhook: {delivery_id}, {error}")
          # Still return 200 to avoid retries
          return jsonify({'received': True, 'warning': 'Processing queued'}), 200

  def process_webhook_async(event_type, data):
      """Process webhook in background"""
      logger.info(f"Processing {event_type} event: {data}")
      # Implement your webhook processing logic here

  if __name__ == '__main__':
      app.run(port=3000)
  ```

  ```php PHP expandable theme={null}
  <?php

  error_reporting(E_ALL);
  ini_set('display_errors', 1);

  $webhookSecret = getenv('WEBHOOK_SECRET');

  if (!$webhookSecret) {
      throw new Exception("WEBHOOK_SECRET environment variable is required");
  }

  function verifyWebhookSignature($payload, $receivedSignature, $secret) {
      if (empty($receivedSignature)) {
          return false;
      }

      $expectedSignature = hash_hmac('sha256', $payload, $secret);
      return hash_equals($expectedSignature, $receivedSignature);
  }

  function isTimestampValid($timestamp, $maxAgeSeconds = 300) {
      if (empty($timestamp)) {
          return false;
      }

      $now = round(microtime(true) * 1000);
      $ts = (int)$timestamp;
      $age = $now - $ts;

      // Reject if older than 5 minutes or more than 1 minute in the future
      return $age < ($maxAgeSeconds * 1000) && $age > -60000;
  }

  // Get raw POST body
  $payload = file_get_contents('php://input');

  // Get headers
  $deliveryId = $_SERVER['HTTP_X_WEBHOOK_DELIVERY_ID'] ?? '';
  $timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
  $event = $_SERVER['HTTP_X_WEBHOOK_EVENT'] ?? '';
  $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';

  // Validate timestamp
  if (!isTimestampValid($timestamp)) {
      error_log("Invalid timestamp: {$deliveryId}, {$timestamp}");
      http_response_code(401);
      echo json_encode(['error' => 'Invalid timestamp']);
      exit;
  }

  // Verify signature
  if (!verifyWebhookSignature($payload, $signature, $webhookSecret)) {
      error_log("Invalid signature: {$deliveryId}, {$event}");
      http_response_code(401);
      echo json_encode(['error' => 'Invalid signature']);
      exit;
  }

  // Parse and process webhook
  $data = json_decode($payload, true);
  $eventType = $data['type'] ?? '';
  $eventData = $data['data'] ?? [];
  $createdAt = $data['createdAt'] ?? '';

  error_log("Webhook received and verified: {$deliveryId}, {$eventType}");

  try {
      // Process webhook
      processWebhookAsync($eventType, $eventData);

      http_response_code(200);
      echo json_encode(['received' => true]);
  } catch (Exception $error) {
      error_log("Error processing webhook: {$deliveryId}, {$error->getMessage()}");
      // Still return 200 to avoid retries
      http_response_code(200);
      echo json_encode(['received' => true, 'warning' => 'Processing queued']);
  }

  function processWebhookAsync($eventType, $data) {
      error_log("Processing {$eventType} event");
      // Implement your webhook processing logic here
  }
  ?>
  ```

  ```ruby Ruby expandable theme={null}
  require 'sinatra'
  require 'json'
  require 'openssl'
  require 'time'
  require 'logger'

  WEBHOOK_SECRET = ENV['WEBHOOK_SECRET']

  if WEBHOOK_SECRET.nil? || WEBHOOK_SECRET.empty?
    raise "WEBHOOK_SECRET environment variable is required"
  end

  logger = Logger.new(STDOUT)

  def verify_webhook_signature(payload, signature, secret)
    return false if signature.nil? || signature.empty?

    expected = OpenSSL::HMAC.hexdigest('sha256', secret, payload)
    Rack::Utils.secure_compare(expected, signature)
  end

  def is_timestamp_valid(timestamp, max_age_seconds = 300)
    return false if timestamp.nil? || timestamp.empty?

    now = (Time.now.to_f * 1000).to_i
    ts = timestamp.to_i
    age = now - ts

    # Reject if older than 5 minutes or more than 1 minute in the future
    age < (max_age_seconds * 1000) && age > -60000
  end

  post '/webhooks/autosend' do
    # Get headers
    delivery_id = request.env['HTTP_X_WEBHOOK_DELIVERY_ID']
    timestamp = request.env['HTTP_X_WEBHOOK_TIMESTAMP']
    event = request.env['HTTP_X_WEBHOOK_EVENT']
    signature = request.env['HTTP_X_WEBHOOK_SIGNATURE']

    # Get raw body
    payload = request.body.read

    # Validate timestamp
    unless is_timestamp_valid(timestamp)
      logger.error("Invalid timestamp: #{delivery_id}, #{timestamp}")
      status 401
      return { error: 'Invalid timestamp' }.to_json
    end

    # Verify signature
    unless verify_webhook_signature(payload, signature, WEBHOOK_SECRET)
      logger.error("Invalid signature: #{delivery_id}, #{event}")
      status 401
      return { error: 'Invalid signature' }.to_json
    end

    # Parse and process webhook
    data = JSON.parse(payload)
    event_type = data['type']
    event_data = data['data']
    created_at = data['createdAt']

    logger.info("Webhook received and verified: #{delivery_id}, #{event_type}")

    begin
      # Process webhook asynchronously
      process_webhook_async(event_type, event_data)
      status 200
      { received: true }.to_json
    rescue => error
      logger.error("Error processing webhook: #{delivery_id}, #{error.message}")
      # Still return 200 to avoid retries
      status 200
      { received: true, warning: 'Processing queued' }.to_json
    end
  end

  def process_webhook_async(event_type, data)
    # Implement your webhook processing logic here
    puts "Processing #{event_type} event: #{data}"
  end
  ```

  ```go Go expandable theme={null}
  package main

  import (
      "crypto/hmac"
      "crypto/sha256"
      "encoding/hex"
      "encoding/json"
      "io/ioutil"
      "log"
      "net/http"
      "os"
      "strconv"
      "time"
  )

  var webhookSecret string

  func init() {
      webhookSecret = os.Getenv("WEBHOOK_SECRET")
      if webhookSecret == "" {
          log.Fatal("WEBHOOK_SECRET environment variable is required")
      }
  }

  func verifyWebhookSignature(payload []byte, signature string, secret string) bool {
      if signature == "" {
          return false
      }

      mac := hmac.New(sha256.New, []byte(secret))
      mac.Write(payload)
      expected := hex.EncodeToString(mac.Sum(nil))

      return hmac.Equal([]byte(expected), []byte(signature))
  }

  func isTimestampValid(timestamp string, maxAgeSeconds int) bool {
      if timestamp == "" {
          return false
      }

      ts, err := strconv.ParseInt(timestamp, 10, 64)
      if err != nil {
          return false
      }

      now := time.Now().UnixMilli()
      age := now - ts

      // Reject if older than 5 minutes or more than 1 minute in the future
      return age < int64(maxAgeSeconds*1000) && age > -60000
  }

  func webhookHandler(w http.ResponseWriter, r *http.Request) {
      // Read raw body
      payload, err := ioutil.ReadAll(r.Body)
      if err != nil {
          http.Error(w, "Error reading body", http.StatusBadRequest)
          return
      }

      // Get headers
      deliveryId := r.Header.Get("X-Webhook-Delivery-Id")
      timestamp := r.Header.Get("X-Webhook-Timestamp")
      event := r.Header.Get("X-Webhook-Event")
      signature := r.Header.Get("X-Webhook-Signature")

      // Validate timestamp
      if !isTimestampValid(timestamp, 300) {
          log.Printf("Invalid timestamp: %s, %s", deliveryId, timestamp)
          http.Error(w, "Invalid timestamp", http.StatusUnauthorized)
          return
      }

      // Verify signature
      if !verifyWebhookSignature(payload, signature, webhookSecret) {
          log.Printf("Invalid signature: %s, %s", deliveryId, event)
          http.Error(w, "Invalid signature", http.StatusUnauthorized)
          return
      }

      // Parse webhook
      var data map[string]interface{}
      if err := json.Unmarshal(payload, &data); err != nil {
          http.Error(w, "Invalid JSON", http.StatusBadRequest)
          return
      }

      eventType, _ := data["type"].(string)
      eventData, _ := data["data"].(map[string]interface{})
      createdAt, _ := data["createdAt"].(string)

      log.Printf("Webhook received and verified: %s, %s, %s", deliveryId, eventType, createdAt)

      // Process webhook
      if err := processWebhookAsync(eventType, eventData); err != nil {
          log.Printf("Error processing webhook: %s, %v", deliveryId, err)
          // Still return 200 to avoid retries
      }

      w.WriteHeader(http.StatusOK)
      json.NewEncoder(w).Encode(map[string]bool{"received": true})
  }

  func processWebhookAsync(eventType string, data map[string]interface{}) error {
      // Implement your webhook processing logic here
      log.Printf("Processing %s event: %v", eventType, data)
      return nil
  }

  func main() {
      http.HandleFunc("/webhooks/autosend", webhookHandler)
      log.Println("Webhook server listening on port 3000")
      log.Fatal(http.ListenAndServe(":3000", nil))
  }
  ```
</CodeGroup>

***

## Security Best Practices

<AccordionGroup>
  <Accordion title="Always Use Constant-Time Comparison">
    **Never use `===` or `==` to compare signatures.** Use constant-time comparison functions to prevent timing attacks:

    ```javascript theme={null}
    // ❌ Bad - Vulnerable to timing attacks
    if (receivedSignature === expectedSignature) {
      // Process webhook
    }

    // ✅ Good - Constant-time comparison
    try {
      if (
        crypto.timingSafeEqual(
          Buffer.from(receivedSignature),
          Buffer.from(expectedSignature)
        )
      ) {
        // Process webhook
      }
    } catch (error) {
      // Buffer lengths don't match - signature is invalid
      return false;
    }
    ```

    <Info>
      The `crypto.timingSafeEqual()` function throws an error if the buffer lengths don't match. Always wrap it in a try-catch block.
    </Info>
  </Accordion>

  <Accordion title="Store Secrets Securely">
    Never hardcode webhook secrets in your code:

    ```javascript theme={null}
    // ❌ Bad - Hardcoded secret
    const WEBHOOK_SECRET = 'a1b2c3d4e5f6g7h8...';

    // ✅ Good - Environment variable
    const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

    if (!WEBHOOK_SECRET) {
      throw new Error('WEBHOOK_SECRET environment variable is required');
    }
    ```

    **Store secrets in:**

    * Environment variables (`.env` files for local development)
    * Secure secret management services (AWS Secrets Manager, HashiCorp Vault, etc.)
    * Encrypted configuration files

    **Never:**

    * Commit secrets to version control
    * Include secrets in client-side code
    * Share secrets in logs or error messages
    * Use the same secret across multiple environments
  </Accordion>

  <Accordion title="Use the Raw Request Body">
    Compute signatures using the **raw, unparsed request body**. Do not re-stringify the parsed JSON:

    ```javascript theme={null}
    // ✅ Good - Use raw body
    app.use(
      express.json({
        verify: (req, res, buf) => {
          req.rawBody = buf.toString('utf8');
        },
      })
    );

    const signature = crypto
      .createHmac('sha256', secret)
      .update(req.rawBody) // Use raw body
      .digest('hex');

    // ❌ Bad - Don't re-stringify parsed body
    const signature = crypto
      .createHmac('sha256', secret)
      .update(JSON.stringify(req.body)) // May not match original
      .digest('hex');
    ```

    <Warning>
      JSON stringification is not deterministic. The order of object keys may differ, causing signature verification to fail.
    </Warning>
  </Accordion>

  <Accordion title="Validate the Timestamp">
    Validate the `X-Webhook-Timestamp` header to reject old or replayed requests:

    ```javascript theme={null}
    function isTimestampValid(timestamp, maxAgeSeconds = 300) {
      if (!timestamp) {
        return false;
      }

      const now = Date.now();
      const age = now - parseInt(timestamp);

      // Reject if older than 5 minutes or more than 1 minute in the future
      return age < maxAgeSeconds * 1000 && age > -60000;
    }

    app.post('/webhooks/autosend', (req, res) => {
      const timestamp = req.headers['x-webhook-timestamp'];

      if (!isTimestampValid(timestamp)) {
        return res.status(401).json({ error: 'Invalid or expired timestamp' });
      }

      if (!verifyWebhookSignature(req, webhookSecret)) {
        return res.status(401).json({ error: 'Invalid signature' });
      }

      // Process webhook
    });
    ```

    <Info>
      The timestamp is in **milliseconds** (not seconds). AutoSend sends timestamps as `Date.now().toString()`.
    </Info>
  </Accordion>

  <Accordion title="Use HTTPS for Production">
    **Always use HTTPS** for your webhook endpoints in production:

    ```javascript theme={null}
    // ❌ Bad - HTTP in production
    const webhookUrl = 'http://api.example.com/webhooks/autosend';

    // ✅ Good - HTTPS
    const webhookUrl = 'https://api.example.com/webhooks/autosend';
    ```

    HTTPS ensures:

    * Requests are encrypted in transit
    * Man-in-the-middle attacks are prevented
    * Webhook data remains confidential
    * Your webhook secret is protected

    <Note>
      AutoSend does not enforce HTTPS for webhook URLs, but it is strongly recommended for production use.
    </Note>
  </Accordion>

  <Accordion title="Respond Quickly (Under 10 Seconds)">
    Webhook requests have a **10-second timeout**. Always respond within this time:

    ```javascript theme={null}
    // ✅ Good - Queue processing and respond immediately
    app.post('/webhooks/autosend', async (req, res) => {
      // Verify signature
      if (!verifyWebhookSignature(req, webhookSecret)) {
        return res.status(401).json({ error: 'Invalid signature' });
      }

      // Queue for background processing
      await queue.add('process-webhook', req.body);

      // Respond immediately
      res.status(200).json({ received: true });
    });

    // ❌ Bad - Long processing blocks response
    app.post('/webhooks/autosend', async (req, res) => {
      // This might take too long
      await processWebhook(req.body);
      await updateDatabase(req.body);
      await sendNotification(req.body);

      res.status(200).json({ received: true });
    });
    ```

    <Warning>
      If your endpoint doesn't respond within 10 seconds, AutoSend will consider the delivery failed and retry up to 3 times.
    </Warning>
  </Accordion>

  <Accordion title="Handle Webhook Retries Idempotently">
    AutoSend retries failed deliveries up to **3 times**. Make your webhook handler idempotent:

    ```javascript theme={null}
    // ✅ Good - Idempotent processing
    app.post('/webhooks/autosend', async (req, res) => {
      const deliveryId = req.headers['x-webhook-delivery-id'];

      // Check if already processed
      const exists = await db.webhookLogs.findOne({ deliveryId });
      if (exists) {
        return res.status(200).json({ received: true, status: 'duplicate' });
      }

      // Process webhook
      await processWebhook(req.body);

      // Store delivery ID
      await db.webhookLogs.create({ deliveryId, processedAt: new Date() });

      res.status(200).json({ received: true });
    });
    ```

    <Info>
      Use the `X-Webhook-Delivery-Id` header to track which deliveries you've already processed.
    </Info>
  </Accordion>

  <Accordion title="Rotate Secrets Periodically">
    Regularly rotate your webhook secrets for enhanced security:

    <Steps>
      <Step title="Create a new webhook with the same events and URL" titleSize="p" />

      <Step title="Update your application to support both old and new secrets temporarily" titleSize="p" />

      <Step title="Verify the new webhook is working correctly" titleSize="p" />

      <Step title="Delete the old webhook" titleSize="p" />

      <Step title="Remove the old secret from your application" titleSize="p" />
    </Steps>

    ```javascript theme={null}
    // Support multiple secrets during rotation
    const WEBHOOK_SECRETS = [
      process.env.WEBHOOK_SECRET,
      process.env.WEBHOOK_SECRET_OLD, // Remove after rotation complete
    ].filter(Boolean);

    function verifyWithMultipleSecrets(req) {
      return WEBHOOK_SECRETS.some((secret) => {
        const expectedSignature = crypto
          .createHmac('sha256', secret)
          .update(req.rawBody)
          .digest('hex');

        try {
          return crypto.timingSafeEqual(
            Buffer.from(req.headers['x-webhook-signature']),
            Buffer.from(expectedSignature)
          );
        } catch {
          return false;
        }
      });
    }
    ```
  </Accordion>
</AccordionGroup>

***

## Webhook Payload Structure

AutoSend sends webhook payloads in this format:

```json theme={null}
{
  "type": "email.opened",
  "createdAt": "2025-01-08T10:30:00.000Z",
  "data": {
    "emailId": "email_abc123",
    "campaignId": "campaign_xyz789",
    "templateId": "template_def456",
    "from": "sender@example.com",
    "to": {
      "email": "recipient@example.com",
      "name": "John Doe"
    },
    "subject": "Welcome to AutoSend",
    "userAgent": "Mozilla/5.0...",
    "ipAddress": "192.168.1.1",
    "timestamp": "2025-01-08T10:30:00.000Z"
  }
}
```

<ParamField path="type" type="string" required>
  The event type (e.g., `"email.opened"`, `"contact.created"`)
</ParamField>

<ParamField path="createdAt" type="string" required>
  ISO 8601 timestamp when the event occurred
</ParamField>

<ParamField path="data" type="object" required>
  Event-specific data (varies by event type)
</ParamField>

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="Signature Verification Always Fails">
    **Symptoms**: All webhook requests return 401 Unauthorized

    **Common Causes:**

    1. **Using the wrong secret**

       ```javascript theme={null}
       // Debug: Check which secret you're using
       console.log('Secret starts with:', webhookSecret.substring(0, 10));
       console.log('Secret length:', webhookSecret.length); // Should be 64 chars
       ```

    2. **Body parsing issues**

       ```javascript theme={null}
       // Ensure you're using raw body
       console.log('Raw body:', req.rawBody);
       console.log('Raw body length:', req.rawBody.length);
       ```

    3. **String encoding issues**

       ```javascript theme={null}
       // Ensure consistent UTF-8 encoding
       const expectedSignature = crypto
         .createHmac('sha256', webhookSecret)
         .update(req.rawBody, 'utf8') // Explicit encoding
         .digest('hex');
       ```

    4. **Comparing wrong values**

       ```javascript theme={null}
       // Debug signature comparison
       console.log('Received signature:', receivedSignature);
       console.log('Expected signature:', expectedSignature);
       console.log(
         'Lengths match:',
         receivedSignature.length === expectedSignature.length
       );
       ```

    5. **Secret contains whitespace**
       ```javascript theme={null}
       // Trim whitespace from secret
       const webhookSecret = process.env.WEBHOOK_SECRET.trim();
       ```
  </Accordion>

  <Accordion title="Timestamp Validation Fails">
    **Symptoms**: Requests fail with "Invalid timestamp" error

    **Common Causes:**

    1. **Wrong time unit** - Timestamp is in milliseconds, not seconds

       ```javascript theme={null}
       // ❌ Bad - Treating as seconds
       const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);

       // ✅ Good - Milliseconds
       const age = Date.now() - parseInt(timestamp);
       ```

    2. **Clock skew** - Server time is off

       ```javascript theme={null}
       // Allow 1 minute of clock skew
       const age = Date.now() - parseInt(timestamp);
       return age < 300000 && age > -60000; // -1 min to +5 min
       ```

    3. **Timezone issues**
       ```javascript theme={null}
       // Timestamps are always UTC
       const now = Date.now(); // Always use Date.now(), not local time
       ```
  </Accordion>

  <Accordion title="Testing Signature Verification Locally">
    Test your signature verification without waiting for real webhooks:

    ```javascript theme={null}
    const crypto = require('crypto');

    function testSignatureVerification() {
      const webhookSecret = 'test-secret-12345';

      // Create a test payload (matching AutoSend's format)
      const payload = {
        type: 'email.opened',
        createdAt: new Date().toISOString(),
        data: {
          emailId: 'test-123',
          campaignId: 'campaign-456',
          timestamp: new Date().toISOString(),
        },
      };

      const payloadString = JSON.stringify(payload);

      // Generate signature
      const signature = crypto
        .createHmac('sha256', webhookSecret)
        .update(payloadString)
        .digest('hex');

      console.log('Test payload:', payloadString);
      console.log('Test signature:', signature);

      // Verify it works
      const expectedSignature = crypto
        .createHmac('sha256', webhookSecret)
        .update(payloadString)
        .digest('hex');

      console.log('Verification passes:', signature === expectedSignature);

      // Test with your actual verification function
      const isValid = crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expectedSignature)
      );

      console.log('timingSafeEqual passes:', isValid);
    }

    testSignatureVerification();
    ```
  </Accordion>

  <Accordion title="Webhooks Not Being Received">
    **Symptoms**: No webhook requests arriving at your endpoint

    **Troubleshooting Steps:**

    1. **Check webhook is active**

       * Navigate to Webhooks in AutoSend
       * Verify webhook status is "Active"
       * Check if failure count is high (auto-disabled after 5 failures)

    2. **Verify URL is accessible**

       ```bash theme={null}
       # Test your endpoint is publicly accessible
       curl -X POST https://your-domain.com/webhooks/autosend \
         -H "Content-Type: application/json" \
         -d '{"test": true}'
       ```

    3. **Check delivery logs**

       * Click on your webhook in AutoSend
       * View the "Delivery Logs" tab
       * Look for error messages or status codes

    4. **Test with resend**
       * Create a test event
       * Use the "Resend" feature to manually trigger delivery
       * Check your server logs
  </Accordion>
</AccordionGroup>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Event Types" icon="https://mintcdn.com/autosend-13920f5c/nx_wYfWx3qeZwg1C/icons/event-types.svg?fit=max&auto=format&n=nx_wYfWx3qeZwg1C&q=85&s=7e37cf26e7cf6375a72cf73483295f3f" href={APP_PATHS.webhookEventType} width="24" height="24" data-path="icons/event-types.svg">
    Learn about all available webhook events and their payloads
  </Card>

  <Card title="Retries and Replays" icon="https://mintcdn.com/autosend-13920f5c/nx_wYfWx3qeZwg1C/icons/retries-refresh.svg?fit=max&auto=format&n=nx_wYfWx3qeZwg1C&q=85&s=6c8bfc5edaa01d3624986eebbb4cbb09" href={APP_PATHS.webhookRetries} width="24" height="24" data-path="icons/retries-refresh.svg">
    Understand how AutoSend handles failed deliveries
  </Card>

  <Card title="Best Practices" icon="https://mintcdn.com/autosend-13920f5c/6j5m-Y2eck3vmZRR/icons/best-practices.svg?fit=max&auto=format&n=6j5m-Y2eck3vmZRR&q=85&s=69778574ae9457e8006f9187de6757a4" href={APP_PATHS.webhookBestPractices} width="24" height="24" data-path="icons/best-practices.svg">
    Production deployment guidelines and optimization tips
  </Card>

  <Card title="Manage Webhooks" icon="https://mintcdn.com/autosend-13920f5c/nx_wYfWx3qeZwg1C/icons/webhook.svg?fit=max&auto=format&n=nx_wYfWx3qeZwg1C&q=85&s=14ad6675c71731ac04f786559a813ee1" href={AUTOSEND_PATHS.webhooks} width="24" height="24" data-path="icons/webhook.svg">
    Configure and monitor your webhooks in AutoSend
  </Card>
</CardGroup>

***

## Related Resources

<Columns cols={2}>
  <Card title="Introduction" icon="https://mintcdn.com/autosend-13920f5c/nx_wYfWx3qeZwg1C/icons/introduction.svg?fit=max&auto=format&n=nx_wYfWx3qeZwg1C&q=85&s=9b4ef691e39d4517b800d381b32a89db" href={APP_PATHS.webhookIntroduction} width="24" height="24" data-path="icons/introduction.svg">
    Getting started with AutoSend webhooks
  </Card>

  <Card title="Delivery Logs" icon="https://mintcdn.com/autosend-13920f5c/nx_wYfWx3qeZwg1C/icons/email-activity.svg?fit=max&auto=format&n=nx_wYfWx3qeZwg1C&q=85&s=2ecad7369f217ee7d03c3d8dfdd36d22" href={APP_PATHS.webhookLogs} width="24" height="24" data-path="icons/email-activity.svg">
    View webhook delivery history and debug issues
  </Card>
</Columns>
