Developers
API reference
Everything this interface does — creating an address, watching the inbox, reading a message, downloading attachments — is a plain HTTP + JSON call you can make from your own code. The API lives under /api/v1.
Base URL
/api/v1 — the examples below use the shell variable $API for it. The same origin serves both this page and the API (the web server proxies /api to the mail server), so no CORS setup is needed.
Authentication
There is no API key. POST /api/v1/addresses returns a token and every other call carries it in the path: /api/v1/{token}/emails.
Quick start
# 1. a domain you can use
curl "$API/api/v1/domains"
# 2. create an address (keep the token)
curl -X POST "$API/api/v1/addresses" -H "Content-Type: application/json" -d '{}'
# 3. list the inbox
curl "$API/api/v1/$TOKEN/emails"
# 4. read one message (body_html is untrusted, render it sandboxed)
curl "$API/api/v1/$TOKEN/emails/$EMAIL_ID"Endpoints
- GET
/api/v1/healthService and database status - GET
/api/v1/domainsList the domains you can create addresses on - POST
/api/v1/addressesCreate a temporary address - GET
/api/v1/{token}/emailsList the inbox - GET
/api/v1/{token}/emails/{email_id}Read one message - GET
/api/v1/{token}/emails/{email_id}/rawDownload the raw message - GET
/api/v1/{token}/emails/{email_id}/attachments/{attachment_id}Download an attachment - DELETE
/api/v1/{token}/emails/{email_id}Delete a message
/api/v1/healthService and database status
status, database and the configured domains. Cached for 5 seconds.
Request
curl "$API/api/v1/health"Response
{
"status": "healthy",
"database": "connected",
"domains": ["rota.ath.cx"]
}Errors
- 503 Database unreachable (status is
unhealthy).
/api/v1/domainsList the domains you can create addresses on
domains: the domains configured on the server, in the order they are used.
Request
curl "$API/api/v1/domains"Response
{
"domains": ["rota.ath.cx"]
}/api/v1/addressesCreate a temporary address
The created address. Keep token — every other call is authenticated by it. expires_at is 24 hours by default and the year 9999 when lifetime_hours is 0 (permanent).
Parameters
usernamestring?bodyCustom local part: 3–64 characters, letters, numbers, dot, underscore or hyphen. Omitted or empty generates a random one.
domainstring?bodyOne of the values from /api/v1/domains. Defaults to the first configured domain.
lifetime_hoursinteger?bodyHow long the address lives. Omitted uses the server default (24 h),
0means it never expires (permanent address), and 1–876000 asks for that many hours.
Request
curl -X POST "$API/api/v1/addresses" \
-H "Content-Type: application/json" \
-d '{"username": "signup", "domain": "rota.ath.cx"}'Response
{
"id": "c89fbe6f-078c-4940-b2d4-4bf193c8a92b",
"email": "signup@rota.ath.cx",
"token": "FgKngb1jXhCfHHJAAVU5oN67Xt-Wu7aqz4An-LYqO7rp6ULd55_6GSyJ39o6ycE_",
"created_at": "2026-09-12T22:26:45.179Z",
"expires_at": "2026-09-13T22:26:45.179Z"
}Errors
- 400
Domain 'x' is not available. Use GET /api/v1/domains to see available domains - 400
Username 'x' is reserved and cannot be used - 403
Custom usernames are not allowed on this server - 409
Email address 'x' is already taken - 422 Invalid username or domain format (validation detail list)
/api/v1/{token}/emailsList the inbox
A page of message summaries plus counters (total, page, per_page, has_next, has_prev).
Parameters
tokenstringrequiredpathAccess token returned by POST /api/v1/addresses. It is the only credential.
pageintegerqueryPage number, starting at 1. Default 1.
per_pageintegerqueryItems per page, 1–100. Default 50.
unread_onlybooleanqueryReturn only unread messages. Default false.
searchstringqueryCase-insensitive search across subject, sender and body.
Request
curl "$API/api/v1/$TOKEN/emails?per_page=20&search=invoice"Response
{
"emails": [
{
"id": "22222222-2222-4222-8222-222222222222",
"subject": "Your invoice for August",
"from_address": "billing@acme-corp.example",
"to_address": "signup@rota.ath.cx",
"received_at": "2026-09-12T20:31:09.412Z",
"is_read": false,
"has_attachments": true,
"size_bytes": 264133
}
],
"total": 1,
"page": 1,
"per_page": 20,
"has_next": false,
"has_prev": false
}Errors
- 404
Address not found - 404
Address has expired— create a new address and use its token
/api/v1/{token}/emails/{email_id}Read one message
Headers, both body versions and the DKIM/SPF/DMARC verdicts. body_html is untrusted markup — render it in a sandboxed iframe.
Parameters
tokenstringrequiredpathAccess token returned by POST /api/v1/addresses. It is the only credential.
email_iduuidrequiredpathMessage identifier taken from the inbox listing.
mark_readbooleanquerySets
is_readwhile reading. Sendfalseto peek without changing the flag. Default true.
Request
curl "$API/api/v1/$TOKEN/emails/22222222-2222-4222-8222-222222222222?mark_read=false"Response
{
"id": "22222222-2222-4222-8222-222222222222",
"message_id": "<invoice-88213@acme-corp.example>",
"subject": "Your invoice for August",
"from_address": "billing@acme-corp.example",
"to_address": "signup@rota.ath.cx",
"raw_headers": "From: billing@acme-corp.example\r\nSubject: Your invoice for August\r\n",
"body_plain": "Please find your invoice attached.",
"body_html": null,
"size_bytes": 264133,
"dkim_valid": false,
"spf_result": "softfail",
"dmarc_result": "none",
"has_attachments": true,
"received_at": "2026-09-12T20:31:09.412Z",
"is_read": false,
"attachments": [
{
"id": "aaaaaaa1-aaaa-4aaa-8aaa-aaaaaaaaaaa1",
"filename": "invoice-august.pdf",
"content_type": "application/pdf",
"size_bytes": 184320
}
]
}Errors
- 404
Email not found
/api/v1/{token}/emails/{email_id}/rawDownload the raw message
The original RFC 822 message as a .eml file.
Parameters
tokenstringrequiredpathAccess token returned by POST /api/v1/addresses. It is the only credential.
email_iduuidrequiredpathMessage identifier taken from the inbox listing.
Request
curl -OJ "$API/api/v1/$TOKEN/emails/$EMAIL_ID/raw"Body
Binary attachment (`message/rfc822`).Errors
- 404
Email not found
/api/v1/{token}/emails/{email_id}/attachments/{attachment_id}Download an attachment
The decoded file, with its original filename and content type.
Parameters
tokenstringrequiredpathAccess token returned by POST /api/v1/addresses. It is the only credential.
email_iduuidrequiredpathMessage identifier taken from the inbox listing.
attachment_iduuidrequiredpathIdentifier from the
attachmentsarray of the message detail.
Request
curl -OJ "$API/api/v1/$TOKEN/emails/$EMAIL_ID/attachments/$ATTACHMENT_ID"Body
Binary attachment.Errors
- 404
Email not found - 404
Attachment not found
/api/v1/{token}/emails/{email_id}Delete a message
Empty body with status 204. The message disappears from the listing.
Parameters
tokenstringrequiredpathAccess token returned by POST /api/v1/addresses. It is the only credential.
email_iduuidrequiredpathMessage identifier taken from the inbox listing.
Request
curl -X DELETE -i "$API/api/v1/$TOKEN/emails/$EMAIL_ID"Response
HTTP/1.1 204 No ContentErrors
- 404
Email not found
Worth knowing
- The token is the only credential: anyone holding it can read the inbox of that address until it expires, so treat it like a password.
- Addresses live for 24 hours by default and accept up to 100 messages, both configurable on the server (
address_lifetime_hours,max_emails_per_address). - Custom usernames must be 3-64 characters using letters, numbers, dot, underscore or hyphen; reserved words (admin, postmaster, abuse, ...) are rejected with 400.
body_htmlis untrusted markup: never inject it into your page. Render it inside a sandboxed iframe exactly as this interface does.- Attachments and the raw
.emlare streams, not JSON: call them with the token in the path and save the response as a file.
The project is open source: source code.