Transactional Email API: Send Email from Code
Short answer: A transactional email API lets your app send one-to-one messages, such as password resets, order confirmations and alerts, with an HTTPS request instead of talking SMTP. With NameOcean you send from an address on your own domain with one POST /api/v1/email/send, signed with your domain's DKIM key, and get delivery, bounce and delay events back as webhooks. Sending is included in the email hosting plan: 1,000 messages a month for each user the plan allows, so the $12/year plan for 5 users can send 5,000 a month. This guide shows the setup, working requests in curl, Python and Node.js, and the limits.
Transactional vs. marketing email
Transactional mail is triggered by something a user did: signing up, resetting a password, placing an order, getting a reply. It goes to one person and they expect it. Newsletters and campaigns are marketing mail and need unsubscribe handling, consent and list management; this API is built for the first kind. Keep the two on separate addresses so a complaint about a newsletter never hurts your password-reset mail.
Got an idea? Find the perfect domain for it
Our AI domain name generator suggests available names across hundreds of extensions. Free SSL, DNS and Whois privacy included.
Before you send
- A mail domain: an email hosting plan for your domain. You send from one of its mailboxes or aliases, for example
no-reply@example.com. - SPF and DKIM: receiving servers check them. If the domain uses NameOcean DNS, the records are written for you; otherwise
GET /api/v1/email/domains/example.com/dns-checklists what to publish and whether it is live. - An API key with the
email:sendpermission, created at Dashboard → API. Keep it on the server, never in a browser or mobile app.
Send a message
curl
curl -X POST https://nameocean.net/api/v1/email/send \
-H "Authorization: Bearer $NAMEOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: welcome-user-42" \
-d '{
"from": "Example App <hello@example.com>",
"to": ["alice@example.org"],
"subject": "Welcome to Example App",
"text": "Thanks for signing up. Your account is ready.",
"html": "<p>Thanks for signing up. Your account is ready.</p>",
"metadata": {"user_id": "42"}
}'
The answer lists each recipient with an id and its first status, queued:
{"message_id": "<kVTha0Q…@example.com>", "from": "hello@example.com",
"subject": "Welcome to Example App",
"recipients": [{"id": "6f1c2a4e-…", "to": "alice@example.org", "kind": "to", "status": "queued"}]}
Python
import os
import uuid
import requests
resp = requests.post(
"https://nameocean.net/api/v1/email/send",
headers={
"Authorization": f"Bearer {os.environ['NAMEOCEAN_API_KEY']}",
# the same key on a retry never sends the message twice
"Idempotency-Key": f"password-reset-{uuid.uuid4()}",
},
json={
"from": "Example App <no-reply@example.com>",
"to": ["alice@example.org"],
"subject": "Reset your password",
"text": "Open this link to choose a new password: https://app.example.com/reset/abc123",
"html": '<p><a href="https://app.example.com/reset/abc123">Choose a new password</a></p>',
"metadata": {"user_id": "42", "kind": "password-reset"},
},
timeout=30,
)
resp.raise_for_status()
for r in resp.json()["recipients"]:
print(r["to"], r["status"], r["id"])
Node.js
import { readFile } from "node:fs/promises";
const pdfBase64 = (await readFile("invoice-1042.pdf")).toString("base64");
const resp = await fetch("https://nameocean.net/api/v1/email/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NAMEOCEAN_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "order-1042-shipped",
},
body: JSON.stringify({
from: "Shop <orders@example.com>",
to: ["alice@example.org"],
subject: "Your order 1042 has shipped",
text: "Your order is on its way. Track it at https://shop.example.com/orders/1042",
html: '<p>Your order is on its way. <a href="https://shop.example.com/orders/1042">Track it</a></p>',
attachments: [{ filename: "invoice-1042.pdf", content_type: "application/pdf", content_base64: pdfBase64 }],
metadata: { order: "1042" },
}),
});
if (!resp.ok) throw new Error(`${resp.status}: ${(await resp.json()).error}`);
console.log(await resp.json());
What you can send
| Field | What it does |
|---|---|
from | A mailbox or alias of your mail domain, with an optional name: "Shop <orders@example.com>". |
to, cc, bcc | One address or a list; up to 50 recipients in total per message. |
reply_to | Where replies go, for example your support address. |
subject, text, html | Send text, html or both. Sending both helps deliverability. |
attachments | A list of filename, content_type and content_base64; add content_id to use one as an inline cid: image. The whole message can be up to 10 MB. |
headers | Extra headers such as List-Unsubscribe, or In-Reply-To and References to keep a reply in its thread. From, Subject and the other headers that have their own field can't be set here. |
metadata | Up to 10 string keys you choose (an order id, a user id); they come back in every event and in the log. |
send_at | An ISO 8601 time with a timezone, up to 7 days ahead, to schedule the message. |
Retries without duplicates
Networks fail after a request has already been accepted. Send an Idempotency-Key header with something unique to the message, such as order-1042-shipped: if you retry with the same key and the same body within 24 hours, you get the first answer back (with Idempotent-Replayed: true) and the customer gets one email, not two.
Delivery events
Add a webhook endpoint subscribed to email.* and you are told what happened to each recipient:
email.delivered: the receiving server accepted it.email.delayed: a temporary failure; delivery is still being retried.email.bounced: a permanent failure, with the SMTP code and error. A hard bounce also adds the address to the domain's suppression list.email.suppressed: not sent, because the address is on the suppression list.
X-NameOcean-Event: email.bounced
{
"id": "evt_…",
"type": "email.bounced",
"created": "2026-11-12T09:21:04+00:00",
"data": {
"domain": "example.com",
"email": {
"id": "6f1c2a4e-…", "message_id": "<kVTha0Q…@example.com>",
"from": "hello@example.com", "to": "alice@example.org",
"subject": "Welcome to Example App", "status": "failed",
"smtp_code": 550, "smtp_enhanced_code": "5.1.1",
"error": "mailbox unavailable", "metadata": {"user_id": "42"}
},
"suppressed": true
}
}
Each event carries your metadata, so you can mark the order or user in your own database without a lookup. The same history, for the last 30 days, is at GET /api/v1/email/sent.
Limits
- Monthly: 1,000 messages for each user your plan allows (5,000 on the 5-user plan, 10,000 on the 10-user plan). Each recipient counts once.
GET /api/v1/email/domains/example.comshows what is used. - New domains: 100 messages a day for the first 14 days of a mail domain, while its sending reputation builds up.
- Per message: 50 recipients and 10 MB.
- Suppression list: addresses that hard-bounced aren't mailed again. Remove one with
DELETE, or add one yourself (an unsubscribe) withPOST /api/v1/email/domains/example.com/suppressions.
SMTP instead of HTTP
If a framework or tool only speaks SMTP (Django's email backend, Rails Action Mailer, WordPress, a monitoring tool), use the mailbox's address and password on mail.nameocean.net, port 465 (TLS). The HTTP API is the better fit for application code: idempotency keys, metadata, scheduling and per-recipient events.
FAQ
Do I need a separate email sending service?
Not for transactional mail from your own domain: sending is part of the email hosting plan, next to the team's mailboxes, at no extra cost up to the monthly limit.
Can I receive email in my app too?
Yes. An address can post each mail it receives to your app as JSON; see Inbound Email Webhook: Receive Email as JSON.
Why did my message go to spam?
Check SPF, DKIM and DMARC with the DNS check, send both a text and an HTML part, use a real subject and a from address people recognise, and don't send marketing mail from the address you use for password resets.
Can my AI assistant send email for me?
Yes, through our MCP server's send_email tool, with a key that has email:send. The tool tells the assistant to show you the sender, recipients, subject and body first and to send only after you say yes.
Ready to send? See the Email API documentation and the email hosting plans, or find a domain below to start with.
Got an idea? Find the perfect domain for it
Our AI domain name generator suggests available names across hundreds of extensions. Free SSL, DNS and Whois privacy included.
Read in other languages: