Overview
BounceLayer delivers events via HTTP POST requests to your designated callback URL. Every webhook transmission includes:
- A cryptographic signature header (
X-BounceLayer-Signature) computed over the raw request payload using your secret key. - A unique event identifier (
X-BounceLayer-Event-ID) to prevent replay attacks and ensure idempotent processing. - A timestamp header (
X-BounceLayer-Timestamp) indicating when the payload was dispatched.
Supported Event Types
Webhook Event Payload Example (job.completed)
{
"event": "job.completed",
"event_id": "evt_01923a1f-829d-7b20-91fc-982348a201b9",
"timestamp": 1729785600,
"data": {
"job_id": "01923a1f-829d-7b20-91fc-982348a201b9",
"status": "completed",
"total_emails": 15688,
"processed_emails": 15688,
"valid_count": 14210,
"invalid_count": 940,
"catch_all_count": 312,
"disposable_count": 226,
"credits_used": 15688,
"started_at": "2026-10-24T10:12:00Z",
"completed_at": "2026-10-24T10:14:28Z",
"download_url": "https://api.bouncelayer.com/v1/verify/bulk/01923a1f-829d-7b20-91fc-982348a201b9/download?format=csv"
}
}
Verifying HMAC-SHA256 Signatures
To ensure webhook requests originate genuinely from BounceLayer and have not been tampered with in transit, verify the cryptographic signature sent in the X-BounceLayer-Signature header.
Signature Computation
signature = HMAC-SHA256(raw_request_body, webhook_signing_secret)
Node.js / Express Example
import express from "express";
import crypto from "crypto";
const app = express();
// Use express.raw() to preserve the raw request buffer for HMAC hashing
app.post("/webhooks/bouncelayer", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-bouncelayer-signature"] as string;
const secret = process.env.BOUNCELAYER_WEBHOOK_SECRET!; // whsec_...
if (!signature) {
return res.status(401).send("Missing signature header");
}
const expected = crypto
.createHmac("sha256", secret)
.update(req.body)
.digest("hex");
// Constant-time comparison to prevent timing attacks
const isValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
if (!isValid) {
return res.status(401).send("Invalid cryptographic signature");
}
const payload = JSON.parse(req.body.toString());
console.log("Verified Webhook Event:", payload.event, payload.data.job_id);
// Return a 200 OK immediately to acknowledge receipt
res.status(200).json({ received: true });
});
Python / FastAPI Example
import hmac
import hashlib
import os
from fastapi import FastAPI, Request, HTTPException, status
app = FastAPI()
WEBHOOK_SECRET = os.getenv("BOUNCELAYER_WEBHOOK_SECRET", "").encode()
@app.post("/webhooks/bouncelayer")
async def handle_bouncelayer_webhook(request: Request):
signature = request.headers.get("X-BounceLayer-Signature")
if not signature:
raise HTTPException(status_code=401, detail="Missing X-BounceLayer-Signature")
raw_body = await request.body()
computed_signature = hmac.new(
WEBHOOK_SECRET,
raw_body,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, computed_signature):
raise HTTPException(status_code=401, detail="Invalid HMAC signature")
payload = await request.json()
event_type = payload.get("event")
job_id = payload.get("data", {}).get("job_id")
print(f"Verified event: {event_type} for job {job_id}")
return {"received": True}
Go (Golang) Example
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
)
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("X-BounceLayer-Signature")
secret := []byte(os.Getenv("BOUNCELAYER_WEBHOOK_SECRET"))
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Cannot read body", http.StatusBadRequest)
return
}
mac := hmac.New(sha256.New, secret)
mac.Write(body)
expectedSignature := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(signature), []byte(expectedSignature)) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"received":true}`))
}
Delivery Guarantee & Retry Policy
BounceLayer enforces a durable at-least-once delivery policy:
- Response Expectation: Your server must respond with an HTTP status code in the
2xx range (200-204) within 10 seconds. - Automatic Retries: If your server returns any non-2xx status code (
4xx or 5xx) or times out, our worker queues automatic retries using exponential backoff:
- Attempt 1: Immediate
- Attempt 2: 1 minute later
- Attempt 3: 5 minutes later
- Attempt 4: 30 minutes later
- Attempt 5: 2 hours later
- Deactivation: If an endpoint continuously fails across 5 attempts over 24 hours or returns
410 Gone, the webhook subscription is marked degraded in your dashboard.
Webhook Management Endpoints