Webhooks

Handle email delivery webhooks

Receive delivery events from Rovela in a Python application. Covers HMAC signature verification, acknowledging fast so the server does not retry, and processing events reliably in the background.

When a message is delivered, bounced, or marked as spam, Rovela sends a POST to a webhook URL you configure. The webhook carries the event type in a header and the event details in the body. This guide shows how to verify that the request really came from Rovela, acknowledge it fast enough to avoid retries, and process it without losing events.

Why signature verification matters

Anyone can send a POST to your webhook URL. Without verification, a forged request could mark a message as delivered or trigger a downstream action on fake data. Rovela signs every webhook with HMAC-SHA256. The signature is in the x-resend-signature header as t=<timestamp>,v1=<hex_digest>, where the digest is computed over the string formed by joining the timestamp, a period, and the raw request body. Verify the signature before you trust the contents.

Verifying the signature

Read the raw request body before any JSON parsing. Parse the x-resend-signature header — it has the form t=<timestamp>,v1=<hex_digest> — extract the timestamp and the digest, then compute HMAC-SHA256 over the string "<timestamp>.<raw_body>" using your webhook secret. Use hmac.compare_digest rather than == so a timing attack cannot leak the secret one byte at a time. Reject requests whose timestamp is more than five minutes old to block replay attacks.

import hmac
import hashlib
import time

def verify_signature(raw_body: bytes, sig_header: str, secret: bytes) -> bool:
    # x-resend-signature format: t=<unix_timestamp>,v1=<hmac_sha256_hex>
    parts = dict(p.split("=", 1) for p in sig_header.split(",") if "=" in p)
    ts, v1 = parts.get("t", ""), parts.get("v1", "")
    if not ts or not v1:
        return False
    # Reject timestamps older than 5 minutes to block replay attacks
    if abs(time.time() - int(ts)) > 300:
        return False
    payload = ts.encode() + b"." + raw_body
    expected = hmac.new(secret, payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

Acknowledge fast, process later

Rovela retries a webhook that does not return a 2xx status within 10 seconds. If your handler does slow work — writing to a database, calling another API — do it after acknowledging, from a queue or a background task. Return 2xx first, then process. A webhook that times out is retried, and the retry can produce a duplicate event, so make your processing idempotent too.

# The server retries a webhook that does not answer 2xx within 10s.
# Acknowledge fast, do the work after, from a queue or a thread.
@app.post("/webhooks/rovela")
def rovela_webhook(req: Request):
    raw = asyncio.run(req.body())  # raw bytes, not parsed JSON
    sig = req.headers.get("x-resend-signature", "")
    if not verify_signature(raw, sig, SECRET):
        raise HTTPException(401)
    enqueue(req.headers["x-resend-event-type"], raw)  # handle later
    return {"ok": True}  # 2xx — acknowledged

Handling event types

The x-resend-event-type header names the event: email.delivered, email.bounced, email.complained, email.opened, email.clicked, and others. Branch on it in your handler. The body's email_id matches the id returned by the send API, so you can correlate the event with the message you sent.

Common questions

What happens if my server is down when Rovela sends a webhook?
Rovela retries up to 5 times with exponential backoff. Recover your endpoint within that window and the event will be delivered. If all attempts fail, the delivery is recorded as failed and you can still see it in the dashboard's event log.
Should I verify the signature on every event type?
Yes. Verification is about authenticity, not content. A bounced event and an opened event carry different data, but the signature check is the same and should run first for both.
Can I use a framework other than FastAPI?
Yes. The signature verification is plain Python and works in Flask, Django, Starlette, or any framework. Only the way you read the raw body and return a response differs.
How do I test webhooks locally?
Expose your local server to the internet with a tunnel tool such as ngrok, then register that URL as a webhook endpoint. Any email you send or event that fires will reach your local handler in real time.

Ready to send your first email?

Rovela gives you a plain HTTP API, business mailboxes on your own domain, idempotency keys, and delivery webhooks — all on a free plan with no card required.