Skip to content
Webhooks

Email webhooks: events, signature verification, and idempotent handlers

Subscribe to email events, verify Standard Webhooks signatures, and handle retries idempotently.

Published

Email is asynchronous. A successful send means a provider accepted the message; delivery, bounce, failure, and reply events resolve later. Webhooks push those outcomes to your application without polling.

Choose events

EventFires when
message.sentA provider accepted the email.
message.deliveredThe recipient's server confirmed delivery.
message.bouncedA message was rejected or reported as spam.
message.failedThe email could not be delivered.
message.receivedAn inbound email was routed to you.

Register an endpoint

import { createClient } from "samva";

const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
const result = await samva.webhooks.create({
  name: "Delivery events",
  url: "https://your-app.com/webhooks/samva",
  eventTypes: ["message.delivered", "message.bounced", "message.failed"],
  channels: ["email"],
});

// Store this now. Later reads do not return it.
const signingSecret = result.secret;

Custom endpoint headers cannot override webhook-id, webhook-timestamp, webhook-signature, or other reserved delivery headers. Secret-looking values are redacted in API responses and logs.

Verify the exact request

webhook-id: evt_01…
webhook-timestamp: 1786796531
webhook-signature: v1,base64-signature

Verification covers those headers and the exact raw body. The timestamp tolerance is five minutes.

import { verifyRequest, WebhookVerificationError } from "samva/webhooks";

export async function POST(request: Request) {
  try {
    const event = await verifyRequest(
      request,
      process.env.SAMVA_WEBHOOK_SECRET!,
    );
    await persistAndHandleOnce(request.headers.get("webhook-id")!, event);
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) {
      return new Response("Invalid webhook", { status: 401 });
    }
    throw error;
  }
}

Use samva/webhooks/effect when Effect owns your control flow. Both exports are runtime-neutral; there is no /node adapter.

Design for delivery

Delivery is at least once and unordered. A retry retains its stable webhook-id, but gets a fresh timestamp and signature. Persist the id under a unique constraint before applying side effects.

Samva gives each POST 30 seconds. Non-2xx responses and timeouts retry after approximately 5s, 5m, 30m, 2h, 5h, 10h, and 10h, with jitter. Retry-After can delay the next attempt. Return 2xx after durably accepting the event.

Test deliveries are queued through the normal lifecycle. Manual redelivery is a one-shot attempt, not a new retry series. Logs are retained for 90 days. Secret rotation provides 24 hours of overlap, and an endpoint is disabled after five continuous days without a successful delivery.

For exact payloads, see the webhook event catalog.

Frequently Asked Questions

Related Resources

Get started

Ship your first email today.

Transactional and product email through one typed API. Signed events, conversation threading, deliverability handled.