Event AutomationPOST/api/v1/webhooks

Webhooks & Cryptographic Signatures

Receive real-time asynchronous notifications whenever bulk verification batches finish, with cryptographic HMAC-SHA256 signature verification and exponential backoff retry policies.

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

Event NameTrigger ConditionDescription
`job.completed`Bulk cleaning finishedDispatched when all emails in a bulk verification batch have finished processing. Contains the complete tally of valid, invalid, catch-all, and disposable addresses, plus a direct download URL.
`job.failed`Unrecoverable bulk failureDispatched if a corrupted CSV or server error halted processing. Any unused reserved credits are automatically refunded to your balance.
`single.verified`Single verification eventOptional event triggered whenever a single verification completes via SDK or API.

Webhook Event Payload Example (job.completed)

json
{
  "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

text
signature = HMAC-SHA256(raw_request_body, webhook_signing_secret)

Node.js / Express Example

typescript
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

python
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

go
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:

  1. Response Expectation: Your server must respond with an HTTP status code in the 2xx range (200-204) within 10 seconds.
  2. 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
  1. 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

MethodEndpointDescription
`POST`/api/v1/webhooksRegister a new webhook endpoint with callback URL and event subscriptions.
`GET`/api/v1/webhooksList all active webhook subscriptions and their delivery success rate.
`POST`/api/v1/webhooks/{id}/testTrigger an instant test payload (test.ping) with live HMAC signatures to verify connectivity.
`DELETE`/api/v1/webhooks/{id}Delete a webhook subscription.