Overview
Let Rovela tell you what happened, instead of asking.
Sending returns an id, not an outcome. Whether a message was delivered, opened or bounced is decided later, by other machines, and a webhook is how you find out without polling.
/webhooksAPI key or sessionRegistering an endpoint
The field is named endpoint — the URL to call — and events is the list of event types you want. It must be an http or https address, and it may not point at a private or loopback host.
curl -X POST https://api.rovela.dev/webhooks \
-H "Authorization: Bearer <session token>" \
-H "Content-Type: application/json" \
-d '{
"endpoint": "https://example.com/hooks/rovela",
"events": ["email.delivered", "email.bounced", "email.complained"]
}'The response includes a signing secret. It is shown when the endpoint is created and is what you verify deliveries with — see Security.
events array subscribes to everything, including event types added after you registered. That is usually not what you want in production: a handler written for three event types will start receiving twenty-five. List the ones you handle.What we send
For each event, an HTTP POST with a JSON body and three headers:
POST <your-endpoint>
content-type: application/json
x-resend-signature: t=<unix_timestamp>,v1=<hmac_sha256_hex>
x-resend-event-type: email.delivered
x-resend-delivery-id: <uuid>
{
"id": "evt_<uuid>",
"type": "email.delivered",
"created_at": "2026-09-25T09:14:02.481+00:00",
"data": { ... }
}The event type appears twice — in the header and in the body. The id is unique per event, so a delivery you have already processed can be recognised on retry; deduplicate on it rather than on created_at, which is when the event happened rather than when it was sent to you.
x-resend-delivery-id identifies this attempt, not the event. It differs between retries of the same event, which makes it the right thing to quote in a log when you need to talk to us about a specific delivery.
What you must return
Return 2xx once you have accepted the event. The status code is a statement about whether you have taken custody of the event, not whether you have finished processing it — return quickly and do the work in your own queue.
Retries
A delivery that is not answered 2xx is retried, to a maximum of five attempts in total, with the delay doubling each time:
attempt 1 immediate
attempt 2 after 1 second
attempt 3 after 2 seconds
attempt 4 after 4 seconds
attempt 5 after 8 secondsOnly transport-level failures are retried: 5xx, 408 and 429. Every other response — including all other 4xx — marks the delivery failed and it is not tried again.
400 you return because a payload surprised you is a permanent loss: you will never be told about that event again. Validate defensively and store anything you do not yet understand rather than rejecting it. Conversely, returning 500 because your own database is momentarily unavailable spends one of four retries, which is exactly the right use for it.Ordering
Events are not guaranteed to arrive in the order they happened. Retries make this concrete rather than theoretical: a delayed email.delivered can arrive after the email.opened that followed it. Order on created_at if the sequence matters to you, and treat any single event as a fact about the past rather than as the current state.
Managing endpoints
GET /webhooks— list your endpoints.DELETE /webhooks/:id— remove one. Deliveries already queued for it are dropped.
These take a session token rather than an API key.