Introduction
The Utobo Mail API lets you send transactional and marketing emails from any app, script, or automation tool. It is a simple HTTP REST API — you send a JSON body, you get back a JSON response with an email ID.
Utobo Mail is built on AWS SES, which means industry-leading deliverability at a fraction of the cost of competitors like Mailchimp or Resend.
Base URL
httphttps://api.mail.utobo.com/v1ℹ Note
All requests must use HTTPS. HTTP requests are rejected.Response format
All responses are JSON. Successful responses return a 200 status code. Errors return an appropriate 4xx or 5xx code with an error field explaining what went wrong.
json// Success
{ "id": "3c9b0f4e-1a2b-...", "message": "Email sent." }
// Error
{ "error": "The 'from' domain is not verified." }Quick start (5 minutes)
From zero to sending your first email in 5 steps:
Create your Utobo Mail account
Sign up at mail.utobo.com. Your organization is created automatically.
Sign up →Verify a sending domain
Go to Account → Domains and add your domain (e.g. yourcompany.com). Add the TXT/CNAME records shown in your DNS provider. This usually takes under 5 minutes.
Create an API key
Go to Account → API Keys and click Create API key. Copy the key — it's shown only once.
Install the SDK (optional)
Use the official Node.js SDK for the best experience, or call the API directly with any HTTP client.
npm install utobo-mailSend your first email
Call the API with your key and verified domain:
import { UtoboMail } from 'utobo-mail';
const client = new UtoboMail('av_live_your_api_key_here');
const { data, error } = await client.emails.send({
from: 'Hello <hello@yourverifieddomain.com>',
to: 'customer@example.com',
subject: 'Welcome!',
html: '<p>Thanks for signing up.</p>',
});
console.log(data?.id); // "3c9b0f4e-..."Authentication
Every API request must include your API key in the Authorization header as a Bearer token.
httpAuthorization: Bearer av_live_your_api_key_hereAPI key format
All keys start with av_live_ followed by 64 hexadecimal characters (32 random bytes). Keys are shown once on creation — store them immediately in your environment variables.
bash# Store your key as an environment variable (never hardcode it)
export UTOBO_MAIL_API_KEY="av_live_a1b2c3d4e5f6..."Key permissions
| Permission | What it can do | Recommended for |
|---|---|---|
| Send only | Send emails, retrieve email status | Production apps |
| Full access | Send + manage contacts, lists, domains | Admin scripts, internal tools |
⚠ Warning
Never expose your API key in client-side browser code, public GitHub repos, or anywhere else that is publicly accessible. Use environment variables or a secrets manager.Send your first email
A minimal working example across all supported languages:
Node.js (SDK)
javascriptimport { UtoboMail } from 'utobo-mail';
const client = new UtoboMail(process.env.UTOBO_MAIL_API_KEY);
const { data, error } = await client.emails.send({
from: 'Acme <noreply@acme.com>',
to: 'customer@example.com',
subject: 'Welcome to Acme!',
html: '<h1>Welcome!</h1><p>Thanks for signing up.</p>',
});
if (error) {
console.error('Send failed:', error.message);
} else {
console.log('Email ID:', data.id);
}cURL
bashcurl -X POST https://api.mail.utobo.com/v1/send-email \
-H "Authorization: Bearer $UTOBO_MAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <noreply@acme.com>",
"to": ["customer@example.com"],
"subject": "Welcome to Acme!",
"html": "<h1>Welcome!</h1><p>Thanks for signing up.</p>"
}'A successful request returns HTTP 200 with a JSON body containing the email id. Use this ID to retrieve the delivery status later.
json{ "id": "3c9b0f4e-1a2b-4c3d-8e5f-6a7b8c9d0e1f", "message": "Email sent." }Send email
/v1/send-emailSends a transactional or marketing email to one or more recipients.
Request parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| from | string | ✓ | Sender address. Use 'Name <email>' format for display name. Must be from a verified domain. |
| to | string | array | ✓ | Recipient address(es). Maximum 50 addresses total across to, cc, and bcc. |
| subject | string | ✓ | Email subject line. |
| html | string | — | HTML body of the email. Required if text is not provided. |
| text | string | — | Plain-text body. Required if html is not provided. Used as fallback by email clients. |
| reply_to | string | array | — | Reply-to address(es). Recipients will reply to this address instead of from. |
| cc | string | array | — | CC recipients. Counts toward the 50-recipient limit. |
| bcc | string | array | — | BCC recipients. Counts toward the 50-recipient limit. Invisible to other recipients. |
| headers | object | — | Custom email headers as key-value pairs. e.g. { "X-Order-ID": "12345" } |
Example request
jsonPOST /v1/send-email
Authorization: Bearer av_live_your_api_key_here
Content-Type: application/json
{
"from": "Acme Support <support@acme.com>",
"to": ["alice@example.com", "bob@example.com"],
"cc": "manager@acme.com",
"subject": "Your order #4521 has shipped",
"html": "<p>Your order is on the way! Track it at <a href='...'>acme.com/track</a></p>",
"text": "Your order is on the way! Track it at acme.com/track",
"reply_to": "orders@acme.com",
"headers": { "X-Order-ID": "4521" }
}Success response — HTTP 200
| Field | Type | Description |
|---|---|---|
| id | string | Unique email ID (UUID). Use with GET /v1/send-email/:id to retrieve status. |
| message | string | Confirmation message. Always "Email sent." |
json{ "id": "3c9b0f4e-1a2b-4c3d-8e5f-6a7b8c9d0e1f", "message": "Email sent." }✦ Tip
The returnedid is a UUID. Save it to your database to track delivery status per-email without polling. Use GET /v1/send-email/:id to retrieve status on demand.Get email status
/v1/send-email/:idRetrieve the delivery status of a previously sent email by its ID. The ID is the UUID returned by the Send email endpoint.
ℹ Note
You can only retrieve emails sent by your own organization. Requests for another organization's email IDs return 404.Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string (UUID) | ✓ | The email ID returned by POST /v1/send-email. |
Example request
bashcurl https://api.mail.utobo.com/v1/send-email/3c9b0f4e-1a2b-4c3d-8e5f-6a7b8c9d0e1f \
-H "Authorization: Bearer $UTOBO_MAIL_API_KEY"Success response — HTTP 200
| Field | Type | Description |
|---|---|---|
| id | string | The email ID. |
| from | string | Sender address as passed in the request. |
| to | string[] | List of recipient addresses. |
| subject | string | Email subject. |
| status | string | "sent" if successfully delivered to SES, "failed" if an error occurred. |
| ses_message_id | string|null | AWS SES internal message ID. Useful for support tickets. May be null on failure. |
| created_at | string | ISO 8601 timestamp of when the email was sent. |
json{
"id": "3c9b0f4e-1a2b-4c3d-8e5f-6a7b8c9d0e1f",
"from": "Acme Support <support@acme.com>",
"to": ["alice@example.com"],
"subject": "Your order has shipped",
"status": "sent",
"ses_message_id": "0100019...@us-east-1.amazonses.com",
"created_at": "2026-05-29T10:30:00.000Z"
}Rate limits
Rate limits are applied per 1-minute sliding window. All limits are enforced server-side and cannot be increased without contacting support.
| Scope | Limit | Notes |
|---|---|---|
| Per API key | 100 req/min | Applies to each individual key |
| Per organization | 500 req/min | Shared across all keys in the org |
| Per IP (no auth) | 10 req/min | Prevents brute-force key enumeration |
Rate limit response
When a limit is exceeded, the API returns HTTP 429 with retry guidance:
jsonHTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Resource: key
{ "error": "API key rate limit exceeded.", "retry_after": 60 }Best practices
Error codes
All error responses include an error string with a human-readable message.
| Code | Status | Common cause |
|---|---|---|
| 400 | Bad Request | Missing required field (from, to, subject, html/text), invalid JSON, or malformed email address. |
| 401 | Unauthorized | API key is missing, invalid, expired, or has been revoked. |
| 403 | Forbidden | API key does not have the required permission (e.g. using a 'send-only' key for a manage operation). |
| 404 | Not Found | Email ID not found, or belongs to a different organization. |
| 405 | Method Not Allowed | Wrong HTTP method used (e.g. GET instead of POST for send). |
| 422 | Unprocessable Entity | The 'from' domain has not been verified. Verify it in Account → Domains. |
| 429 | Too Many Requests | Rate limit exceeded. Check the Retry-After header. |
| 500 | Internal Server Error | Something went wrong on our end. Retrying after 5–10 seconds usually resolves transient issues. |
Error handling example (Node.js SDK)
javascriptconst { data, error } = await client.emails.send({ ... });
if (error) {
switch (error.statusCode) {
case 401:
console.error('Invalid API key — check your UTOBO_MAIL_API_KEY env var');
break;
case 422:
console.error('Domain not verified — go to Account → Domains');
break;
case 429:
console.error('Rate limited — retry after 60 seconds');
break;
default:
console.error('Send failed:', error.message);
}
return;
}
console.log('Sent:', data.id);Node.js SDK
The official utobo-mail npm package is the recommended way to interact with the API. It provides full TypeScript types, the { data, error } response pattern (no try/catch needed), and automatic react-email support.
Installation
bashnpm install utobo-mail # or yarn add utobo-mail # or pnpm add utobo-mailInitialization
typescriptimport { UtoboMail } from 'utobo-mail';
// Option 1: Pass key directly
const client = new UtoboMail('av_live_your_api_key');
// Option 2: Use environment variable (recommended)
// Set UTOBO_MAIL_API_KEY in your .env file
const client = new UtoboMail(); // reads process.env.UTOBO_MAIL_API_KEY
// Option 3: Custom base URL (advanced)
const client = new UtoboMail(process.env.UTOBO_MAIL_API_KEY, {
baseUrl: 'https://api.mail.utobo.com/v1',
});Send an email
typescriptconst { data, error } = await client.emails.send({
from: 'Acme <noreply@acme.com>',
to: 'user@example.com', // or ['a@b.com', 'c@d.com']
subject: 'Welcome to Acme!',
html: '<p>Thanks for signing up.</p>',
// Optional:
text: 'Thanks for signing up.', // Plain text fallback
cc: 'team@acme.com',
bcc: 'archive@acme.com',
reply_to: 'support@acme.com',
headers: { 'X-User-ID': '12345' },
});
if (error) console.error(error.message);
else console.log('Email ID:', data.id);Retrieve email status
typescriptconst { data, error } = await client.emails.get('3c9b0f4e-...');
if (!error) {
console.log(data.status); // "sent" | "failed"
console.log(data.created_at); // ISO timestamp
}With react-email
Install @react-email/render as an optional peer dependency, then pass a React component directly:
bashnpm install @react-email/render react react-domtypescriptimport { WelcomeEmail } from './emails/WelcomeEmail';
const { data, error } = await client.emails.send({
from: 'Acme <noreply@acme.com>',
to: 'user@example.com',
subject: 'Welcome!',
react: <WelcomeEmail name="Alice" plan="Starter" />,
// html is auto-generated from the React component
});TypeScript types
The SDK ships with full TypeScript declarations. All request and response shapes are exported from the package root:
typescriptimport type {
CreateEmailOptions,
CreateEmailResponseSuccess,
GetEmailResponseSuccess,
ErrorResponse,
Response,
} from 'utobo-mail';✦ Tip
TheResponse<T> type is a discriminated union: either { data: T, error: null } or { data: null, error: ErrorResponse }. This means TypeScript enforces that you check for errors before accessing data.cURL
Use cURL for quick tests, shell scripts, or CI pipelines.
Send email
bashcurl -X POST https://api.mail.utobo.com/v1/send-email \
-H "Authorization: Bearer $UTOBO_MAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <noreply@acme.com>",
"to": ["user@example.com"],
"subject": "Hello from Utobo Mail!",
"html": "<p>Your message here.</p>"
}'Get email status
bashcurl https://api.mail.utobo.com/v1/send-email/3c9b0f4e-1a2b-... \
-H "Authorization: Bearer $UTOBO_MAIL_API_KEY"Send with attachments
bash# Encode file as base64
B64=$(base64 -i invoice.pdf | tr -d '\n')
curl -X POST https://api.mail.utobo.com/v1/send-email \
-H "Authorization: Bearer $UTOBO_MAIL_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"from\": \"Billing <billing@acme.com>\",
\"to\": [\"customer@example.com\"],
\"subject\": \"Your invoice\",
\"html\": \"<p>Please find your invoice attached.</p>\",
\"attachments\": [{
\"filename\": \"invoice.pdf\",
\"content\": \"$B64\",
\"contentType\": \"application/pdf\"
}]
}"Python
Use the requests library or httpx for async support.
Installation
bashpip install requestsSend email
pythonimport os
import requests
UTOBO_MAIL_API_KEY = os.environ["UTOBO_MAIL_API_KEY"]
response = requests.post(
"https://api.mail.utobo.com/v1/send-email",
headers={
"Authorization": f"Bearer {UTOBO_MAIL_API_KEY}",
"Content-Type": "application/json",
},
json={
"from": "Acme <noreply@acme.com>",
"to": ["user@example.com"],
"subject": "Welcome to Acme!",
"html": "<p>Thanks for joining us.</p>",
},
)
data = response.json()
if response.status_code == 200:
print(f"Sent! Email ID: {data['id']}")
else:
print(f"Error {response.status_code}: {data.get('error')}")Async with httpx
pythonimport asyncio
import os
import httpx
async def send_email():
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.mail.utobo.com/v1/send-email",
headers={"Authorization": f"Bearer {os.environ['UTOBO_MAIL_API_KEY']}"},
json={
"from": "Acme <noreply@acme.com>",
"to": ["user@example.com"],
"subject": "Async email!",
"html": "<p>Sent asynchronously.</p>",
},
)
return response.json()
asyncio.run(send_email())PHP
Works with native cURL functions or the Guzzle HTTP client.
With Guzzle (recommended)
bashcomposer require guzzlehttp/guzzlephp<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client();
$response = $client->post('https://api.mail.utobo.com/v1/send-email', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('UTOBO_MAIL_API_KEY'),
'Content-Type' => 'application/json',
],
'json' => [
'from' => 'Acme <noreply@acme.com>',
'to' => ['user@example.com'],
'subject' => 'Welcome!',
'html' => '<p>Thanks for joining.</p>',
],
]);
$data = json_decode($response->getBody(), true);
echo 'Email ID: ' . $data['id'];With native cURL
php<?php
$payload = json_encode([
'from' => 'Acme <noreply@acme.com>',
'to' => ['user@example.com'],
'subject' => 'Welcome!',
'html' => '<p>Thanks for joining.</p>',
]);
$ch = curl_init('https://api.mail.utobo.com/v1/send-email');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('UTOBO_MAIL_API_KEY'),
'Content-Type: application/json',
],
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
echo 'Email ID: ' . $data['id'];Verify your domain
Before you can send emails, you must verify that you own the sending domain. This protects your recipients and ensures your emails reach the inbox, not spam.
Go to Account → Domains
In your Utobo Mail dashboard, navigate to Account → Domains and click Add domain.
Enter your domain
Type your domain name (e.g. yourcompany.com). Do not include http:// or a subdomain.
Add DNS records
Utobo Mail will show you TXT and CNAME records to add in your DNS provider (Cloudflare, Route53, Namecheap, etc.). Add all records shown.
Wait for propagation
DNS changes usually propagate within 5–15 minutes. Some providers take up to 48 hours. Click Check DNS in the dashboard to verify.
Domain is verified
Once all records are confirmed, the domain status changes to Verified. You can now send from any address at that domain.
ℹ Note
You only need to verify the root domain (e.g. yourcompany.com) once. You can then send from any address at that domain: noreply@, support@, hello@, etc.Common DNS providers
Cloudflare
DNS → Add record
AWS Route53
Hosted zones → Record sets
Namecheap
Domain list → Advanced DNS
GoDaddy
DNS Management
Google Domains
DNS → Manage custom records
Porkbun
DNS Records
API key best practices
Use one key per environment
Create separate keys for Development, Staging, and Production. This way you can rotate a compromised key without affecting other environments.
Store keys in environment variables
Never hardcode your API key in source code. Use environment variables (.env for local, platform secrets for production).
UTOBO_MAIL_API_KEY=av_live_your_key_hereUse least-privilege permissions
Choose 'Send only' permission for keys used in production apps. Only use 'Full access' for admin scripts that need to manage contacts or domains.
Rotate keys periodically
Create a new key, update your app's environment, verify it works, then revoke the old key. This is good security hygiene.
Revoke immediately if compromised
If a key leaks (e.g. accidentally committed to git), revoke it immediately in Account → API Keys. All requests using that key will stop working instantly.
Never use keys in browser code
API keys must only be used server-side. If you need to send email from a client-side app, create a server endpoint that calls the Utobo Mail API, and call that endpoint from your client.
Handle bounces & delivery errors
Email bounces happen when an email cannot be delivered. There are two types:
Hard bounce
Permanent delivery failure. The email address doesn't exist or the domain is invalid. Remove these addresses immediately — continuing to send harms your deliverability.
Soft bounce
Temporary failure. The mailbox is full or the server is temporarily unavailable. You can retry after 24–48 hours. After 3–5 soft bounces, treat as a hard bounce.
Viewing bounces in the dashboard
Go to Delivery Issues in your dashboard sidebar. All bounces and complaints are logged there with the email address, bounce type, and timestamp.
Best practices
React Email templates
The Utobo Mail Node.js SDK supports @react-email/render for building beautiful, cross-client email templates in JSX instead of raw HTML.
Setup
bashnpm install utobo-mail @react-email/render react react-dom npm install -D @react-email/componentsCreate a template component
typescript// emails/WelcomeEmail.tsx
import { Html, Head, Body, Container, Text, Button } from '@react-email/components';
interface WelcomeEmailProps {
name: string;
loginUrl: string;
}
export function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Body style={{ fontFamily: 'sans-serif', backgroundColor: '#f4f4f4' }}>
<Container style={{ maxWidth: '600px', margin: '0 auto', padding: '24px' }}>
<Text style={{ fontSize: '24px', fontWeight: 'bold' }}>
Welcome, {name}!
</Text>
<Text style={{ color: '#555' }}>
Your account is ready. Click below to sign in.
</Text>
<Button
href={loginUrl}
style={{ background: '#4f46e5', color: '#fff', padding: '12px 24px', borderRadius: '6px' }}
>
Sign in to your account
</Button>
</Container>
</Body>
</Html>
);
}Send with the template
typescriptimport { UtoboMail } from 'utobo-mail';
import { WelcomeEmail } from './emails/WelcomeEmail';
const client = new UtoboMail(process.env.UTOBO_MAIL_API_KEY);
const { data, error } = await client.emails.send({
from: 'Acme <noreply@acme.com>',
to: 'alice@example.com',
subject: 'Welcome to Acme!',
// The SDK renders the React component to HTML automatically
react: <WelcomeEmail name="Alice" loginUrl="https://acme.com/login" />,
});✦ Tip
React Email components work in Next.js, Remix, Express, and any Node.js runtime. They also render correctly in all major email clients including Outlook, Gmail, and Apple Mail.Ready to start sending?
Create your account and start sending in minutes. Plans from $9/mo.