Home/Transactional email API

Transactional email API: the send call is the easy half, and an agent only writes that half

The response tells you the message was accepted. Delivery is decided later, somewhere else, and in an integration an agent wrote there is nobody rostered to go and look.

VerifiedBy George Hartley, Co-founder·Updated

What a transactional email API is

A transactional email API is an HTTP endpoint your application calls to send one message to one person because something just happened: an account that needs confirming, a password that needs resetting, an order that needs a receipt. You post a recipient, a subject and a body, and you get an identifier back. That response means the message was accepted for sending, not delivered. On Nitrosend the Free plan includes 5,000 emails up front, then 500 a month, and a new account starts at 100 recipients per rolling 24 hours, rising to 5,000 with sender standing. The same call works from Node, curl or Python.

What changed is who writes the call. A developer used to read the docs, write the integration, ship it, and come back a week later to see whether it had worked. Increasingly the integration is written inside Claude, Codex, Cursor or ChatGPT, in one session, from a sentence describing what should happen. The code that comes out is usually fine. The problem is that the session ends.

A transactional email API cannot tell you whether the email arrived. It tells you it took the message. Those are two different facts separated by an unbounded amount of time, and the gap between them used to be closed by a person who eventually went and looked at a log. Agent-written integrations quietly remove that person.

Accepted is not delivered, and the field names admit it

Nitrosend's documentation for the send endpoint says it plainly. POST /v1/my/messages "queues a single transactional email or SMS and returns the message resource. Delivery happens through Nitrosend's normal background sender, so the response means 'accepted for sending', not 'delivered'." (Nitrosend REST API docs.)

Then look at the shape of what comes back. The send response carries { id, status, accepted_at }. There is no delivered_at on it, because at the moment that response is written nobody knows. delivered is a value you filter for afterwards, on GET /v1/my/messages?status=delivered, which is a separate call somebody has to decide to make.

This is not a Nitrosend quirk. It is what the protocols say. HTTP defines 202 Accepted as meaning "the request has been accepted for processing, but the processing has not been completed. The request might or might not eventually be acted upon, as it might be disallowed when processing actually takes place." Then it adds the sentence that belongs on a wall in every team shipping email: "There is no facility in HTTP for re-sending a status code from an asynchronous operation." (RFC 9110, section 15.3.3.)

The real answer travels on a different channel, and it always has. Email's standard way of reporting how a message actually went is a Delivery Status Notification, a separate machine-readable message whose stated job is to "report the result of an attempt to deliver a message to one or more recipients" (RFC 3464). Separate message. Later. Different direction of travel. No API design changes that.

So every transactional email API that returns quickly, which is all of them, hands you half an answer by construction. Feature tables will not tell you which one to pick, because on this they are identical. The question that actually separates them is what happens to the other half. For how providers compare on everything else, see the email API service reviews.

What an agent does differently: nobody is rostered to read the other half

An agent writing a transactional integration differs from a developer in a few concrete ways. It works in one session and then stops. It retries a call by itself when a request times out. It writes the API key into the code or the environment as it goes. And it treats an accepted response as done, because nothing in the response says otherwise.

In the dashboard era the other half had a reader. Not a diligent one, but a real one. Something felt off, or a customer wrote in, and a human opened the activity log and scrolled. That habit was never in anyone’s runbook, and it was load bearing.

An integration written by an agent in a single session has no such person. The agent wrote the call, the call returned a success, the session closed. Nothing in that sequence rosters anybody to come back in three weeks and confirm that receipts are still going out. The failure that follows is silence: an integration that stops sending raises no error anyone sees, so the useful alert describes an absence, along the lines of these transactional emails have not sent for a while.

