Skip to main content
API v1 · StableSDK v1.0.0

Developer Documentation

Everything you need to send emails programmatically with Utobo Mail. Clean REST API, official Node.js SDK, and real-time delivery logs.

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:

1

Create your Utobo Mail account

Sign up at mail.utobo.com. Your organization is created automatically.

Sign up →
2

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.

3

Create an API key

Go to Account → API Keys and click Create API key. Copy the key — it's shown only once.

4

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-mail
5

Send 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_here

API 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

PermissionWhat it can doRecommended for
Send onlySend emails, retrieve email statusProduction apps
Full accessSend + manage contacts, lists, domainsAdmin 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

POST/v1/send-email

Sends a transactional or marketing email to one or more recipients.

Request parameters

ParameterTypeRequiredDescription
fromstringSender address. Use 'Name <email>' format for display name. Must be from a verified domain.
tostring | arrayRecipient address(es). Maximum 50 addresses total across to, cc, and bcc.
subjectstringEmail subject line.
htmlstringHTML body of the email. Required if text is not provided.
textstringPlain-text body. Required if html is not provided. Used as fallback by email clients.
reply_tostring | arrayReply-to address(es). Recipients will reply to this address instead of from.
ccstring | arrayCC recipients. Counts toward the 50-recipient limit.
bccstring | arrayBCC recipients. Counts toward the 50-recipient limit. Invisible to other recipients.
headersobjectCustom 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

FieldTypeDescription
idstringUnique email ID (UUID). Use with GET /v1/send-email/:id to retrieve status.
messagestringConfirmation message. Always "Email sent."
json{ "id": "3c9b0f4e-1a2b-4c3d-8e5f-6a7b8c9d0e1f", "message": "Email sent." }

✦ Tip

The returned id 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

GET/v1/send-email/:id

Retrieve 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

ParameterTypeRequiredDescription
idstring (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

FieldTypeDescription
idstringThe email ID.
fromstringSender address as passed in the request.
tostring[]List of recipient addresses.
subjectstringEmail subject.
statusstring"sent" if successfully delivered to SES, "failed" if an error occurred.
ses_message_idstring|nullAWS SES internal message ID. Useful for support tickets. May be null on failure.
created_atstringISO 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.

ScopeLimitNotes
Per API key100 req/minApplies to each individual key
Per organization500 req/minShared across all keys in the org
Per IP (no auth)10 req/minPrevents 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

Read the Retry-After header and wait that many seconds before retrying.
Use exponential backoff for retries in production apps.
If you need higher limits, contact support@utobo.com.
For bulk sending (campaigns), use the Campaigns feature in the dashboard instead of the API.

Error codes

All error responses include an error string with a human-readable message.

CodeStatusCommon cause
400Bad RequestMissing required field (from, to, subject, html/text), invalid JSON, or malformed email address.
401UnauthorizedAPI key is missing, invalid, expired, or has been revoked.
403ForbiddenAPI key does not have the required permission (e.g. using a 'send-only' key for a manage operation).
404Not FoundEmail ID not found, or belongs to a different organization.
405Method Not AllowedWrong HTTP method used (e.g. GET instead of POST for send).
422Unprocessable EntityThe 'from' domain has not been verified. Verify it in Account → Domains.
429Too Many RequestsRate limit exceeded. Check the Retry-After header.
500Internal Server ErrorSomething 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-mail

Initialization

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-dom
typescriptimport { 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

The Response<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 requests

Send 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/guzzle
php<?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.

1

Go to Account → Domains

In your Utobo Mail dashboard, navigate to Account → Domains and click Add domain.

2

Enter your domain

Type your domain name (e.g. yourcompany.com). Do not include http:// or a subdomain.

3

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.

4

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.

5

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_here

Use 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

Remove hard-bounced addresses from your lists immediately.
Keep your bounce rate below 2% — high bounce rates trigger AWS SES sending limits.
Use double opt-in for newsletter signups to ensure addresses are valid.
Don't send to purchased or scraped email lists — they have high bounce rates.
Use Utobo Mail's built-in bounce tracking: go to Delivery Issues to see and act on bounces.

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/components

Create 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.