Webhooks Guide
Receive real-time SES email events at your HTTPS endpoint. Set up webhook delivery, authenticate requests, and handle events.
Receive real-time SES email events at your HTTPS endpoint. Get notified about deliveries, bounces, complaints, opens, clicks, and more.
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.
SES → EventBridge → API Destination → Your Endpoint
wraps email init)wraps email upgrade)npx @wraps.dev/cli email upgradeThe CLI will display a list of available upgrades. Choose the webhook option.
Provide the full URL where you want to receive events (e.g. https://yourapp.com/webhooks/email).
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.
Every event is delivered as a JSON POST request with the following shape:
{ "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 | Type | Description |
|---|---|---|
event | string | SES event type (Send, Delivery, Bounce, Open, Click, etc.) |
detail | object | Full SES event detail payload |
timestamp | string | ISO 8601 timestamp from EventBridge |
messageId | string | SES message ID from mail.messageId |
source | string | Always "wraps" |
For the full payload shape of each event type, see the EventBridge Events reference.
Every webhook request includes an X-Wraps-Signature header containing the secret value generated during setup. To validate incoming requests:
X-Wraps-Signature header from the incoming requestimport 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) );}A complete Express.js route that validates the signature and handles different event types:
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);A Next.js App Router route handler with the same validation and event handling:
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 });}Run wraps email upgrade and select "Manage webhook endpoint" to access these options:
Update the endpoint URL where events are delivered.
Generate a new secret for authenticating requests.
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.
X-Wraps-Signature header value matches your stored secret exactlymessageId + eventFull payload reference for every SES event type including bounces, complaints, and delivery delays.
Event ReferenceLearn about the Starter, Production, and Enterprise presets and how to enable event tracking.
View Presets