Wraps Logo
Guide

Webhooks

Receive real-time SES email events at your HTTPS endpoint. Get notified about deliveries, bounces, complaints, opens, clicks, and more.

5 min read

Overview

Wraps can forward SES email events directly to your application via an HTTPS webhook. Under the hood, this creates an EventBridge API Destination in your AWS account that POSTs events to your endpoint in real time.

Events flow through the same EventBridge rule that powers the Wraps dashboard, so your webhook receives the same data you see in the UI — send, delivery, bounce, open, click, complaint, and more.

Event Flow

SES → EventBridge → API Destination → Your Endpoint

Prerequisites

  • Wraps email infrastructure deployed (wraps email init)
  • Event tracking enabled (Production preset or higher, or manually via wraps email upgrade)
  • An HTTPS endpoint ready to receive POST requests

Setup

1

Run the upgrade command

GNU Bashterminal.sh
npx @wraps.dev/cli email upgrade
2

Select "Configure webhook endpoint"

The CLI will display a list of available upgrades. Choose the webhook option.

3

Enter your HTTPS URL

Provide the full URL where you want to receive events (e.g. https://yourapp.com/webhooks/email).

4

Save the generated secret

The CLI will display a secret value for authenticating requests. Store it securely — it is only displayed once.

Event tracking enabled automatically

If event tracking isn't enabled yet, the CLI will prompt you to enable it automatically before configuring the webhook.

Webhook Payload

Every event is delivered as a JSON POST request with the following shape:

JSONpayload.json
{  "event": "Delivery",  "detail": {    "delivery": {      "timestamp": "2024-01-15T10:30:00.000Z",      "processingTimeMillis": 1234,      "recipients": ["user@example.com"],      "smtpResponse": "250 2.0.0 OK",      "reportingMTA": "a]8-31.smtp-out.amazonses.com"    },    "mail": {      "messageId": "abc-123-def",      "source": "hello@yourapp.com",      "destination": ["user@example.com"]    }  },  "timestamp": "2024-01-15T10:30:00Z",  "messageId": "abc-123-def",  "source": "wraps"}

Field Reference

FieldTypeDescription
eventstringSES event type (Send, Delivery, Bounce, Open, Click, etc.)
detailobjectFull SES event detail payload
timestampstringISO 8601 timestamp from EventBridge
messageIdstringSES message ID from mail.messageId
sourcestringAlways "wraps"

For the full payload shape of each event type, see the EventBridge Events reference.

Authenticating Requests

Every webhook request includes an X-Wraps-Signature header containing the secret value generated during setup. To validate incoming requests:

  • Read the X-Wraps-Signature header from the incoming request
  • Compare it against the secret stored in your environment variables
  • Use constant-time comparison to prevent timing attacks
TypeScriptverify.ts
import express from "express";import crypto from "crypto";const app = express();app.use(express.json());const WEBHOOK_SECRET = process.env.WRAPS_WEBHOOK_SECRET;function verifySignature(req) {  const signature = req.headers["x-wraps-signature"];  if (!signature || !WEBHOOK_SECRET) return false;  // Use constant-time comparison to prevent timing attacks  return crypto.timingSafeEqual(    Buffer.from(signature),    Buffer.from(WEBHOOK_SECRET)  );}

Example: Express.js Handler

A complete Express.js route that validates the signature and handles different event types:

TypeScriptserver.ts
app.post("/webhooks/email", (req, res) => {  if (!verifySignature(req)) {    return res.status(401).json({ error: "Invalid signature" });  }  const { event, detail, messageId } = req.body;  switch (event) {    case "Delivery":      console.log(`Email ${messageId} delivered`);      break;    case "Bounce":      const bounceType = detail.bounce?.bounceType;      console.log(`Email ${messageId} bounced: ${bounceType}`);      // Remove hard-bounced addresses from your list      if (bounceType === "Permanent") {        // markAddressAsBounced(detail.bounce.bouncedRecipients);      }      break;    case "Complaint":      console.log(`Email ${messageId} received complaint`);      // Unsubscribe the user immediately      // unsubscribeUser(detail.complaint.complainedRecipients);      break;    default:      console.log(`Received ${event} for ${messageId}`);  }  res.status(200).json({ received: true });});app.listen(3000);

Example: Next.js Route Handler

A Next.js App Router route handler with the same validation and event handling:

TypeScriptapp/api/webhooks/email/route.ts
import crypto from "crypto";import { NextResponse } from "next/server";import type { NextRequest } from "next/server";const WEBHOOK_SECRET = process.env.WRAPS_WEBHOOK_SECRET!;export async function POST(request: NextRequest) {  const signature = request.headers.get("x-wraps-signature");  if (!signature) {    return NextResponse.json(      { error: "Missing signature" },      { status: 401 }    );  }  // Constant-time comparison  const isValid = crypto.timingSafeEqual(    Buffer.from(signature),    Buffer.from(WEBHOOK_SECRET)  );  if (!isValid) {    return NextResponse.json(      { error: "Invalid signature" },      { status: 401 }    );  }  const body = await request.json();  const { event, detail, messageId } = body;  switch (event) {    case "Delivery":      console.log(`Email ${messageId} delivered`);      break;    case "Bounce":      const bounceType = detail.bounce?.bounceType;      if (bounceType === "Permanent") {        // markAddressAsBounced(detail.bounce.bouncedRecipients);      }      break;    case "Complaint":      // unsubscribeUser(detail.complaint.complainedRecipients);      break;  }  return NextResponse.json({ received: true });}

Managing Your Webhook

Run wraps email upgrade and select "Manage webhook endpoint" to access these options:

Change URL

Update the endpoint URL where events are delivered.

Regenerate Secret

Generate a new secret for authenticating requests.

Disable

Stop sending events to your endpoint. The API Destination is removed from your AWS account.

Regenerating invalidates immediately

Regenerating the secret invalidates the previous one immediately. Update your endpoint's stored secret before regenerating to avoid rejected requests.

Troubleshooting

Events not arriving
  • Verify event tracking is enabled (Production preset or higher)
  • Ensure your URL uses HTTPS and is publicly accessible
  • Check that your endpoint returns a 2xx status code — EventBridge retries on failure but will eventually stop
401/403 errors
  • Verify the X-Wraps-Signature header value matches your stored secret exactly
  • Check that you haven't regenerated the secret without updating your endpoint
  • Ensure you're reading the header name correctly (case-insensitive)
Duplicate events
  • EventBridge may retry delivery if your endpoint returns a non-2xx response
  • Make your handler idempotent by deduplicating on messageId + event
Rate limiting
The API Destination is rate-limited to 300 requests per second. If you're sending high volumes, events are queued and delivered in order.

Next Steps

EventBridge Events

Full payload reference for every SES event type including bounces, complaints, and delivery delays.

Event Reference
Configuration Presets

Learn about the Starter, Production, and Enterprise presets and how to enable event tracking.

View Presets