# How to Choose a Transactional Email API: Checklist
URL: https://samva.dev/guides/transactional-email-api
> Evaluate a transactional email API on idempotent sends, domain authentication, webhooks, templates, and suppression, then wire a first integration with code.
- Published: 2026-07-07

Choosing a transactional email API comes down to a handful of checks, and most of them have a test you can run before you commit: send the same request twice, break a webhook signature, bounce an address and see what the provider does next. This guide lists the checks, then shows a first integration with Samva so you can see what passing them looks like in code.

If you want the product page instead, [Samva's transactional email API](/features/transactional-email) covers what Samva sends, retries, schedules, and reports. For the difference between transactional and marketing mail, read [transactional vs marketing email](/blog/transactional-vs-marketing-email). The examples below use Samva, but the checklist applies to any provider on the shelf.

## What the API should handle so your code does not [#what-the-api-should-handle-so-your-code-does-not]

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, so you do not run a mail server.
- **Authentication.** The API signs your mail with DKIM and sets up SPF alignment on domains you own, and checks the DMARC policy you publish, so mailbox providers can 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 does not provide.

## How to evaluate one [#how-to-evaluate-one]

### Delivery API and idempotency [#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 the `Idempotency-Key` header. 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 [#domain-authentication]

Mail from a domain a provider cannot authenticate has little chance of reaching the inbox, and increasingly gets rejected outright. Authentication rests on three DNS mechanisms for 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 DKIM and SPF records per domain and verifies them for you, and that checks the DMARC policy you publish rather than leaving it to guesswork. Setup you do once and reporting you can read afterward beats a wall of documentation and a manual DNS checklist.

### Webhooks and events [#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.

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 [#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 [#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 [#deliverability-observability]

The provider should show 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 [#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:

```typescript
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>",
});

console.log("Queued message:", result.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:

```typescript
import { createClient } from "samva";

const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });

await samva.email.send({
  to: [{ contactId: "contact_k9mvt4znw8rh7q2x" }],
  templateSlug: "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`:

```bash
curl -X POST https://api.samva.dev/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:

```typescript
import { createClient } from "samva";

const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });

const result = await samva.webhooks.create({
  name: "Delivery events",
  url: "https://acme.com/webhooks/samva",
  eventTypes: ["message.delivered", "message.bounced", "message.failed"],
  channels: ["email"],
});

const signingSecret = result.secret;
```

Then verify each incoming webhook before you trust it:

```typescript sdk-typecheck-skip="Fragment uses application values or preceding examples: persistAndHandleOnce"
import { verifyRequest } from "@samva/webhooks";

export async function POST(request: Request) {
  const verified = await verifyRequest(request, process.env.SAMVA_WEBHOOK_SECRET!);
  await persistAndHandleOnce(verified.id, verified.event);
  return new Response(null, { status: 204 });
}
```

The verifier checks `webhook-id`, `webhook-timestamp`, and `webhook-signature` against the exact
raw body and rejects stale timestamps. Delivery is at least once and unordered. Retries keep a
stable `webhook-id` and carry a fresh timestamp and signature. The
[webhook event reference](/docs/platform/webhook-events) lists the retry schedule, timeouts, and
tolerance.

## Failure modes to design for [#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 `Idempotency-Key` header and reuse it for retries. Samva returns the original message for an identical replay and rejects changed input with `409 Conflict`.
- **Rate limits.** Under a burst, the API returns `429` with a `retryAfterSeconds` value. 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 about the address.
- **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.

The REST API returns a flat JSON error with a `_tag` discriminator. The Promise SDK turns those
documented errors into matching classes, so use checks such as
`error instanceof RateLimitedError` and read fields like `retryAfterSeconds` directly. Handling
specific failures turns a vague "send failed" into a concrete recovery path.

## Where to go next [#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.

For implementation, start with the [email sending guide](/docs/channels/email/send-an-email) and choose a [transactional email template](/templates/category/transactional). For a provider decision, compare [email API options](/compare) and check [Samva pricing](/pricing) against your expected monthly send volume.

## FAQ

### What should I look for in a transactional email API?

Six things: an idempotent send call that is safe to retry, domain authentication it sets up and verifies for you, signed delivery webhooks, versioned templates rendered by the provider, automatic suppression of bad addresses, and one place to see delivery status. Each one removes work from your application code, and each has a test you can run before you commit.

### How is a transactional email API different from SMTP?

SMTP is the wire protocol that moves mail between servers. A transactional email API is an HTTP layer on top of that: you send JSON over HTTPS instead of speaking SMTP, and you get typed responses, delivery webhooks, template rendering, and suppression handling that raw SMTP does not provide. Most APIs still offer an SMTP relay for tools that only speak SMTP, but the HTTP API is where the events and the observability live.

### Do I need an idempotency key to avoid duplicate sends?

Pass an Idempotency-Key header on POST /v1/messages when your application may retry. Replaying the same request with the same key returns the original message without sending again or charging again. Reusing the key with different request data returns a 409 Conflict. Keys are scoped to your organization and contain 1 to 255 characters.

### How do I know whether an email was delivered?

Subscribe to delivery webhooks. Samva emits message.sent when it hands the message off for delivery, message.delivered when the recipient's mail server confirms it, and message.bounced or message.failed when it does not land. Each event is signed so you can verify it came from Samva. You can also poll the message by id to read its current status if you prefer a pull model for a specific send.

### What DNS records do I need before sending from my own domain?

Samva generates the DKIM and custom MAIL FROM records it needs to verify your sending domain. DMARC is a separate domain-wide TXT policy: Samva never creates, replaces, or requires it. Keep an existing policy, or optionally begin with v=DMARC1; p=none; adkim=r; aspf=r while you monitor authentication.

### How do I verify a webhook came from Samva and not an attacker?

Use @samva/webhooks to verify webhook-id, webhook-timestamp, and webhook-signature against the exact raw body. The verifier rejects stale timestamps; the webhook event reference lists the tolerance.
## Related

- [Email API features](/features): Deliverability, versioned templates, inbound threading, and signed webhooks behind one typed send call.
- [Transactional vs marketing email explained](/blog/transactional-vs-marketing-email): How the two classes of email differ in intent, consent, and deliverability, and why you route them differently.
- [One-click unsubscribe (RFC 8058) explained](/blog/one-click-unsubscribe-rfc-8058): What Gmail and Yahoo now require from bulk senders, and how to implement List-Unsubscribe-Post.
- [Samva documentation](/docs): Reference and integration guides for the Samva email API.
- [Send email with Node.js](/guides/send-email-nodejs): Install the TypeScript SDK and send from Node, including a move from Nodemailer.
- [Send email with Python](/guides/send-email-python): POST the same send to the REST API with requests or httpx.
- [Changelog](/changelog): What shipped in the email API, SDK, CLI, hosted MCP, and dashboard.
- [Replace an Amazon SES email stack](/guides/replace-amazon-ses): A staged migration path from direct Amazon SES and custom glue to a typed email API.
- [Migrate from Resend](/guides/migrate-from-resend): Map sends, batches, templates, webhooks, and domains, then cut over one flow.