Two checks follow for anyone running an integration an agent wrote.

  • Expect contract changes in the response. Code an agent wrote does not read changelogs, so a change has to reach it in band. The Deprecation header field signals "to consumers of a resource (identified by a URI) that the resource will be or has been deprecated" (RFC 9745), and the Sunset header field "indicates that a URI is likely to become unresponsive at a specified point in the future" (RFC 8594). Log both. A required header change that nobody sees can silently stop sends.
  • Give silence its own alarm. A sender that has gone to zero produces zero errors, zero bounces and zero complaints, which on a chart looks the same as a quiet week. Check daily for steady transactional senders that have gone quiet, rather than only for errors.

Worked example: the send call, and the one header to pass

The prompt, in the user’s words: Send a receipt email from our checkout service when an order is paid, and make sure we know if it arrives.

Before. Unguided, an agent writes the Node SDK call below, runs it once, sees an id come back and reports the job done. Nothing checked the sending domain, nobody saw the receipt render, and the call carries no idempotency key, so the agent’s own retry after a timeout can send the receipt twice.

import { Nitrosend } from '@nitrosend/sdk';

const ns = new Nitrosend({ apiKey: process.env.NITROSEND_API_KEY });

await ns.messages.send({
  channel: 'email',
  from: '[email protected]',
  to: order.customerEmail,
  subject: `Your receipt for order ${order.id}`,
  html: renderReceipt(order),
}, `receipt-${order.id}`);

After. With the Nitrosend MCP server connected, the same request runs as a gated sequence, one call per step.

  1. nitro_get_status gets the current account context, sender identity and readiness, so the agent knows whether the account can send at all.
  2. nitro_manage_domains verifies or diagnoses the customer sending domain the receipt will come from.
  3. nitro_review_delivery runs a read-only review of the receipt template, with validation, spam analysis, preflight and previews. It never approves or sends.
  4. nitro_send_test_message sends one real test of that template to the team. It never sends to an audience.
  5. nitro_send_message sends the live transactional email for the paid order, with an idempotency_key taken from the order id.

Over REST, the live send with the key looks like this.

curl -X POST https://api.nitrosend.com/v1/my/messages \
  -H "Authorization: Bearer $NITROSEND_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order_1042" \
  -d '{
    "from": "[email protected]",
    "to": "[email protected]",
    "subject": "Your receipt for order 1042",
    "html": "<p>Thanks for your order.</p>"
  }'

The retry is where the key earns its place. If the live send times out, the agent cannot tell whether the first attempt committed, so it sends again with the same key and the same payload, and gets the original message back instead of a second receipt. The tool description is specific that the key is reused only for an exact retry. A new order gets a new key. The full version of that argument lives on the email API page.

Agents get the same surface rather than a lesser one. Campaigns, flows, templates, segments, approvals and delivery review are exposed as typed tools over MCP, and a single transactional send is available there too as nitro_send_message. REST and the SDK are the recommendation for mail your application triggers, not a restriction on what an agent may call. The rest of the endpoints are on the email API page. The same call from Python, Go, Java, C# and the command line is covered on its own page, and other languages are listed on the email API languages page.

What to read back, and when

Treat the read path as part of the integration rather than as observability you will add later. If an agent is writing the send, have it write this at the same time, in the same session, while the context is still loaded.

The six read-back channels for a transactional send
ChannelPush or pullEndpoint or toolUse it when
EventsPushedSigned webhooksYour code should react as a message is delivered, bounces or fails
MessagesPulledGET /v1/my/messages?status=You need to know whether a specific send landed, or you may have missed a push
SuppressionsPulledGET /v1/my/suppressionsA specific customer stopped receiving mail
CapacityPulledGET /v1/my/delivery/statusYou want pacing and a bounded reason before a burst
Per personPulledContact timelineThe question is about one person rather than one message
TrendsAskednitro_get_insightsSomeone asks how receipts are doing

Events. Webhooks carry email.sent, email.delivered, email.opened, email.clicked, email.bounced, email.complained and email.failed. Payloads are signed with HMAC-SHA256 in the Standard Webhooks format, retried with exponential backoff, and include the message id, recipient, timestamp and event detail. Setup and signature checks are on the email webhooks page.

