Webhook Security
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 app
- Overload your webhook endpoint with requests
How Sent Signs Webhooks
Sent signs every webhook request with an HMAC-SHA256 signature computed from the webhook's signing secret. This creates a unique signature for each request that proves:
- The request came from Sent
- 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 your desired webhook endpoint in your Sent Dashboard and click the Eye Icon button to view the secret, or you can click 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:
| Header | Description | Example |
|---|---|---|
x-webhook-signature | HMAC-SHA256 signature | v1,abc123... (base64) |
x-webhook-id | Unique webhook endpoint ID | 550e8400-e29b-41d4-a716-446655440000 |
x-webhook-timestamp | Unix timestamp (seconds) | 1705334531 |
x-webhook-event-type | Fully qualified event type | message.delivered, message.received, or templates |
The signature format is:
x-webhook-signature: v1,{base64_encoded_signature}The signature is computed as follows:
- Strip the
whsec_prefix from your signing secret - Base64-decode the remaining secret to get raw key bytes
- Concatenate:
{webhookId}.{timestamp}.{payload}(dot-separated) - Compute HMAC-SHA256 using the raw key bytes
- Base64-encode the result
- 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');
});The example calls a verifySignature helper that implements the six steps in Step-by-Step Verification. Copy a complete implementation for your language from the verifier in seven languages, and always run it against the raw request body. A body parsed and re-serialized by your framework produces a different signature.
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.
Prefer HTTPS
Sent accepts both http:// and https:// endpoints at registration time, but you should always use HTTPS in production:
{
"url": "https://your-app.com/webhook" // ✅ Recommended
}{
"url": "http://your-app.com/webhook" // ⚠️ Not recommended — payload travels in clear text
}Use HTTPS in production
While the platform allows http URLs (useful for some tunnels and internal testing), all production traffic should be HTTPS so the signed payload cannot be intercepted or modified in transit.
Receiving Inbound Messages via the message.received Webhook
How to receive contact replies in real time with the message.received webhook, acknowledge and deduplicate deliveries, and skip compliance keywords.
Handling Retries
Make your Sent webhook handler idempotent: choose a payload-based dedupe key, handle duplicate and out-of-order events, and understand why retries happen.