The Email API Developer Glossary (2027)
14 email API terms that decide whether your integration is reliable or quietly broken — starting with the one that sent 41 welcome emails to one user.
14 email API terms that decide whether your integration is reliable or quietly broken — starting with the one that sent 41 welcome emails to one user.
41 welcome emails. One user. Ninety seconds.
The signup endpoint called the email API, the API took longer than my HTTP client's 10-second timeout, the client threw, my retry wrapper caught it and tried again — and every one of those sends had actually succeeded. The API had no idea the requests were the same request, because I'd never given it a way to know.
The fix was one header. Four minutes, once I knew the word for it.
That's the annoying thing about email API terms: the concepts are simple, but without the vocabulary you can't search for the fix. You don't know what you're missing, so you can't google it.
So here are 14 of them, written for whoever's wiring up sends at 11pm and getting weird results.
| Term | What it actually means |
|---|---|
| Idempotency key | A client-generated string that makes a repeated send request a no-op |
| At-least-once delivery | The API guarantees your request lands, maybe more than once — dedupe is on you |
| Retry backoff | Escalating waits between retry attempts, ideally with jitter |
| Rate window | The rolling time span your request quota is measured over |
| 429 / Retry-After | "Slow down" plus the server telling you exactly how long |
| Batch endpoint | One HTTP call carrying many messages, each with its own result |
| Webhook signature | An HMAC proving the event came from your provider, not a stranger |
| Timestamp tolerance | The age limit that stops a captured webhook from being replayed |
| Dead-letter queue | Where events go after your endpoint fails every retry |
| Message stream | A named sending lane with its own reputation and suppressions |
| Sandbox mode | Accepts and validates a send, delivers nothing |
| Suppression list | Addresses the provider refuses to send to, permanently |
| Message ID | The per-message identifier that ties a send to its later events |
| Sending domain | The domain that signs the mail, which isn't always the From domain |
A unique string you attach to a send so the server can tell "retry of request A" from "new request B." Same key inside the retention window, same response, no second email. Stripe popularised the pattern for payments; email adopted it for the same reason — the failure mode is expensive and visible.
Generate it once, when you create the job, and store it. Generating it inside the retry loop defeats the whole mechanism — a mistake I've made. Don't hash the request body either: two separate password resets to the same address in an hour are legitimately two emails, and body-hashing swallows the second. JustEmails accepts idempotency keys on its REST API for exactly this.
The contract almost every queue and HTTP API actually offers: your message will be processed, possibly more than once. Exactly-once is mostly marketing — the honest version is at-least-once plus a dedupe key, which is at-least-once wearing a nicer shirt.
| Semantic | What you get | Who handles duplicates |
|---|---|---|
| At-most-once | Fire and forget, may vanish | Nobody — you lose messages |
| At-least-once | Always arrives, may repeat | You, via idempotency key |
| Exactly-once | Marketing copy | Still you |
Escalating waits between attempts — 1s, 2s, 4s, 8s — so a struggling server gets room to recover instead of a wall of retries. Jitter is the part people skip: randomness, so ten thousand clients that failed at the same second don't all retry at the same second. Without it you've built a synchronised stampede that re-creates the outage you were retrying past.
Cap the attempts, too. An unbounded retry loop against a permanently failing endpoint is a self-inflicted outage, and it's your own alerts inbox that pays for it — I've spent a morning deleting the evidence.
The span your quota is measured over — 100 requests per rolling 60 seconds, say. Fixed and rolling windows differ at the edges, and the difference is real: with a fixed window you can send 100 at 11:59:59 and 100 more at 12:00:01, pass both, and still get throttled by a downstream rolling counter that saw 200 in two seconds. Check which one you're up against before tuning worker concurrency. Good luck finding it in the docs, though — in my experience roughly half of providers just don't say, so you learn which one it is by getting throttled.
429 Too Many Requests means you hit the limit. The Retry-After header tells you how long to wait, in seconds or as an HTTP date. Honour it. Treating a 429 like a 500 — retrying immediately — is the most common way to turn a brief throttle into a long block. I know because I wrote the retry wrapper that did it.
One request carrying many messages. Fewer TLS handshakes, less overhead, higher throughput. The catch nobody documents loudly enough: batch responses are usually partial success. HTTP 200, and an array where message 3 failed validation while the other 49 went out fine.
Check the status code and move on, and you've silently dropped an email while logging a success. Iterate the array. Every time.
Burying partial-success semantics three levels down in an API reference is, I think, the worst documentation habit in this whole category.
An HMAC (SHA-256, typically) computed over the raw payload with a shared secret, sent in a header. Your endpoint is a public URL anyone can POST to, so the signature is what proves the event came from your provider. Skip verification and a stranger can tell your app a customer's email hard-bounced, then watch your code helpfully suppress that address forever.
Hash the raw bytes, before any JSON parsing. Compare in constant time. Two rules, and most broken verification code violates one of them.
The maximum age of a signed payload you'll accept, usually five minutes. Signatures don't expire on their own — without this check, a payload captured once stays valid forever. The timestamp goes inside the signed material, otherwise an attacker just edits it.
An endpoint that checks the signature but skips the timestamp isn't "mostly secure." It's unauthenticated with extra steps.
When your endpoint returns a non-2xx or times out, the provider retries on a backoff, then eventually gives up and parks the event. That parking spot is the dead-letter queue.
So your handler needs to be fast and idempotent: acknowledge with a 200, push the work onto a queue, process asynchronously. A handler doing 8 seconds of database work inline will time out under load, get retried, and process the same bounce four times. Bounces and complaints are the events you least want to mishandle — handling bounce and complaint webhooks covers the reputation side.
The provider accepts the request, validates it, returns a realistic response — and delivers nothing. Mailtrap built a business on the idea. It exists because "staging can't send real mail" is a promise your environment config makes and eventually breaks, usually the day someone restores a production dump into staging.
Strong opinion, take it or leave it: a provider that still ships no sandbox is telling you exactly how much they test their own API.
A named lane with its own reputation, suppression list, and event log. Postmark made the term common. Transactional mail and bulk announcements have opposite engagement profiles, so mixing them lets the complaint rate from your newsletter decide whether password resets reach the inbox.
Related but separate: the dedicated sending subdomain, which does the same isolation job at the DNS level.
Addresses the provider will refuse to send to — hard bounces, spam complaints, unsubscribes. It's a safety rail, not a bug. Sending to a known-dead address is a reputation hit with a guaranteed-zero payoff.
Check whether your provider's API lets you read the list. If it does, sync it into your own database — otherwise your app queues a send, gets a silent drop, and support spends a morning on "the customer never got the email."
The identifier returned on a successful send, referenced by every later webhook event. Store it on the row that triggered the send. Skip that and you get a webhook saying "message a1b2c3 bounced" with no way to know which user that was — I've written that backfill after the fact and it isn't fun. JustEmails keeps message logs and sends webhook delivery notifications, including on the 1,000 transactional emails/month that come with the $49/year plan.
The sending domain signs the mail (DKIM) and owns the Return-Path; the From domain is what the human sees. They can differ — that's what Gmail's "sent via" label is reacting to — and aligning them is what DMARC checks. Return-Path vs From address has the mechanics; why no-reply@ is a bad default covers the part that's a product decision, not a DNS one.
202 vs 200 — most email APIs return 202 Accepted, meaning queued, not delivered; treating it as delivered is a reporting bug waiting to happen. Idempotency window — how long keys are remembered, typically hours to a day. Hard vs soft bounce — permanent versus temporary; suppress the first, retry the second, and mixing them up quietly shrinks your list. List-Unsubscribe-Post — the RFC 8058 header behind one-click unsubscribe, now effectively mandatory for bulk senders. Template versioning — pinning a send to a version so a designer's edit can't rewrite last month's receipts. Oh, and structured logging on the app side: JustAnalytics keeps logs, traces, and uptime together, which beats chasing a send failure across three dashboards. Rate limiting at the edge is a cousin of the same problem — ClickzProtect does it for bot traffic before the request costs you anything.
If you keep four of these email API terms, keep idempotency key, partial success on batch endpoints, webhook signature with a timestamp check, and 202 means queued. Those four cover the failure modes that produce silent, expensive bugs — duplicates, dropped messages, spoofed events, dashboards that lie.
And a take not everyone shares: your integration's reliability depends far more on your retry and webhook code than on which provider you picked. Providers converge on features fast. Your retry loop is bespoke, untested, and runs at 3am. That's where the bugs live.
It's a unique string you generate and attach to a send request so the API can recognise a repeat of that exact request. If your client times out and retries, the server sees the key it already processed, returns the original result, and does not send a second email. The key has to be stable across retries and unique across distinct sends — a UUID stored alongside the job usually works, while a hash of the request body does not, because two genuinely separate welcome emails to the same address hash identically. Most APIs remember keys for a bounded window measured in hours or days, so it protects against retry storms, not against a duplicate you fire next week.
Compute an HMAC over the raw request body using your signing secret, then compare it to the header value with a constant-time comparison function. Three details break most implementations: parsing the JSON before hashing (your framework re-serialises it and the bytes no longer match), using == instead of hmac.compare_digest (timing leak), and ignoring the timestamp. Without a timestamp tolerance — five minutes is the common window — a signature stays valid forever, so anyone who captures one payload can replay it at will. Verify signature and timestamp, then process.
Usually yes, and for a reason that has nothing to do with environments. A staging server with real API credentials still hits real inboxes when someone seeds it with a production database dump. Sandbox mode accepts the request, validates it, returns a normal response, and drops the message instead of delivering it — so your integration tests exercise the real code path without mailing 4,000 people a test order confirmation. If your provider has no sandbox, the workaround is a fake domain plus an allowlist of internal addresses, checked in code before the send call.
A message stream is a named lane inside your account with its own sending reputation, suppression list, and event history. Transactional mail — password resets, receipts — gets opened and rarely reported. Bulk announcements get ignored and sometimes marked as spam. Put them in one stream and the complaint rate from the second category drags down delivery of the first, which is how a company ends up with password reset emails in the junk folder. Separate streams (or at minimum separate sending subdomains) keep one category's reputation from poisoning the other.
Unlimited custom domain email hosting for $49/year flat — unlimited domains, unlimited mailboxes, 10 GB storage, full IMAP/SMTP. Built for agencies, freelancers, and anyone managing email across more than one domain.
The trial wants a card up front. That's there to stop trial-farming, not to catch you out — cancel anytime before day seven and nothing is charged.