← Back to blog

Email API Integration (2026): Adding Rovela to Your Application

Architecture for a Rovela email API integration: server-only sending_access, a single send wrapper, Idempotency-Key design, retries, and a verified webhook handler.

Updated November 10, 2026
Email API Integration (2026): Adding Rovela to Your Application

Integrating an email API into your application

An email API integration is the work of making Rovela a boring dependency: one module your handlers call, secrets that never leave the server, retries that cannot double-send, and a webhook endpoint that tells you what happened after the queue. It is not “how to send in Node” (that is How to Send Email in Node.js) and not “how to send from a signup hook” (that is Send Transactional Email from Your App). This is the architecture those posts sit on.

Start from the Email API product page, the Quick Start, the Send Email API, and Authentication. Also Webhooks overview and Webhook security.

Related product guides in this series:

The pieces, and who owns them

PieceOwner
Verified sending domain, DNSYou + Rovela dashboard / Domains guide
ROVELA_API_KEY (sending_access), MAIL_FROMServer env / secret store
sendTransactional(…) wrapperYour backend
Idempotency-Key per logical actionThe caller of the wrapper
Queue / HTTP retriesYour platform
Delivery, DKIM, bouncesRovela
Webhook signature check + event handlerA server route with the signing secret (and full_access only if you read messages)

Rovela does not host your application, workers, or cron. You call POST /emails; it queues and delivers.

1. Domain and keys before code

Verify the domain you will send from. Create a sending_access key for the app and store it only in the environment that runs the wrapper. Optionally restrict the key to that one domain (Authentication, Verified Domain Email API). Create a second, tighter process (or use the dashboard) with full_access when you need to register webhooks or inspect a message. Rotate by create-then-revoke. Never put a key in a SPA, mobile app, or public repo.

2. A single wrapper

All product code should go through one function so headers and error handling stay consistent:

const res = await fetch("https://api.rovela.dev/emails", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.ROVELA_API_KEY,
    "Content-Type": "application/json",
    "Idempotency-Key": `signup-verify:${userId}:${codeId}`,
  },
  body: JSON.stringify({
    from: process.env.MAIL_FROM, // e.g. "Acme <no-reply@yourdomain.com>"
    to: user.email,
    subject: `Your Acme code: ${code}`,
    html: `<p>Your verification code is <b>${code}</b>. It expires in 10 minutes.</p>`,
    text: `Your verification code is ${code}. It expires in 10 minutes.`,
    tags: { kind: "signup-otp", userId },
  }),
});
if (!res.ok) {
  const err = await res.json();
  throw new Error(`Rovela ${res.status}: ${err.message}`);
}
const { id } = await res.json(); // accepted and queued — not yet delivered

A Python variant with the same headers is equally valid (urllib / httpx). The wrapper should:

  • Read ROVELA_API_KEY and MAIL_FROM from the environment.
  • Require the caller to pass an idempotencyKey (fail loudly if it is missing on user-triggered mail).
  • Send both html and text (or a template id + variables).
  • Throw on non-2xx and include the API message (the documented error shape is { statusCode, name, message }).
  • Treat 200 as “queued,” returning the id for logs — not as “delivered.”

See Idempotency Keys for Email APIs for key design (signup-otp:${userId}:${codeId}, not Date.now()).

3. Retries and rate limits

If your HTTP client or queue retries, the same Idempotency-Key must come with the retry. On 429, back off; honour Retry-After when present. Each message in a batch counts toward the rate limit individually (Send Email API). Do not catch a timeout, generate a new key, and send again.

4. The webhook side of the integration

Register an HTTPS endpoint (not a private or loopback host) with the events you handle — typically email.bounced, email.complained, and maybe email.failed. An empty events array subscribes to everything, including types added later, which is usually wrong in production (Overview).

Verify x-resend-signature as documented (t=<unix>,v1=<hex> HMAC-SHA256 over "<timestamp>.<raw body>"). Deduplicate on event id. Return 2xx once you have taken custody; only 5xx, 408, and 429 are retried, up to five attempts. Details: Verifying Webhook Signatures, Transactional Email Webhooks.

5. Config and environments

Use a verified domain and real keys in every environment; Rovela has no separate sandbox mode or test key that behaves differently (Quick Start). In development, send to a catch-all you control, or rely on idempotency keys so repeated local runs do not flood a real user. Keep staging keys and domains separate from production so a load test cannot mail customers.

FAQ

Should the wrapper live in a separate microservice? Only if many apps must share it. Inside one product, a module is enough and easier to keep sending_access scoped.

Where do templates belong? Stored templates are managed with full_access or the dashboard and sent by id from the wrapper (Email Templates API Guide). Batch cannot take template — render first, or send templated mail one-by-one.

Can an edge function hold the key? Yes, if it is a server runtime with secrets, not a browser bundle. Cloudflare Workers and similar already have a dedicated sending post on the blog; the key rule does not change.

What is out of scope for this integration? Mailbox provisioning, IMAP, and webmail (Transactional Email API + Hosted Mailboxes), and inbound parsing (Inbound and Outbound Email API) — add them as separate modules when the product needs them.

Make sending a dependency, not a side quest

One wrapper, a send-only key, mandatory idempotency keys, and a verified webhook handler. That is an email API integration you can operate — then every product event in Send Transactional Email from Your App is a one-line call.