← Back to blog

Inbound and Outbound Email API (2026): Send and Receive with Rovela

Use one Rovela account to send via POST /emails and receive via inbound MX or ingress, email.received webhooks, and GET /inbound/:id.

Updated November 10, 2026
Inbound and Outbound Email API (2026): Send and Receive with Rovela

Inbound and outbound email in one API

An inbound + outbound email API is a single product that can send transactional mail and receive messages at your domain, then hand those arrivals to your app. With Rovela, outbound is POST /emails (and batch). Inbound is “turn inbound on for a verified domain, publish MX (or forward to an ingress URL), subscribe to email.received, then GET /inbound/:id for the body.”

This is the combined product picture. The step-by-step inbound implementation, including a full handler, already lives in Inbound Email API Webhooks and the receiving email example. Delivery events for outbound mail are Transactional Email Webhooks. Human inboxes are Transactional Email API + Hosted Mailboxes.

Start from the Email API product page, the Quick Start, the Send Email API, and Authentication. Also Webhooks and the event catalog.

Related product guides in this series:

Outbound: what your app sends

Outbound is the path in Transactional Email API and HTTP Email API: a sending_access key, a verified from, HTML + text (or a template), and an Idempotency-Key for anything retried. A 200 queues the message. Outcomes arrive as email.sent, email.delivered, email.bounced, and related events — or via GET /emails/:id with full_access.

Set reply_to (or send from an address people can reply to) when you want the conversation to come back into the same domain you receive on.

Inbound: what your domain accepts

From the inbound example (this needs a full_access key and a domain you have already verified):

curl -X POST https://api.rovela.dev/domains/<domain_id>/inbound   -H "Authorization: Bearer re_your_full_access_key"   -H "Content-Type: application/json"   -d '{"catch_all": false}'

The response includes enabled, catch_all, an ingress_url (treat it like a secret; calling the endpoint again issues a new one), and the mx record to publish on the domain. Until MX resolves, senders keep delivering wherever the old MX pointed.

  • If you omit catch_all or set it to the documented default of true, anything at the domain is accepted.
  • With catch_all: false, only addresses you list are accepted (keeps random local-part spam out of the app). Add them with POST /domains/<domain_id>/inbound/recipients. Mail to an unlisted address is refused at the door. The address must belong to that domain or you get 400.

Subscribe to email.received:

curl -X POST https://api.rovela.dev/webhooks   -H "Authorization: Bearer re_your_full_access_key"   -H "Content-Type: application/json"   -d '{"endpoint": "https://app.acme.com/webhooks/rovela", "events": ["email.received"]}'

The event carries envelope and subject, not the body: inbound_message_id, to, from, subject. Fetch GET /inbound/:id with full_access for text and html (either may be null). Inbound HTML was written by a stranger — do not insert it into your pages unsanitized; prefer text for parsing. The from address is whatever the sender claimed, not proof of identity.

List arrivals (newest first, up to 100 per page) with GET /inbound?limit=50&offset=0. If you already run a mail server, you can skip the MX change and POST the raw message to ingress_url (over 2 MB is 413), with X-Envelope-To required and a stable X-Provider-Event-Id so retries do not fire email.received twice.

Signature verification and retry rules are the same as other webhooks (Verifying Webhook Signatures, Webhook security).

Putting both sides in one app

A support-style loop looks like this:

  1. Your app sends “we received your order” with POST /emails and reply_to: support@mail.acme.com (or a reply+ticket-381@ address).
  2. The customer replies. Inbound accepts the message (recipient list or catch-all), stores it, and fires email.received.
  3. Your handler verifies the signature, deduplicates on event id, loads GET /inbound/:id, and appends the body to the ticket.
  4. Your agent (or your code) sends the next outbound message with the same API, using a new idempotency key.

Do not use the sending key to fetch inbound bodies — reading inbound needs full_access. Keep that key only on the webhook worker.

The event catalog describes email.received as an inbound message accepted for a hosted mailbox; the inbound example also shows domain-level inbound with recipient rules. Follow those docs for the setup you are using, and use webmail when a human should just read the mailbox (Transactional Email API + Hosted Mailboxes).

FAQ

Is this a replacement for the inbound how-to? No. Use Inbound Email API Webhooks when you are writing the handler. This post is why you might want send + receive in one vendor.

Can I use inbound without webmail? Yes — enable inbound on the domain and consume webhooks. Webmail is optional for humans.

Do opened/clicked events apply to inbound? Engagement events are about outbound messages, and only when tracking is enabled on the sending domain (Events).

What belongs on sending_access vs full_access? Outbound POST /emails and batch: sending_access. Enabling inbound, registering webhooks, and GET /inbound/:id: full_access.

One account for the whole thread

Send with the Email API, receive with inbound webhooks, and optionally give people webmail on the same domain. That is an inbound + outbound email API: one vendor, two directions, still just HTTP.