Webhook Security

Verify webhook authenticity and implement security best practices

Webhook security is critical to ensure that requests to your endpoint are actually coming from Sent and haven't been tampered with. This page covers how to verify webhook authenticity and implement security best practices.

Why Webhook Security Matters

Without proper verification, attackers could:

  • Send fake webhook requests to trigger unwanted actions
  • Modify payload data to manipulate your application
  • Overload your webhook endpoint with requests

How Sent Signs Webhooks

Sent signs every webhook request using HMAC-SHA256 encryption with your account's secret key. This creates a unique signature for each request that proves:

  1. The request came from Sent
  2. The payload hasn't been modified

Finding Your Secret Key

Each webhook endpoint has a unique secret that's used to sign all requests to that endpoint.

To access your webhook secret, you can click on your desired webhook endpoint in your Sent Dashboard and click on the Eye Icon button to view the secret, or you can click on the Copy button to copy the secret to your clipboard.

Keep Your Secret Secure

Keep your webhook secret secure and never expose it publicly. If you believe your secret has been compromised, regenerate it immediately from your dashboard settings.

Signature Verification

How Signatures Work

Each webhook request includes several headers for verification:

HeaderDescriptionExample
x-webhook-signatureHMAC-SHA256 signaturev1,abc123... (base64)
x-webhook-idUnique webhook endpoint ID550e8400-e29b-41d4-a716-446655440000
x-webhook-timestampUnix timestamp (seconds)1705334531
x-webhook-event-typeEvent typemessages or templates

The signature format is:

x-webhook-signature: v1,{base64_encoded_signature}

The signature is computed as follows:

  1. Strip the whsec_ prefix from your signing secret
  2. Base64-decode the remaining secret to get raw key bytes
  3. Concatenate: {webhookId}.{timestamp}.{payload} (dot-separated)
  4. Compute HMAC-SHA256 using the raw key bytes
  5. Base64-encode the result
  6. Prefix with v1,

Step-by-Step Verification

Extract headers - Get x-webhook-signature, x-webhook-id, and x-webhook-timestamp

Prepare your secret - Strip the whsec_ prefix and Base64-decode to get raw bytes

Build signed content - Concatenate: {webhookId}.{timestamp}.{rawBody}

Compute HMAC-SHA256 using the decoded secret key bytes

Compare signatures using a timing-safe comparison function

Only process the webhook if signatures match

Replay Attack Prevention

You should also verify that the timestamp is recent (within 5 minutes) to prevent replay attacks.

Security Best Practices

Always Validate Signatures

Never trust a webhook request without signature verification:

// ❌ BAD - No verification
app.post('/webhook', (req, res) => {
  processWebhook(req.body); // Dangerous!
  res.status(200).send('OK');
});

// ✅ GOOD - Verified first
app.post('/webhook', (req, res) => {
  if (!verifySignature(req)) {
    return res.status(401).send('Unauthorized');
  }
  processWebhook(req.body);
  res.status(200).send('OK');
});

Use Timing-Safe Comparisons

Standard string comparison can leak timing information. Use dedicated functions:

  • Node.js: crypto.timingSafeEqual()
  • Python: hmac.compare_digest()
  • Go: subtle.ConstantTimeCompare()
  • Other languages: Look for "constant-time" or "timing-safe" comparison functions

Why Timing-Safe Comparisons?

Regular string comparison (==) can exit early when it finds a mismatch, potentially leaking information about the correct signature through timing analysis. Timing-safe functions always compare the entire string, preventing this attack vector.

Require HTTPS

Always use HTTPS endpoints for webhooks:

{
  "url": "https://your-app.com/webhook" // ✅ Secure
}
{
  "url": "http://your-app.com/webhook" // ❌ Insecure
}

HTTPS Required

Sent will only deliver webhooks to HTTPS endpoints. HTTP endpoints will be rejected during webhook configuration.


On this page