I just launched gradients.sh. Come check it out →
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

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

verify is required. registerRoutes() throws when it is missing, and a request that fails verification gets a 401 before the component sees it.

Verifiers

The component ships verifier helpers built on the SDK's webhook verification:

HelperChecks
resendWebhookVerifier({ secret, toleranceSeconds? })Resend (Svix) signature and timestamp
mailgunWebhookVerifier({ secret, toleranceSeconds?, signatureField? })Mailgun HMAC over timestamp and token
postmarkBasicAuthVerifier({ username, password })Basic auth credentials you set on the Postmark webhook URL
verifyByProvider({ ... })Dispatches by provider name and rejects providers without a verifier

Each helper rejects requests for a different provider. You can also pass your own function: verify receives the provider name, original Request, raw body, and normalized headers, and returns true to accept.

Unverified routes are for local development only

unsafeAllowUnverifiedWebhooks: true registers routes without verification and logs a warning. Anyone who can reach the URL can then forge delivery events. Never deploy it.

What the component stores

Every accepted delivery receives a stable provider delivery id when the provider supplies one. Replayed webhook requests with the same id return successfully without creating a duplicate event. Without a provider id, the component hashes the provider and raw body; this fallback deduplicates byte-identical retries only, not semantically equivalent JSON with changed whitespace or retry fields.

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 component reuses the SDK's webhook normalizer for Resend, Postmark, and Mailgun JSON objects. Invalid JSON, null, arrays, and primitive payloads from these providers are rejected. Other providers retain the component's generic compatibility path when they expose a provider message id and event name; test their exact payload shape before relying on normalized deliveryStatus.

Normalization support is not verification support. The SDK exports Resend and Mailgun signature verifiers. Postmark does not sign webhooks, so protect its URL with Basic auth credentials and postmarkBasicAuthVerifier. Mailgun authenticates only timestamp plus token, not the event body; use HTTPS and persistent token replay protection. See the verification reference for key rotation, timestamp tolerance, and Mailgun's body-integrity limitation.

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