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
import { httpRouter } from "convex/server";
import { resendWebhookVerifier, verifyByProvider } from "@opencoredev/convex-email";
import { email } from "./email";
const http = httpRouter();
email.registerRoutes(http, {
pathPrefix: "/email",
providers: ["resend"],
verify: verifyByProvider({
resend: resendWebhookVerifier({ secret: process.env.RESEND_WEBHOOK_SECRET! }),
}),
});
export default http;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:
| Helper | Checks |
|---|---|
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 event | Stored result |
|---|---|
| Delivered | deliveryStatus: "delivered" and deliveredAt |
| Permanent bounce or rejection | deliveryStatus: "bounced" |
| Spam complaint | deliveryStatus: "complained" |
| Temporary failure | Event history only |
| Open, click, or unknown event | Event 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
- Send through a provider test address and capture the stored
providerMessageId. - POST a signed test webhook to the route.
- Query
email.statusandemail.listEvents. - Replay the same delivery id and confirm no second event appears.
Send and track email
Read queue state and the webhook-backed delivery result.