Suppressions. Hard bounces, soft-bounce suppressions, complaints and manual suppressions surface here, with the bounded provider diagnostic text kept from the original feedback event.

Capacity. The delivery projection never authorizes or reserves a send, and the documentation tells clients not to calculate headroom from a plan name.

Trends, from the agent. nitro_get_insights gets email analytics with trends, benchmarks and recommendations. An agent can call it at the end of the session that wrote the integration, and again whenever someone asks how receipts are doing, so the read-back becomes a question anyone can ask instead of a log someone has to open.

One caution about counters. Sent and delivered are written by two different things. Sent is written when the message is handed off. Delivered is written when the sending provider reports back, which only happens if the feedback path is wired. A healthy sent count next to a delivered count of exactly zero is a plumbing symptom, so check that feedback is wired before touching content. A low delivered count is a deliverability symptom. They look similar on a chart and they need opposite responses.

Then there is the half that faces your user, which most APIs leave to you. A transactional email that fails should be visible to the person waiting for it, with a way to act, such as correcting the address and sending again. Route email.failed and email.bounced to that path.

Where it goes wrong when an agent sends transactional email

Four failure modes are specific to an agent owning the send path, and each one has a check.

  • Retry storms and double receipts. An agent that sees a timeout retries, and an agent stuck in a loop retries many times. Without an idempotency_key every retry is a new message, so one order can produce several receipts. Take the key from the event, such as the order id, and reuse it only for an exact retry.
  • A made-up to address. A model asked to fill in a recipient it does not have will produce a plausible one. The recipient has to come from the application's own data at send time, never from the prompt or the model's guess, and test sends go to the team's own test recipients.
  • Marketing content in the transactional lane. A transactional API is for one message to the one person who caused it. Using it for promotions, or for unsolicited cold outreach, breaks email compliance norms and puts receipts and password resets at risk. Broadcasts belong in nitro_compose_campaign, which creates a draft and never delivers it.
  • A webhook endpoint nobody verifies. An agent can register a webhook URL and move on without checking the signature or confirming that events arrive. Verify the HMAC-SHA256 signature on every payload, send one test, and confirm an email.delivered event reached the endpoint before the session ends.

Why transactional runs in its own lane

Transactional and marketing email run on separate sending infrastructure here, so a promotional send cannot take receipts and password resets down with it. What counts as transactional in the first place is covered on the transactional email hub.

The less ordinary part is what a transactional lane is for, and therefore what it does not carry. One message goes to one person who caused it, so there is no audience to pace, no subscription to manage and no campaign report at the end, because none of those ideas apply to a receipt. Kam Low, Co-founder and CTO, describes the transactional_plain design mode in the same terms: it "is for receipts and verifications, requiring transaction details and a support path, and it rejects marketing copy."

That thinness is usually discussed as an abuse problem, because a path with fewer guard rails is the path abuse walks down, which the email API page covers. The consequence for honest senders is the inverse: the lane with the least supervision is the lane that owes you the most feedback.

A campaign has a report, an audience you chose on purpose, and somebody who will notice if the numbers look wrong. A transactional integration has none of those. It is wired up once, usually in an afternoon, and then it is expected to run untouched for years while the people who wrote it move on. Every property that makes the transactional path fast and quiet also makes it the part of your email that can fail longest before anyone finds out. Design the instrumentation in at the start.

Why Nitrosend is built this way

The defaults act without a reader. Hard bounces auto-suppress and complaints auto-unsubscribe, as behavior rather than as a setting somebody has to find. Neither requires you to have read a webhook, which is the point when the integration was written in a session that has since closed. Mailbox providers price the alternative directly. Google tells senders to "keep spam rates reported in Postmaster Tools below 0.10% and avoid ever reaching a spam rate of 0.30% or higher" (Google, Email sender guidelines). A complaint nobody read still counts against that number.

