Email SDK
IntegrationsConvex

Process webhooks

Verify provider webhook requests in application code and store normalized delivery events in the Convex component.

Provider webhooks are public HTTP endpoints. The application owns the URL and signature check; the component owns deduplication, message correlation, event history, and normalized delivery state.

Register a route

convex/http.ts
import { httpRouter } from "convex/server";
import { email } from "./email";
import { verifyResendWebhook } from "./webhookVerification";

const http = httpRouter();

email.registerRoutes(http, {
  pathPrefix: "/email",
  providers: ["resend"],
  verify: async ({ body, headers }) =>
    verifyResendWebhook({
      body,
      headers,
      secret: process.env.RESEND_WEBHOOK_SECRET!,
    }),
});

export default http;

This registers POST /email/webhooks/resend on your deployment's .convex.site domain.

Never omit verification in production

verify receives the provider name, original Request, raw body, and normalized headers. Verify the raw request with the provider's official algorithm before the component sees it. A shared header comparison is acceptable for a private development tunnel, not for an internet-facing production endpoint.

What the component stores

Every accepted delivery receives a stable provider delivery id. Replayed webhook requests return successfully without creating a duplicate event.

When providerMessageId matches a stored send, the component appends a webhook event and updates normalized state:

Provider eventStored result
DelivereddeliveryStatus: "delivered" and deliveredAt
Permanent bounce or rejectiondeliveryStatus: "bounced"
Spam complaintdeliveryStatus: "complained"
Temporary failureEvent history only
Open, click, or unknown eventEvent history only

Bounces and complaints remain sticky against a late delivered event. When both a bounce and complaint arrive, the latest terminal webhook wins.

Supported payload shapes

The current normalizer understands Resend, Postmark, and Mailgun-style payloads. Other providers are stored through the generic event path when they expose a provider message id and event name, but you should test their exact payload shape before relying on normalized deliveryStatus.

Test locally

  1. Send through a provider test address and capture the stored providerMessageId.
  2. POST a signed test webhook to the route.
  3. Query email.status and email.listEvents.
  4. Replay the same delivery id and confirm no second event appears.

Send and track email

Read queue state and the webhook-backed delivery result.

On this page