Create an endpoint
Create a webhook in your project's Webhooks screen or with a management key:
const webhook = await panda.webhooks.create({
url: "https://yourapp.com/webhooks/sendpanda",
events: ["email.delivered", "email.bounced"],
});
Store the signing secret when it is shown. Choose only the event types your application needs. Subscribe to email.received for incoming mail.
Verify the raw body
Webhook headers are webhook-id, webhook-timestamp (Unix seconds), and webhook-signature containing space-separated v1=HEX signatures. Verify against the raw body before JSON parsing and use constant-time comparison:
import { verifyWebhook } from "@sendpanda/webhooks";
const event = verifyWebhook(
rawRequestBody, // exact string or bytes, before JSON parsing
requestHeaders, // Headers or a header record
process.env.SENDPANDA_WEBHOOK_SECRET!,
);
// Persist and deduplicate event.id before processing application side effects.
The helper checks the delivery timestamp (300-second tolerance by default), compares signatures in constant time, accepts rotation signatures and validates the event envelope. Invalid requests throw WebhookVerificationError. For Next.js, use the Node.js runtime and await request.text(); for Express, mount express.raw() on the webhook route before express.json(). See the package README for complete examples.
Deduplicate by event ID in a transaction. Respond 2xx after durable receipt; dispatch work asynchronously. Deliveries retry up to 8 attempts, with exponential jitter. Manual replay preserves event ID and refreshes timestamp/signature. During rotation both old and new secrets sign deliveries for 24 hours. Never log the signing secret. Endpoints require public HTTPS/443, public DNS for every resolved address and no redirects; the sender pins DNS per attempt to resist rebinding.
Next.js route handler
import { verifyWebhook } from "@sendpanda/webhooks";
export const runtime = "nodejs";
export async function POST(request: Request) {
let event;
try {
event = verifyWebhook(
await request.text(),
request.headers,
process.env.SENDPANDA_WEBHOOK_SECRET!,
);
} catch {
return new Response("Invalid signature", { status: 400 });
}
// Durably store and deduplicate event.id before returning 2xx.
// Return a non-2xx response if that storage fails so delivery can retry.
await storeEventOnce(event);
return new Response(null, { status: 204 });
}
storeEventOnce represents your own durable storage implementation, with a unique constraint on event ID. Process side effects asynchronously after receipt.
Replay and private mailbox access
Inspect deliveries in Webhooks and replay a failed delivery after fixing your endpoint. A replay keeps the original event ID, so it must not trigger duplicate effects.
Mailbox events require an endpoint owned by a member who is still assigned to that mailbox. Removing their mailbox or project access prevents private payload delivery, including deliveries already queued. Legacy ownerless endpoints receive general transactional events; recreate them with a member-owned credential to receive private mailbox events.