Trainmail
API status

Email infrastructure, without the noise.

Build reliable server-side integrations on top of a Trainmail mailbox. Read, search, organize, and send mail through a clear API designed around security and deliberate access.

Start building
Manual approval · Scoped keys · Predictable limits · Audit trail
Base URLhttps://trainmail.online/api/v1
ProtocolJSON over HTTPS
AuthenticationBearer API key
Default limit60 requests / minute

Built for trusted integrations

The Trainmail API is a server-to-server API. Every key is bound to one primary Trainmail mailbox and approved manually by support. A provisioning key may also access only the mailboxes created by that same key—never arbitrary Trainmail accounts.

API v1 is live
Core mailbox, folder, message, search, organization, sending, and controlled provisioning endpoints are available under a stable, versioned contract.

Request access

Tell us what you are building, why mailbox access is required, which permissions you need, and the expected request volume. Support reviews every application before issuing a key.

What makes a good application

Use a Trainmail address you control. Describe the concrete workflow, how you will store the secret, and whether the integration reads mail, changes mailbox state, or sends messages.

Authentication

Send the API key in the Authorization header on every protected request. Keys use the tm_live_ prefix and are shown only once when issued.

HTTP
Authorization: Bearer tm_live_your_key
Content-Type: application/json
Never expose a key in browser code
Do not put API keys in JavaScript bundles, mobile applications, URLs, analytics events, or public repositories. Keep them in a server-side environment variable or secret manager.

Scopes

Support grants only the permissions your integration needs. Requests outside the approved scope return 403 insufficient_scope.

account:readRead mailbox identity and active key scopes.
mail:readList folders, search mail, and retrieve messages.
mail:writeMark messages, move them, archive, and delete.
mail:sendSend email from the bound Trainmail mailbox.
mailbox:createCreate a limited number of new Trainmail mailboxes for an approved server-side workflow.

Quickstart

Start with the account endpoint to verify authentication and inspect the granted scopes.

cURL
curl https://trainmail.online/api/v1/account \
  -H "Authorization: Bearer $TRAINMAIL_API_KEY"

Then fetch the newest inbox messages. Lists use cursor pagination and return at most 50 items per request.

JavaScript · Node.js
const response = await fetch(
  "https://trainmail.online/api/v1/messages?folder=inbox&limit=20",
  { headers: { Authorization: `Bearer ${process.env.TRAINMAIL_API_KEY}` } }
);

const page = await response.json();
console.log(page.data, page.pagination.next_cursor);

Create mailboxes

Keys approved for provisioning can create one real @trainmail.online mailbox per request. The password is generated by Trainmail and returned only in the first successful response, so store it immediately in a secure server-side secret store. You can attach private integration metadata and optionally copy every incoming message to an owned Trainmail collector mailbox.

Per-key provisioning allowance
The default allowance is 3 mailboxes per day and 10 mailboxes in total, but approved keys can have custom limits. The current limits and remaining allowance are returned in every successful provisioning response. All creation is audited.

Create one mailbox

cURL
curl -X POST https://trainmail.online/api/v1/mailboxes \
  -H "Authorization: Bearer $TRAINMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: project-user-001" \
  -d '{"username":"project-user-001","metadata":{"external_id":"user-001","batch":"august"},"forward_to":"collector@trainmail.online"}'

Omit username to receive a generated address. Reusing the same Idempotency-Key never creates a second mailbox; a replay returns the address without returning the password again. metadata is a private JSON object up to 4 KB. forward_to must be the key's primary mailbox or another mailbox created by the same key. Forwarding uses copy mode, so the original message remains in the source inbox.

Small Node.js provisioning script

JavaScript · Node.js 18+
import { randomUUID } from "node:crypto";

const usernames = ["project-one", "project-two"];

for (const username of usernames) {
  const response = await fetch("https://trainmail.online/api/v1/mailboxes", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.TRAINMAIL_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": randomUUID()
    },
    body: JSON.stringify({ username })
  });

  const mailbox = await response.json();
  if (!response.ok) throw new Error(JSON.stringify(mailbox));
  console.log(mailbox.email, mailbox.temporary_password);
}

Manage many mailboxes

A key with mailbox:create can list every mailbox it created. A key with mail:read can then read any of those mailboxes by adding the mailbox query parameter. No mailbox password is sent to the API.

List owned mailboxes
curl "https://trainmail.online/api/v1/mailboxes?limit=100" \
  -H "Authorization: Bearer $TRAINMAIL_API_KEY"
Read a created mailbox
curl "https://trainmail.online/api/v1/messages?mailbox=project-user-001%40trainmail.online&folder=inbox&limit=20" \
  -H "Authorization: Bearer $TRAINMAIL_API_KEY"

The same selector works for /folders, message details, read state, move, and delete endpoints. You may use X-Trainmail-Mailbox instead of the query parameter. If both are present, they must match.

Update metadata or forwarding

PATCH
curl -X PATCH "https://trainmail.online/api/v1/mailboxes/project-user-001%40trainmail.online" \
  -H "Authorization: Bearer $TRAINMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"metadata":{"external_id":"user-001","status":"active"},"forward_to":"collector@trainmail.online"}'

