Skip to content
DocumentationAPI reference Build something worth sending.

Sending email

Webhooks

Verify signatures, receive delivery events and safely handle retries and secret rotation.

Create an endpoint

Create a webhook in your project's Webhooks screen or with a management key:

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

ts
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

ts
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.

↑↓ to navigate↵ to openesc to close