API
Sending transactional email over an API without regretting it later
Updated 3 September 2026 · 7 min read · SendKernel
A transactional email API call is one authenticated POST with a from address on a domain you have verified, a recipient, a subject, and an HTML body. That part takes about ten minutes. The decisions worth spending time on are the ones that are expensive to change afterwards: whether a timeout can cause a duplicate send, where bounce and complaint events land, and whether receipts share a reputation with your marketing campaigns. Get those three right on day one and the integration never needs revisiting.
The short version
- The send call is trivial. Retry semantics, event handling, and stream separation are the parts that decide whether you regret the integration.
- Most APIs are not idempotent. A retry after a timeout sends a second email unless you prevent it yourself.
- Bounces and complaints arrive asynchronously by webhook. A 202 means accepted, never delivered.
- Keep transactional and marketing mail on separate streams so a campaign cannot pull password resets into spam.
The call itself
Every transactional email API in this category converged on the same shape years ago: a bearer token, a JSON body, an accepted-for-sending response with an id you can use to look the message up later. The example below is ours, but the structure transfers directly to any of the major providers.
const response = await fetch("https://www.sendkernel.com/api/v1/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.EMAIL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "Acme <receipts@yourdomain.com>",
to: ["customer@example.com"],
subject: "Your receipt",
html: "<p>Thanks for your order.</p>",
reply_to: "support@yourdomain.com",
}),
});
// 202 means accepted for sending. It does not mean delivered.
const { id } = await response.json();Three details in that snippet are load-bearing. The key comes from an environment variable, because a key in client-side code is a key someone else can send from. The from address is on a domain you have verified, since sending as a domain you have not authenticated fails DMARC and will be rejected. And the response is a 202 rather than a 200 — the message has been accepted for sending, and whether it was delivered is a question answered minutes later, by webhook.
Retries, and the duplicate email problem
This is the failure that reaches customers, and almost every integration has it at first. Your request times out. Your HTTP client, or your job queue, retries. The original request had already been accepted on the far side, so the customer receives two receipts — or, in the case of a password reset, two links, one of which may now be invalid.
Most transactional email APIs, ours included, do not support idempotency keys, which means preventing this is your responsibility rather than the provider's. There is no way to ask after the fact whether a timed-out request was accepted.
1Record the intent before you call
Write a row with your own send key — an order id plus a message type, say — and a pending status. Send only if that write succeeded and no row already exists.
2Store the returned id against that row
Now you have a mapping from your intent to the provider's message, which is also what makes delivery events useful later.
3On a timeout, do not retry blindly
Mark the row uncertain and reconcile it, rather than sending again. Delivering nothing is recoverable; delivering twice is not.
4Retry only on 5xx and 429, with backoff
A 4xx is a bug in your request and will fail identically forever. Retrying it just burns rate limit.
| Status | Meaning | Retry? |
|---|---|---|
| 202 | Accepted for sending | No — it worked |
| 401 | Key missing, invalid, or revoked | No — fix the key |
| 403 | Sending domain not on this workspace | No — verify the domain |
| 422 | Request or send prerequisites failed | No — read the message |
| 429 | Rate limited | Yes, with backoff |
| 5xx | Provider-side failure | Yes, with backoff and a cap |
| Timeout | Unknown — it may have been accepted | No. Reconcile instead |
Delivery events are the half people skip
A successful API call tells you the provider accepted the message. Everything you actually care about — delivered, bounced, marked as spam — happens afterwards and reaches you by webhook. Integrations that skip this work fine in testing and quietly rot in production, because nothing updates the application's view of which addresses are still reachable.
- Hard bounce: the address does not exist. Stop sending to it permanently, and mark it in your own database as well as the provider's suppression list.
- Soft bounce: a temporary failure such as a full mailbox. Retry a few times, then treat repeated failures as hard.
- Complaint: the recipient marked it as spam. Suppress immediately, and treat it as more serious than an unsubscribe.
- Delivered: accepted by the receiving server. Useful for support conversations, and the only event that answers 'did they get it'.
Two properties matter when you write the handler. It must be idempotent, because providers redeliver events and you will see duplicates. And it must verify the signature on the request, because an unauthenticated webhook endpoint lets anyone suppress your customers' addresses by posting fake bounces.
Keep transactional mail away from marketing mail
Password resets and receipts have to arrive. A newsletter would like to arrive. If both go out through the same stream on the same domain, a badly received campaign drags the reset emails down with it — and the person who cannot log in does not care that the cause was a marketing send.
- Separate the streams, either as distinct products or distinct subdomains — receipts@ or mail@ for transactional, news@ for campaigns.
- Never add an unsubscribe header to transactional mail. It is not required by the bulk sender rules, and a recipient who opts out of password resets has locked themselves out of their own account.
- Do not slip marketing content into transactional messages. A receipt with a promotion attached is a marketing message legally and, more importantly, is what recipients report as spam.
- Watch the two complaint rates separately. A transactional complaint rate above zero is a signal that something is wrong with what you are sending.
A short checklist before you ship
- API key in a server-side environment variable, never in client or mobile code.
- Sending domain verified with SPF, DKIM, and DMARC all aligned.
- Your own send key recorded before the call, so a timeout cannot become a duplicate.
- Retries limited to 5xx and 429, with exponential backoff and a ceiling.
- A signed, idempotent webhook handler that writes bounces and complaints back to your database.
- Transactional and marketing on separate streams, with separate reputations.
- A plain-text alternative on every message, generated if you do not write one.
- A staging path that does not deliver, so tests cannot mail real customers.
For what it is worth, the API described here runs the same preflight as the dashboard — an unverified domain, a missing postal address, or a complaint rate over the threshold blocks an API send exactly as it blocks a manual one. There is no API-only bypass. That is occasionally inconvenient and it is the point: a compliance rule that a script can skip is not a rule.
Questions people actually ask
- What is the difference between transactional and marketing email?
- Transactional email is triggered by one person's action and is expected by them — receipts, password resets, shipping updates, security alerts. Marketing email is sent to a list on your schedule. The distinction is not cosmetic: marketing mail requires a one-click unsubscribe header and a postal address, transactional mail is exempt from the unsubscribe requirement, and mixing the two costs you the exemption.
- Does a 202 response mean my email was delivered?
- No. It means the provider accepted the message for sending. Delivery, bounces, and complaints are reported asynchronously by webhook, usually within seconds but sometimes minutes later. Any integration that treats a 202 as proof of delivery will eventually tell a customer their email was sent when it hard-bounced.
- How do I stop retries from sending duplicate emails?
- Record the intent in your own database before you call the API, keyed by something stable like an order id plus a message type, and refuse to send if a row already exists. Most transactional email APIs do not support idempotency keys, so this has to live on your side. On a timeout, reconcile rather than retrying — a missing email is recoverable, a duplicate password reset is not.
- Can I send from a Gmail or Outlook address through an API?
- Not successfully. A From address on a domain you do not control cannot pass DMARC, and Gmail's own policy causes mail claiming to be from gmail.com but sent elsewhere to be rejected. You need a domain you can add DNS records to. This is the most common reason a first API integration appears to work and delivers nothing.
- Do I need to handle bounce webhooks if my provider maintains a suppression list?
- The provider's suppression list stops the mail, which is the urgent half. You still want the events in your own database so your application knows an address is unreachable — otherwise you keep showing a customer as emailed, keep queueing sends that get silently dropped, and have no way to prompt them for a new address.
Get an API key and send a test message
Create a key on any plan, including free, and use the read-only account endpoint to check it before you send anything. 2,000 transactional emails a month, no card.
Start with SendKernel freeRead next
The preflight checklist that should run before every send
Eight checks that should block a send rather than warn about it — identity, consent, suppression, quota, reputation — and why one shared path matters.
The bulk sender rules for Gmail, Yahoo, and Microsoft, and what happens when you miss one
The 5,000-a-day threshold, the 0.3% complaint ceiling, and the authentication every mailbox provider now requires — with the failure codes you will see.
Build on Amazon SES, or buy a platform? The calculation nobody does
SES costs a quarter of any platform per message. Here is the layer you build to get there, priced in engineering weeks, and where break-even falls.