A transactional email API sends one message at a time, triggered by something a person did in your product. Someone resets a password, places an order, or requests a login code, and your code makes an HTTP call that puts the right email in their inbox within seconds. You pass a recipient, a subject, and a body, and the API relays the message and reports back on what happened to it. The alternative is running your own mail server, tracking bounces by hand, and rebuilding deliverability tooling that already exists.
This guide covers what to look for when you pick a transactional email API and how a real integration comes together, with code you can run. The examples use Samva, but the evaluation criteria apply to any provider on the shelf.
What a transactional email API handles for you
The send is the visible part. The work sits around it. A production-grade API carries five jobs so your application code does not have to:
- Delivery. One authenticated HTTP call accepts the message and hands it to sending infrastructure that already has warmed IPs and provider relationships.
- Authentication. The API signs your mail with DKIM and aligns SPF and DMARC on domains you own, so mailbox providers trust the message.
- Events. Delivery, bounce, and complaint outcomes come back to your app as signed webhooks, so you react to a failure instead of discovering it in a support ticket.
- Suppression. Addresses that hard-bounce or complain stop receiving mail on their own, which protects the reputation that gets the rest of your mail delivered.
- Templates. Message bodies live as versioned templates rendered on the provider's side, so your code passes data rather than assembling HTML on every send.
A transactional API differs from a marketing platform in intent. Transactional mail goes to one person because of an action they took, and it needs to arrive fast and reliably. It also differs from a raw SMTP relay: SMTP moves bytes between servers, while the HTTP API gives you typed responses, event webhooks, and observability that SMTP alone never surfaces.
How to evaluate one
Delivery API and idempotency
Start with the send call and the shape of its response. A good API returns a stable message id you can use to look the send up later, and a status you can act on. Check what happens when your own code retries. Networks drop responses, and a naive retry sends the email twice.
For retryable work, generate a stable key for the logical send and pass it as idempotencyKey in the request body. Replaying an identical request with that key returns the original message without another provider send or charge. Reusing the key with different recipients or content returns 409 Conflict, which makes accidental key reuse visible instead of silently dropping or duplicating mail. The key is scoped to your organization and must contain 1 to 255 characters.
Domain authentication
Mail from a domain a provider cannot authenticate has little chance of reaching the inbox, and increasingly gets rejected outright. The API you pick should set up three records on domains you own: DKIM to sign each message, SPF to declare who may send for you, and DMARC to tie them into a policy with reporting. A custom MAIL FROM subdomain matters too, so your mail sends from your own domain rather than appearing as sent "via" the provider.
Look for a provider that generates the exact records per domain and verifies them for you. Setup you do once and reporting you can read afterward beats a wall of documentation and a manual DNS checklist.
Webhooks and events
The send tells you the message was accepted. It does not tell you the message landed. Delivery webhooks close that gap. Read the event catalog before you commit: you want distinct events for acceptance, delivery, bounce, complaint, and failure, each with enough payload to route the outcome inside your app.
Signing is not optional. Any endpoint on the public internet receives forged requests, so the provider must sign each webhook and you must verify it. Check that the signature covers the raw body and that the docs are honest about retry behavior when your endpoint is down.
Template workflow
Assembling HTML strings in application code is where transactional email rots. Templates that live with the provider, versioned and rendered server-side, keep the message and the send apart. Your code references a template by id and passes data; the pinned version renders and goes out. Evaluate whether templates are versioned, whether you can test a version before it sends, and whether the same template serves both a one-off send and a larger run.
Suppression behavior
A single dead address that keeps receiving mail drags down the reputation behind every other send. Suppression should be automatic: a hard bounce or a spam complaint adds the address to a list, and the provider stops sending to it without you writing that logic. Confirm the provider does this on its own, and that you can inspect and manage the suppression list when you need to.
Deliverability observability
You cannot fix what you cannot see. The provider should surface delivery status, bounces, complaints, and engagement signals like opens and clicks in one place. Treat engagement numbers as directional, since opens depend on a recipient loading remote content and some providers block tracking pixels. The value is the trend: which sends land, which segments quietly fail, and where reputation is slipping before customers feel it.
A realistic integration
Here is the shape of a first integration with Samva, from install to a verified delivery event.
Install the SDK and send a message. The email.send() facade takes a recipient, a subject, and a body:
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
const result = await samva.email.send({
to: "ada@example.com",
replyTo: "support@acme.com",
subject: "Your order shipped",
html: "<h1>On its way</h1><p>Order A-1042 is out for delivery.</p>",
});
if (result.error) {
console.error("Send failed:", result.error);
} else {
console.log("Queued message:", result.data?.id);
}
Most transactional mail comes from a template, so the body does not live in your code. Reference the template by id and pass its data:
await samva.messages.send({
to: [{ contactId: "9b2c0e3a-7d41-4f2e-8a16-1f2c3d4e5f60" }],
channel: "email",
email: {
subject: "Your order shipped",
templateId: "order-update",
templateData: { orderId: "A-1042", eta: "Tomorrow" },
},
});
Over the REST API, both forms post to the same endpoint. There is no separate email.send HTTP route; the SDK facade is sugar over POST /v1/messages:
curl -X POST https://api.samva.app/v1/messages \
-H "X-API-Key: samva_sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"to": [{ "email": "ada@example.com" }],
"channel": "email",
"email": {
"subject": "Your order shipped",
"html": "<h1>On its way</h1>"
}
}'
A successful send returns 201 with the message id and a pending status. To follow delivery, subscribe an endpoint to the events you care about:
await samva.webhooks.create({
name: "Delivery events",
url: "https://acme.com/webhooks/samva",
events: ["message.delivered", "message.bounced", "message.failed"],
});
Then verify each incoming webhook before you trust it. Samva sends an X-Webhook-Signature header holding an HMAC-SHA256 of the raw body, keyed with your webhook secret:
import crypto from "crypto";
function verify(rawBody: string, signature: string, secret: string): boolean {
const expected = `sha256=${crypto.createHmac("sha256", secret).update(rawBody).digest("hex")}`;
const received = Buffer.from(signature);
const computed = Buffer.from(expected);
if (received.length !== computed.length) return false;
return crypto.timingSafeEqual(received, computed);
}
Compare against the exact bytes you received. A JSON parser that re-serializes the body changes it and breaks the HMAC, so register a raw-body parser on the webhook route. Return a 2xx once you have verified and accepted the event; Samva retries any non-2xx response.
Failure modes to design for
A production integration plans for the send that does not go cleanly. Design for these from the start:
- Retries that duplicate. Give each logical send a stable
idempotencyKeyand reuse it for retries. Samva returns the original message for an identical replay and rejects changed input with409 Conflict. - Rate limits. Under a burst, the API returns
429with aretryAfterSecondsvalue. Back off for that long and retry rather than hammering the endpoint. - Sending to suppressed addresses. A send to an address that already hard-bounced will not deliver, and trying to route around suppression works against your own reputation. Treat suppression as a signal, not an obstacle.
- Unverified webhook payloads. An endpoint that skips signature verification will process forged events. Verify first, then act.
- Bodies that fail authentication. Sending from a domain before its DNS records verify means the mail has no established identity, and receivers reject it. Verify the domain before you move real traffic onto it.
Errors come back as a flat JSON object with a _tag discriminator, so you branch on the tag: RateLimitedError for 429, ValidationError for 422, PaymentRequiredError for 402, and so on. Handling those tags explicitly turns a vague "send failed" into a specific recovery path.
Where to go next
Pick the provider whose delivery model, authentication setup, event catalog, and suppression behavior match how you want to operate, then wire the send, the webhook verifier, and the retry handling before you ship. The send is one line. The reliability lives in the rest.