Transactional Email Webhooks (2026): Delivery Events for Your Rovela Sends
Subscribe to Rovela transactional delivery events (delivered, bounced, complained), verify x-resend-signature, and handle retries — without polling GET /emails/:id.
Transactional email webhooks: delivery events for your app
A 200 from POST /emails means Rovela queued the message. Whether it was delivered, delayed, bounced, or marked as spam is decided later, by other machines. Transactional email webhooks are how your app finds out without polling.
This post is the delivery-event side of the product: which events to subscribe to, the envelope, signatures, and retries. It is not Webhook-Driven Email Automation (using webhooks as an automation backbone), not Verifying Webhook Signatures (signature verification alone), and not Inbound Email API Webhooks (inbound email.received). Webhooks product page, overview, events, and security are the canonical docs.
Start from the Email API product page, the Quick Start, the Send Email API, and Authentication.
Related product guides in this series:
- Transactional Email API
- HTTP Email API
- Migrate SMTP to an Email API
- Email API for SaaS Apps
- Send Email with curl
- Transactional Email API + Hosted Mailboxes
- Inbound and Outbound Email API
- Email Sending API
- Email Notification API
- Email API Integration
- Batch Transactional Email API
- A Simple Transactional Email API
- Send Transactional Email from Your App
- Verified Domain Email API
Register only the events you handle
curl -X POST https://api.rovela.dev/webhooks -H "Authorization: Bearer re_your_full_access_key" -H "Content-Type: application/json" -d '{
"endpoint": "https://example.com/hooks/rovela",
"events": ["email.delivered", "email.bounced", "email.complained"]
}'
The endpoint must be http or https and may not point at a private or loopback host. The response includes a signing secret, shown at creation — store it like a key. Registering a typo in the event name is rejected rather than accepted and never fired.
An empty events array subscribes to everything, including types added later. That is usually wrong in production.
Delivery events that matter for transactional mail
From the event catalog:
| Event | Meaning |
|---|---|
email.sent | Accepted by the SMTP relay; not yet delivered |
email.delivered | The receiving server accepted it for the recipient |
email.delivery_delayed | Still being retried — not a final outcome |
email.bounced | Permanent failure (address does not exist or refuses mail) |
email.failed | Failed for a reason that is not a recipient bounce |
email.suppressed | Blocked before sending by the suppression list |
email.scheduled | Queued for a future scheduled_at |
email.complained | The recipient marked the message as spam |
The docs recommend that email.bounced, email.complained, and contact.unsubscribed are the three that should change something on your side. email.delivered is commonly subscribed and is the least actionable — it confirms what usually happens anyway. email.opened and email.clicked fire only when tracking is enabled on the sending domain; a domain with tracking off looks the same as a domain nobody engages with.
Inbound arrivals are email.received — see Inbound and Outbound Email API. Broadcast, contact, suppression, domain, automation, and mailbox.quota_alert events exist in the catalog; only subscribe if you handle them.
The envelope and the headers
Every event uses:
{
"id": "evt_<uuid>",
"type": "email.delivered",
"created_at": "2026-09-25T09:14:02.481+00:00",
"data": { }
}
Delivery data includes email_id, from, and to. Treat the exact field set of data as additive — parse what you need and ignore the rest, or a handler that rejects unknown fields will drop events permanently.
HTTP headers on the delivery (documented names):
x-resend-signature:t=<unix_timestamp>,v1=<hmac_sha256_hex>x-resend-event-typex-resend-delivery-id(this attempt, not the event; it changes on retry)
The signature is HMAC-SHA256 over "<timestamp>.<raw body>", hex-encoded, keyed with the endpoint secret (Security, Verifying Webhook Signatures). Deduplicate on event id (retries reuse it and the same created_at). Quote x-resend-delivery-id in logs when you need to talk about a specific attempt.
What you must return
Return 2xx once you have taken custody of the event — not once you have finished all downstream work. Do the work in your own queue. Retries: up to five attempts total, delays 0 / 1 / 2 / 4 / 8 seconds. Only 5xx, 408, and 429 are retried. Other 4xx, including “invalid signature → 400,” mark the delivery failed and it is not tried again.
Polling GET /emails/:id or GET /emails/:id/events with full_access is useful for debugging one message. For anything systematic, use a webhook (Quick Start). Bounce handling recipes: Handling bounces, Email Bounce Handling.
FAQ
How fast is email.delivered? The Email API product page says a webhook is typically pushed within about a minute of the receiving server’s decision (Email API).
Can a sending_access key register webhooks? Registering webhooks and reading messages needs full_access or a dashboard session (Authentication).
Should password-reset mail subscribe to opens? Usually no. Act on bounces and complaints; do not make a reset flow depend on a tracking pixel.
Is email.suppressed a bounce? No. The address was withheld before send. Still stop retrying that recipient in your app if the suppression is permanent.
Subscribe to outcomes, not to hope
Queue the message with POST /emails, then let signed webhooks tell you if it landed or bounced. That is the transactional email webhook loop: small event list, verified signatures, idempotent handlers.