Sending allowance is a standing, not a plan tier. Sender standing is Probation, Standard or Trusted, and it combines attributable delivery history, current positive list verification, domain strength and account signals. Qualifying evidence can earn Standard or Trusted on day one, and payment alone does not earn trust. It is evidence based rather than time based, which is why the capacity endpoint tells callers not to work headroom out from a plan name.

One account covers both kinds of mail. Transactional through REST or the SDK, campaigns and flows through the agent, the same contacts and the same deliverability underneath. If you want the argument for why the two are different disciplines rather than two products, it is on the transactional versus marketing page, and the wider case for running email from an agent is on the email marketing hub.

Unlimited contacts, billed per send. A contact you never email costs nothing to keep, so charging for it would only teach you to delete people you might want later. The Free plan includes 5,000 emails up front, then 500 a month, with 100 to 5,000 recipients per rolling 24 hours depending on sender standing. A new account starts at the low end and the allowance rises as standing improves. See pricing.

Transactional email is still wasted real estate at most companies. It is the mail a customer is actually waiting for, and it is the mail most brands phone in.

Transactional email API questions

Is there a free tier, and does it work from Python? Yes. The Free plan includes 5,000 emails up front, then 500 a month. A new account starts at 100 recipients per rolling 24 hours, and the allowance rises to as many as 5,000 as sender standing improves. From Python you call the same REST endpoint with a bearer key.

How do I send transactional email from Node or TypeScript? Install the Node SDK, @nitrosend/sdk, create a client with your API key from an environment variable, and call messages.send with a sender, a recipient, a subject and the HTML body. It is one call, and it returns the message id with an accepted_at time for you to store.

Can I send transactional email over SMTP? No. Nitrosend does not offer an SMTP relay. The supported server-side path for signup verification, password resets and receipts is the REST API, one POST to /v1/my/messages, or the Node SDK. An agent can send the same single message through MCP with nitro_send_message.

What does accepted mean in the response? It means the message is queued for the background sender. The response carries id, status and accepted_at, with no delivered_at, because delivery is not known yet. It arrives later as an email.delivered webhook, or as a status you filter for on the messages endpoint.

Go deeper

Send your first transactional email

Simple pricing. Unlimited contacts.

Every plan includes full stack emailing: Flows, Newsletter Campaigns and Transactional Email, plus our NitroWheel LLM and all agent integrations (Claude, ChatGPT, Codex, Cursor and others). Pay for what you send, not who you store.

Choose your billing interval

Every paid plan includes 3 agent inboxes and no Nitrosend footer.

Starter

For your first email program.

$16USD / month

$192 billed yearly.

20,000 emails / month

Choose Starter
  • 5 brands · 3 seats
  • 5 custom sending domains
  • 500 email verifications/month
  • $1.50 / 1,000 extra emails

Pro

For a growing audience.

$40USD / month

$480 billed yearly.

60,000 emails / month

Choose Pro
  • 10 brands · 5 seats
  • 10 custom sending domains
  • 1,000 email verifications/month
  • $1.20 / 1,000 extra emails

Growth

For teams and agencies.

$120USD / month

$1,440 billed yearly.

200,000 emails / month

Choose Growth
  • Unlimited brands · 10 seats
  • 25 custom sending domains
  • 2,500 email verifications/month
  • $0.90 / 1,000 extra emails

Scale

For high-volume sending.

$240USD / month

$2,880 billed yearly.

500,000 emails / month

Choose Scale
  • Unlimited brands · 20 seats
  • 100 custom sending domains
  • 5,000 email verifications/month
  • $0.70 / 1,000 extra emails
Enterprise

Hey, big sender.

Sending 500,000+ emails a month? Get a custom plan designed for you, with volume pricing for sending, verification and enrichment.

Start free. No card needed.

5,000 emails to start, then 500 emails every month. No expiry on your starter emails.

Start free

Daily allowances depend on your plan and sender standing. Strong list, domain and delivery evidence can raise standing, including on day one. Trusted receives the full plan allowance; available email credits, safety checks and delivery pacing still apply.

Free forever. No credit card required. See full comparison →