Programmable email vs. email API: what's the difference?
An email API sends mail from your code. Programmable email adds the return trip — inbound delivered as JSON, an inbox your app or agent owns, and replies threaded back out, all on one domain you control. A fair comparison with the round-trip code that a send-only API can't give you.
Search “email API” and every result sells you the same verb: send. Send transactional mail, send receipts, send password resets. That’s genuinely useful, and platforms like Resend, Postmark, and SendGrid are very good at it. But sending is half a conversation. The moment a customer hits reply, or an agent needs to read a verification code, or your app wants a thread instead of a one-way blast, a send-only API goes quiet.
An email API talks. Programmable email listens too. It does everything a send API does, then adds the return trip: inbound mail delivered to your code as structured JSON, an inbox your app or agent actually owns, and replies that thread back out — all on one domain you control, with no mail server to run.
Here’s the shape of the difference before the details:
The difference, line by line
| Email API (send-only) | Programmable email | |
|---|---|---|
| Send from code | ✓ | ✓ |
| Receive inbound as JSON | ✗ — a separate product, or not at all | ✓ built in |
| Reply in-thread | Hand-set the In-Reply-To/References headers yourself | ✓ same client, same call |
| Inbox per app or agent | ✗ — you don’t own an address, you own an API key | ✓ unlimited, free |
| Both directions on one domain | Usually two setups (sending + a parse add-on) | ✓ one verified domain |
| Mail server / IMAP to run | Often needed for the inbound side | ✗ none |
The through-line: an email API gives you an outbound pipe. Programmable email gives you an address — something that can receive, hold a thread, and belong to a particular app or agent — with the outbound pipe included.
Credit where it’s due
This isn’t a knock on send APIs. Resend, Postmark, Mailgun, and SendGrid are excellent at outbound, with mature deliverability tooling we’re not going to pretend to match on day one. If your app only ever emits mail — receipts, alerts, resets — a send API is the right, simple call, and you don’t need anything more.
Where it gets awkward is inbound. Most send-first platforms bolt receiving on as a second product: SendGrid Inbound Parse, Mailgun Routes, Brevo Inbound Parse. They work, but they’re a separate setup with thinner docs, they hand you multipart/form-data to decode rather than clean JSON, and reply-threading plus a per-app inbox identity are rarely first-class. You end up gluing two half-products into the round trip you actually wanted.
There’s a naming tell here, too. Twilio coined “Programmable Voice” and “Programmable Messaging” to describe exactly this — communication you drive from code in both directions — but for email it still ships a plain “Email API.” And “programmable email” as a phrase is currently claimed by HubSpot, where it means templated marketing personalization, a different thing entirely. The developer version of the term was sitting unused.
The round trip in one file
Here’s send, receive, and reply — the whole loop — on Node 18+ (npm install mailkite). A send-only API gives you the first call; the rest is the half it’s missing.
import { createServer } from "node:http";
import { MailKite } from "mailkite";
const mk = new MailKite(process.env.MAILKITE_API_KEY);
const SECRET = process.env.MAILKITE_WEBHOOK_SECRET;
// 1. Send — the part every "email API" gives you.
await mk.send({
from: "support@myapp.ai",
to: "ada@example.com",
subject: "Your invoice #1042",
text: "Reply to this email and we'll pick it up — a human or an agent.",
});
// 2. Receive the reply as JSON — the half a send-only API doesn't have.
createServer(async (req, res) => {
let raw = "";
for await (const chunk of req) raw += chunk;
// signature check, replay window, constant-time compare: one call
if (!MailKite.verifyWebhook(req.headers["x-mailkite-signature"], raw, SECRET)) {
return res.writeHead(401).end();
}
res.writeHead(200).end("ok"); // ack fast
const event = JSON.parse(raw);
if (event.type !== "email.received") return;
// 3. Reply in-thread — same client, same domain, both directions.
await mk.send({
from: "support@myapp.ai",
to: event.from.address,
subject: `Re: ${event.subject}`,
text: `Got it — "${event.text.slice(0, 60)}…" — we're on it.`,
});
}).listen(3000);
No IMAP poll, no MIME parser, no mail server. The inbound message arrives already parsed (event.from.address, event.subject, event.text, event.html), signed so you can trust it, and you reply through the same mk.send you used to start the thread. The threading specifics live in reply by email in your app; the full inbound payload is in parse inbound email to JSON.
That third call is also what makes programmable email the natural fit for AI: an agent that can both read inbound and reply in-thread has a real identity on your domain, not just a way to shout. That path is its own post — give your AI agent its own inbox.
So which do you need?
FAQ
Is programmable email just an email API plus inbound parsing?
That’s the core of it, yes — but “plus” is doing real work. Programmable email delivers inbound as clean JSON (not multipart/form-data), signs the webhook so you can trust it, gives you an inbox identity per app or agent, and lets you reply in-thread from the same client — all on one verified domain. Bolting a separate parse product onto a send API gets you partway, with two setups to maintain.
Do I still need a mail server or IMAP? No. You point your domain’s MX at the service and inbound arrives at your HTTPS endpoint as JSON. No Postfix, no IMAP polling, no MIME decoding.
MailKite (which we build) is programmable email: send with one API, receive every message as JSON, reply in-thread, across unlimited domains and mailboxes, free — see the plan. Start from the programmable email overview or point a domain and watch your next inbound email arrive as JSON.
Related: What is programmable email? defines the category, and the honest SendGrid Inbound Parse alternative is the same comparison for the inbound half specifically.