API Documentation
Build on ZoroMail with a free public REST API. No developer account, no API key — just create a mailbox, grab a token, and read email.
https://zoromail.com/api/v1
Overview
The ZoroMail API is a free public REST API. There is no developer registration, no API key and no approval step: create a mailbox, exchange its password for a bearer token, and read mail — the same mailboxes and messages the website uses.
- Base URL:
https://DOMAIN/api/v1 - Format: JSON everywhere,
Content-Type: application/json. - Dates: ISO 8601 UTC, e.g.
2026-10-20T02:30:00+00:00. - IDs are returned as strings.
- CORS is open (
*), so browser apps can call the API directly. Authentication always uses the Bearer token — never cookies. - Method override: if a firewall blocks
DELETE/PATCH, sendPOSTwith anX-HTTP-Method-Override: DELETEheader or a?_method=DELETEquery parameter instead.
Pretty URLs need a 3-line nginx rule (CloudPanel: Sites → domain → Vhost)
Without it, the API still works via /api/index.php/v1/… or /api/?route=/v1/…. Add this for clean /api/v1/… URLs:
location /api/ {
try_files $uri $uri/ /api/index.php?route=$uri&$args;
}Quickstart
- Pick a domain from
GET /domains. - Create a mailbox with
POST /accounts(address + password). - Trade the password for a token at
POST /token. - Call mailbox endpoints with
Authorization: Bearer <token>.
# 1. List domains
curl https://zoromail.com/api/v1/domains
# 2. Create a mailbox
curl -X POST https://zoromail.com/api/v1/accounts \
-H "Content-Type: application/json" \
-d '{"address":"demo@example.com","password":"secret123"}'
# 3. Get a token
TOKEN=$(curl -s -X POST https://zoromail.com/api/v1/token \
-H "Content-Type: application/json" \
-d '{"address":"demo@example.com","password":"secret123"}' \
| python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")
# 4. Read messages
curl https://zoromail.com/api/v1/messages \
-H "Authorization: Bearer $TOKEN"
$base = 'https://zoromail.com/api/v1';
function api($method, $path, $body = null, $token = null) {
$ch = curl_init($base . $path);
$headers = ['Content-Type: application/json'];
if ($token) $headers[] = 'Authorization: Bearer ' . $token;
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$res = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
return [$code, json_decode($res, true)];
}
// Create a mailbox, then trade password for token:
[, $acct] = api('POST', '/accounts',
['address' => 'demo@example.com', 'password' => 'secret123']);
[, $tok] = api('POST', '/token',
['address' => 'demo@example.com', 'password' => 'secret123']);
$token = $tok['token'];
// Read the inbox:
[, $inbox] = api('GET', '/messages', null, $token);
foreach ($inbox['messages'] as $m) {
echo $m['subject'], "\n";
}
const base = 'https://zoromail.com/api/v1';
async function api(path, { method = 'GET', body, token } = {}) {
const res = await fetch(base + path, {
method,
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: 'Bearer ' + token } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) throw new Error(await res.text());
return res.status === 204 ? null : res.json();
}
const address = 'demo@example.com';
const password = 'secret123';
await api('/accounts', { method: 'POST', body: { address, password } });
const { token } = await api('/token', { method: 'POST', body: { address, password } });
const inbox = await api('/messages', { token });
console.log(inbox.messages.map(m => m.subject));
import requests
base = 'https://zoromail.com/api/v1'
address, password = 'demo@example.com', 'secret123'
# Create a mailbox, then trade password for token
requests.post(f'{base}/accounts', json={'address': address, 'password': password})
token = requests.post(f'{base}/token',
json={'address': address, 'password': password}).json()['token']
# Read the inbox
headers = {'Authorization': f'Bearer {token}'}
inbox = requests.get(f'{base}/messages', headers=headers).json()
for m in inbox['messages']:
print(m['subject'])
Authentication
Mailbox endpoints require the mailbox token in the Authorization header:
Authorization: Bearer 9f2c…64 hex chars…Get a token with POST /token using the address + password you chose at creation. The token is a 64-character secret with the same power as the website login — keep it private. It stops working when the mailbox expires or is deleted.
Missing or invalid tokens return 401 {"error": "...", "code": "unauthorized"}. A token only ever grants access to its own mailbox.
Domains
/domainsList domains that can receive mail.[
{ "id": "1", "domain": "example.com", "isActive": true },
{ "id": "2", "domain": "mail.example.net", "isActive": true }
]Accounts
/accountsCreate a mailbox. No signup.Body: {"address": "name@example.com", "password": "secret123"} — the domain must be listed by GET /domains; the name follows the same rules as custom website addresses. Password: 8–72 chars (normalized to lowercase letters+digits, exactly like website logins, so the mailbox also works in the web UI).
{
"id": "42",
"address": "name@example.com",
"createdAt": "2026-10-20T02:30:00+00:00",
"expiresAt": "2026-10-21T02:30:00+00:00"
}/tokenTrade address + password for a bearer token.{ "id": "42", "token": "9f2c…64 hex chars…" }/meThe authenticated mailbox. 🔒/accounts/{id}Mailbox info; id must be your own. 🔒/accounts/{id}Delete your mailbox and all its mail. 🔒 → 204🔒 = requires Authorization: Bearer <token>.
Messages
/messages?page=1&limit=30Paginated inbox, newest first. 🔒{
"messages": [
{
"id": "101",
"accountId": "42",
"from": { "name": "Acme", "address": "noreply@acme.test" },
"subject": "Your code is 482910",
"intro": "Your verification code is…",
"seen": false,
"hasAttachments": true,
"size": 1240,
"receivedAt": "2026-10-20T02:35:10+00:00"
}
],
"page": 1, "limit": 30, "total": 1
}/messages/{id}Full message: text, sanitized HTML, attachments. 🔒/messages/{id}Body {"seen": true} marks read/unread. 🔒/messages/{id}Delete one message. 🔒 → 204Reading a message via the website marks it read automatically; via the API it does not — use PATCH when you want that.
Attachments
Message detail includes an attachments array. Each entry carries a downloadUrl — a signed link bound to your mailbox token:
{
"id": "7",
"filename": "invoice.pdf",
"contentType": "application/pdf",
"size": 48210,
"downloadUrl": "https://DOMAIN/download.php?token=9f2c…&id=7"
}Files stream as Content-Disposition: attachment and are checked against your mailbox on every request.
Errors
All errors use HTTP status codes and a consistent envelope:
{ "error": "Invalid address or password.", "code": "invalid_credentials" }| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_address / invalid_domain / weak_password / invalid_body | Request validation failed. |
| 401 | unauthorized / invalid_credentials | Missing/bad token or wrong password. |
| 404 | not_found | Unknown endpoint, message, or someone else's resource. |
| 405 | method_not_allowed | Wrong HTTP method for the endpoint. |
| 409 | address_taken | The address is already in use. |
| 413 | body_too_large | Request body over 64 KiB. |
| 429 | rate_limited | Slow down and retry later. |
Rate limits
Limits are per IP address, per hour. They are generous for normal use and block abuse:
| Endpoint | Limit |
|---|---|
GET /domains | 120 / hour |
POST /accounts, DELETE /accounts/{id} | 20 / hour |
POST /token | 30 / hour |
GET /messages… | 300 / hour |
PATCH / DELETE /messages/{id} | 120 / hour |
OpenAPI
A machine-readable OpenAPI 3.0 description is available for import into Postman, Insomnia or code generators:
https://zoromail.com/api-docs/openapi.json