Send "forward_to": null to disable forwarding. Trainmail rejects self-forwarding, forwarding loops, external destinations, and addresses outside the key's ownership boundary.

Safe multi-account automation
The mailbox selector is an authorization boundary, not a login shortcut. A key cannot access a mailbox merely because its address is known.

Pagination & server-side search

Message lists are ordered newest first. Pass the opaque next_cursor value back as cursor; do not parse or construct cursors yourself.

Request
GET /api/v1/messages?folder=inbox&limit=20&cursor=eyJiZWZvcmVfdWlkIjo...

Add q to search message subject, sender, and content on the mail server rather than downloading the entire mailbox.

Search
GET /api/v1/messages?folder=inbox&q=invoice&limit=20

Rate limits

Every key has an individual per-minute limit. The default is 60 requests per minute. Sending is additionally protected by a daily quota, usually 100 messages per day. Mailbox provisioning has a separate default allowance of 3 per day and 10 total. Support can approve different limits for a justified use case.

Response headers
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1785859200

When a limit is exceeded, the API responds with 429 and a Retry-After header. Use exponential backoff with jitter; never retry in a tight loop.

Endpoints

All responses are JSON. Message IDs are mailbox UIDs and must be used with the same folder supplied when reading the message.

GET/healthPublic

Returns API availability, version, and server time. No API key is required.

GET/accountaccount:read

Returns the mailbox identity and scopes attached to the current key.

GET/mailboxesmailbox:create

Lists mailboxes created by this key with their metadata, forwarding state, and cursor pagination.

limitinteger1–200. Default: 100.
cursorstringOpaque cursor returned by the previous page.
POST/mailboxesmailbox:create

Creates one Trainmail mailbox. The Idempotency-Key header is required and the generated temporary password is returned once.

usernamestringOptional 3–32 character local part. Omit it for a generated address.
metadataobjectOptional private integration data, up to 32 fields and 4 KB.
forward_tostring|nullOptional owned @trainmail.online destination. Incoming mail is copied, not moved.
Idempotency-KeyheaderRequired unique value, 8–128 visible ASCII characters.
201 response
{
  "object": "mailbox",
  "email": "project-user-001@trainmail.online",
  "temporary_password": "returned-once",
  "limits": { "remaining_today": 2, "remaining_total": 9 }
}
GET/mailboxes/{email}mailbox:create

Returns one mailbox created by this key. URL-encode the email address.

PATCH/mailboxes/{email}mailbox:create

Replaces metadata and/or changes internal forwarding. Send forward_to: null to disable forwarding.

GET/foldersmail:read

Lists supported mailbox folders that currently exist.

mailboxstringOptional primary or key-created mailbox. Default: the key's primary mailbox.
GET/messagesmail:read

Lists or searches messages without downloading full bodies.

mailboxstringOptional primary or key-created mailbox. The header X-Trainmail-Mailbox is also accepted.
folderstringinbox, sent, drafts, archive, trash, or spam. Default: inbox.
limitinteger1–50. Default: 20.
cursorstringOpaque cursor returned by the previous page.
qstringOptional server-side search, up to 120 characters.
GET/messages/{id}mail:read

Returns message metadata, text, HTML when available, and attachment metadata. Pass ?folder=inbox when needed.

PATCH/messages/{id}mail:write

Updates message state.

JSON body
{ "read": true }
POST/messages/{id}/movemail:write

Moves a message between supported folders.

JSON body
{ "from": "inbox", "to": "archive" }
DELETE/messages/{id}mail:write

Moves a message to Trash. Calling DELETE for a message already in Trash permanently removes it.

POST/sendmail:send

Sends a message from the mailbox bound to the key. The sender address cannot be overridden.

JSON body
{
  "to": ["recipient@example.com"],
  "subject": "Hello from Trainmail",
  "text": "A plain-text fallback",
  "html": "<p>A refined email.</p>"
}

Up to 25 combined To, CC, and BCC recipients are accepted. Attachments are intentionally not available in v1.

Errors

Errors use a stable machine-readable code and include a request ID for support. Internal mailbox and server details are never returned.

Error response
{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded",
    "message": "The API key rate limit has been exceeded."
  },
  "request_id": "req_..."
}
StatusMeaningAction
400Invalid requestCorrect parameters, headers, or JSON.
401Invalid or revoked keyCheck the Authorization header.
403Insufficient scope or allowanceRequest only the permission you need or contact support.
404Resource not foundVerify message ID and folder.
409ConflictUse another mailbox name or a new idempotency key.
429Rate limit exceededRespect Retry-After and back off.
503Mail service unavailableRetry later with exponential backoff.

Security model

Keys are generated from cryptographically secure random bytes and stored by Trainmail only as SHA-256 hashes. A full key is delivered once to the approved Trainmail mailbox. Every request is rate-limited and recorded without request bodies or message content.

If a key is exposed
Stop using it immediately and contact support with the key prefix. Support can revoke access without changing the mailbox password.