TrekMail REST API Overview for Developers
Learn how the TrekMail REST API works, including authentication with bearer tokens, plan-based access, rate limits, and response formats.
Article details
Type, difficulty, plans, and last updated info.
▼
Article details
Type, difficulty, plans, and last updated info.
- Type
- Reference
- Difficulty
- Intermediate
- Plans
- Nano · Starter · Pro · Agency
- Last updated
- Oct 24, 2026
Open TrekMail in Claude, choose Connect, sign in and approve access. You do not need to create an API token. To review permissions or disconnect, open AI Agents & API, Tokens, Connected apps.
Your own White Label brand: use the MCP address shown in your branded dashboard and name the connector after your brand. Add it as a custom connector; the public TrekMail listing signs in on TrekMail and does not replace your branded connection.
The TrekMail API lets you manage domains, mailboxes, forwarding, DNS, email migrations, and webmail operations from an HTTP client or AI agent. That includes reading and sending mail, drafts, scheduling, folders, contacts, calendars, identities, templates, and blocked senders. Authenticated requests use a bearer token, responses are JSON, and API activity is audited.
Read email with one key
On Pro and Agency, choose Read email when creating an API key. The explicit scope is mailboxes:messages:read; mailboxes:read only lists mailbox addresses and setup details. Select all mailboxes to include current and future mailboxes, or restrict access to selected domains and mailboxes. Existing keys gain no new permission automatically.
Use the account key with the GET routes below. Sending, deletion and other changes still require separate message credentials and permissions. Hosted MCP accepts OAuth, not tm_live_ or tm_msg_. A token-type error tells the agent which endpoint or authorization method to use.
GET /api/v1/mailboxes/{mailbox}/messages
GET /api/v1/mailboxes/{mailbox}/folders
GET /api/v1/mailboxes/{mailbox}/messages/{uid}
GET /api/v1/mailboxes/{mailbox}/messages/{uid}/raw
GET /api/v1/mailboxes/{mailbox}/messages/{uid}/attachments/{index}
What you get
- REST API v1 with a JSON request/response format.
- Bearer token authentication: no cookies or sessions for authenticated API calls.
- Idempotency keys on the write operations that require them, preventing duplicate work during retries.
- Per-token rate limiting with
Retry-Afterheaders. - Audit Log visible in your dashboard under AI Agents & API, Audit Log.
- MCP server with a catalog filtered to the credential, transport, and safety settings of the current connection. A narrow project connection therefore sees only the tools it can use.
- Domain aliases: connect receive-only addresses on a secondary domain to the same local parts on a primary domain, with saved-versus-live delivery states and safe removal. See Domain Aliases via API and MCP.
- Credential types: account keys for infrastructure and explicit mailbox message reads, and mailbox tokens for full email operations such as reading, sending, drafting, scheduling, contacts, calendars, identities, templates, and folders.
- Outbound deliverability and bounce insights: pull the sent, delivered, hard-bounce, and soft-bounce rollup from the dashboard, plus per-recipient SMTP codes and responses. See Deliverability and Bounces.
- Mail app passwords: manage separate passwords for IMAP, SMTP, calendars and contacts, and set which passwords mail apps accept. These do not replace API bearer tokens or Message API tokens. See Mail App Passwords via API and MCP.
- Mailbox storage usage:
list_mailboxesandget_mailboxreturnused_mb,quota_mb,allocation_mb, andis_pooled, so an agent can spot mailboxes approaching their limit without dashboard access. - White Label administration: inspect setup, manage per-domain branding, invite clients, control roles and domains, suspend or restore access, and review activity through API or MCP. See the branding guide and team-management guide.
Drive API and file automation
Drive is part of the public API surface. It covers Account Drive and mailbox Drive spaces, usage, folder browsing, uploads, file and folder management, Trash, bulk actions, public share links, sync-device password management, and read-only Drive Storage Add-on status.
Drive uses eleven ops-token scopes: drive:account:read, drive:account:write, drive:account:share, drive:account:purge, drive:mailbox:read, drive:mailbox:write, drive:mailbox:share, drive:mailbox:purge, drive:addon:read, drive:devices:read, and drive:devices:write. Billing actions for the Drive Add-on, purchase, resize, and cancel, remain dashboard-only and are not exposed as API or MCP write operations.
Start with Drive API Overview or the Drive API Quickstart.
Dual-token architecture
The API uses two independent token types. You can use one or both depending on your needs:
| Token type | Prefix | What it unlocks |
|---|---|---|
| Ops token | tm_live_ |
Account and infrastructure tools: White Label, domains, DNS, mailboxes, invites, Drive, migrations, SMTP, tickets, billing, and Cloudflare |
| Message token | tm_msg_ |
Webmail operations: messages, folders, attachments, drafts, scheduled send, spam/ham reporting, bulk actions, contacts, contact groups, calendar, compose helpers, identities, templates, and blocked senders |
Ops tokens and message tokens have separate scopes and separate rate limits. A single agent can use both tokens simultaneously by configuring them in the MCP server environment.
Message tokens are available on Pro and Agency plans.
Before you start
- All plans have API access:
- Nano: Email Verifier. Add a Drive Storage Add-on for full Drive API and MCP access.
- Starter: Full Drive, full Email Verifier, and read-only access across the remaining infrastructure areas. Use the dashboard for those write actions.
- Pro / Agency: Full base API access, including message tokens. White Label scopes are added while its trial or paid add-on is active.
- Connecting an AI agent? Add
https://trekmail.net/mcpas a remote MCP server in any compatible client. If it supports browser authorization, no manual token is needed. See Connecting AI Agents (MCP) for remote, CLI/desktop, bridge, and self-hosted options. - Writing your own integration? Create a
tm_live_token under AI Agents & API, Tokens, Create token and send it asAuthorization: Bearer …. See Creating and Managing API Tokens. - New to the API? Click Start tour at the top of the AI Agents & API page for a short walkthrough of connection methods, token management, connected apps, and the audit log.
How authentication works
Every request must include your token in the Authorization header:
Authorization: Bearer tm_live_abc123...
Ops tokens start with tm_live_ and message tokens start with tm_msg_. Both are shown once at creation and cannot be displayed again.
If the token is missing, revoked, or expired, the API returns 401 with an unauthenticated error code.
Base URL and versioning
All endpoints live under:
https://trekmail.net/api/v1
The base URL is shown on your AI Agents & API dashboard under Quick Reference. The version is in the URL path. When a v2 is introduced (if ever), v1 will continue to work.
Response format
Successful responses return JSON with a data key for single resources or a paginated list:
{
"data": [
{ "id": 1, "domain": "example.com", "status": "active" }
],
"links": { "next": "...", "prev": null },
"meta": { "current_page": 1, "last_page": 1, "total": 1 }
}
Error responses follow a consistent structure:
{
"error": {
"code": "unauthenticated",
"message": "Invalid or expired API token",
"hint": "Check that your token is correct and has not been revoked.",
"request_id": "req_abc123",
"retryable": false
}
}
Request IDs
Every response includes an X-Request-Id header. You can also pass your own via X-Request-Id in the request. It will be echoed back and logged in the audit trail.
Rate limits
Each token is rate-limited per minute. When you hit the limit, the API returns 429 with a Retry-After header indicating when you can retry.
Destructive operations (delete intents) have an additional daily limit per token and a cooldown between consecutive deletes.
Migration write operations (start, cancel, retry) have a dedicated rate limit of 10 requests per minute per token, plus a server-wide concurrency cap that returns 503 when too many migrations are running globally.
Message tokens use separate limits. The defaults are 30 read requests per minute per token, 60 send requests per minute per token, 5,000 successful reads per day per token, and 100 API sends per day across one mailbox. A second send safety counter defaults to 500 per token per day; the lower mailbox cap normally applies first. These API safeguards do not replace your plan's managed-SMTP limits or an external provider's own limits.
Idempotency
State-changing endpoints marked as idempotent require an Idempotency-Key header. This covers creates, updates, sends, and deletes where an automatic retry could otherwise duplicate work. Read-like POST actions such as provider detection or a connection test do not require one; check the endpoint table or OpenAPI specification. If you send the same key with the same body, the API replays the original response without creating duplicates. Message sends have one exception: once a send's delivery has failed (on a sending limit, for example), the API no longer replays its earlier 202, so the same message can be sent again later with the same content. If you pass your own X-Request-Id, give that attempt a new one: the API also treats a send from the same mailbox with the same X-Request-Id as a repeat, so the old one only gets the original, failed delivery back and nothing is sent. A retry of a send that is still pending or was delivered is still answered with the original response.
Idempotency-Key: create-mailbox-alice-2024
If you send the same key with a different body, the API returns 409 Conflict.
Mailbox storage allocation
Every endpoint that creates a mailbox or invite, POST /api/v1/mailboxes, /api/v1/mailboxes:bulk, /api/v1/mailboxes/invites, /api/v1/mailboxes/invites:bulk, accepts an optional storage_allocation_mb integer.
| Value | Meaning |
|---|---|
Omitted (or null) |
Mailbox uses the shared account pool (default). |
| Positive integer (MB) | Mailbox is dedicated. That exact amount is carved out of the account pool just for this mailbox. |
Allocations are validated against the live pool minus existing dedicated mailboxes and pending dedicated invites. Bulk endpoints check each dedicated item against what is left when that item is created: items that fit are created, and those that no longer fit fail individually with reason storage_pool_exceeded (207 partial success, or 422 when every item fails). In a domain under a White Label client storage limit, a fixed size must also fit what that limit has left: a single create or invite returns 422 client_storage_limit_exceeded with limit_mb, allocated_mb, available_mb, and requested_mb in details (a key confined to some domains gets only available_mb and requested_mb when the limit also covers a domain outside its reach), and a bulk item fails with reason client_storage_limit_exceeded. The pool refreshes when a dedicated mailbox is deleted, when an invite is redeemed (allocation moves to the new mailbox), and when a pending invite expires.
For invites the allocation is recorded on the access code and copied onto the new mailbox at redeem time. If the pool no longer fits the requested allocation at redeem time (e.g. another admin grew their dedicated allocation in the interim), the redeem gracefully downgrades the new mailbox to shared rather than failing, the recipient sees a notice on the success page.
To change an existing mailbox, send PATCH /api/v1/mailboxes/{id}/storage with storage_allocation_mb set to a fixed size in MB, or to null for shared storage. It needs mailboxes:write and an Idempotency-Key, works on active and paused mailboxes, and runs the same checks as the dashboard's Edit storage: storage_pool_exceeded when the account's free storage is too small and, in a domain under a client storage limit, client_storage_limit_exceeded, or client_storage_below_usage when the new size is below what the mailbox already stores. Shared storage is never refused for its size. A mailbox in Recently deleted or being deleted returns 422 mailbox_unavailable. The MCP tool is set_mailbox_storage.
White Label client storage limits live under /api/v1/white-label/client-limits; see Manage White Label Teams with API and MCP. A key confined to some domains sees only its share of the account in GET /api/v1/account: usage counts only its domains and mailboxes, limits.max_storage_mb is the client's limit when all of its domains share one that covers no other domain, and client_limit holds that limit (id, storage_limit_mb, allocated_mb, used_bytes) or null. Other callers get the response unchanged.
New regular mailboxes, including bulk creation, invites and AI-agent provisioning, follow Account settings > New mailboxes > Mail apps need app passwords, on by default. Changing it does not switch existing mailboxes. POST /api/v1/mailboxes can choose a client_auth_mode of app_password_only or password_or_app_password; if omitted, the account setting applies. Mailbox list and detail resources report client_auth_mode and app_passwords_count (active app passwords). Shared mailboxes have no app passwords or sign-in mode of their own.
The six MCP tools for app-password management are list_mailbox_app_passwords, create_mailbox_app_password, rotate_mailbox_app_password, revoke_mailbox_app_password, set_mailbox_client_auth_mode, update_account. update_account: Set new_mailbox_client_auth_mode for future mailboxes; existing mailboxes keep their mode. Requires mailboxes:write, an unrestricted owner token or connector, and TREKMAIL_ALLOW_DESTRUCTIVE=true in self-hosted stdio.
Mailbox Drive access
Every mailbox carries a drive_access level that decides how much of Drive the person using it reaches in webmail. It is returned on the mailbox resource and can be set with PATCH /api/v1/mailboxes/{id} or, for many mailboxes at once, POST /api/v1/mailboxes:drive-access.
| Value | Meaning |
|---|---|
full |
Everything: the Drive tab, uploading and sharing, file search, and sync to a computer. The default. |
attachments_only |
No Drive in webmail and no sync. Sending still works, a file over the attachment threshold goes out as a download link, and that copy is deleted after the retention window. |
disabled |
No Drive, and a file over the threshold cannot be attached at all. |
Storage is pooled across the account, so this is the control for how much of that pool a single person can fill with files.
Mailbox sign-in suspension
A mailbox can have its sign-in suspended while it carries on receiving mail: webmail, IMAP, SMTP and device passwords are refused and open sessions end, but delivery is untouched, so nothing bounces and everything is waiting when sign-in is restored. Set it with POST /api/v1/mailboxes/{id}:suspend-login (and :resume-login), or across many mailboxes with POST /api/v1/mailboxes:login-access.
The mailbox resource reports it as login_suspended, login_suspended_at and login_suspended_reason. Read login_suspended for whether the person can sign in, and status for whether the mailbox itself is running, a suspended mailbox stays active, because it is still accepting mail. :pause is the other thing: it sets status to disabled and stops delivery as well.
Suspension permanently revokes all app passwords. Restoring sign-in does not restore them. An admin setting a new mailbox password through the dashboard, API or AI agent also revokes all app passwords, as do password recovery, conversion to shared, and moving to Recently deleted. Restoring a deleted mailbox does not bring them back.
See Suspending Mailbox Sign-In via API.
The bulk endpoint takes exactly one selector, mailbox_ids, domain_id, or all, and returns what it did:
{ "data": { "drive_access": "attachments_only", "matched": 24, "updated": 21, "skipped_shared": 3 } }
domain_id is the selector to reach for when a domain is one customer. Mailboxes already on the requested level count as matched but not updated, so the call is safe to repeat.
Shared mailboxes are refused on the single endpoint with 422 drive_access_not_applicable and skipped (and counted) by the bulk one: they have no webmail user of their own, so members open them with their own level and a value stored on the shared row would change nothing.
The restriction applies to the API as well as the interface. A restricted mailbox's Drive space is absent from GET /api/v1/drive/spaces, its files answer 404 by id, and a sync device cannot be created for it.
Forwarding addresses
GET /api/v1/domains/{id}/forwarding-addresses returns more than the list, because two things about a forwarding address are invisible in the address itself:
{
"data": [ { "id": 8, "address": "sales@acme.com", "local_part": "sales",
"domain_id": 4, "recipients": ["team@example.net"], "is_active": true } ],
"limits": { "used": 1, "max": 100, "recipients_per_address": 15 },
"delivery": { "active": true, "requires_plan": "pro",
"paused_until": null, "paused_reason": null }
}
limits.maxis per domain and depends on the plan, 100 on Pro, 300 on Agency, and 25 saved-but-inactive on Nano or Starter.delivery.activeis whether these rules are moving mail right now. It isfalseon a plan belowrequires_plan, andfalsewhilepaused_untilis set (the account passed its hourly sending rate; see Sending Limits per Plan). A rule can beis_active: trueand still not deliver, so readdelivery, not justis_active, before reporting that forwarding works.
Creating on a plan that cannot deliver is allowed and returns 201: the rule is stored and starts working on upgrade. That mirrors the dashboard, which shows such rules as saved and inactive.
Rejections come back as 422 with error.code set to validation_error or limit_exceeded, an address already in use on the domain, a recipient on the same domain (which would loop), a recipient domain with no working MX, or the per-domain budget being full.
POST and DELETE on these endpoints require an Idempotency-Key; PATCH does not.
Delivery history
GET /api/v1/domains/{id}/forwarding-addresses/{addressId}/log returns what actually happened to recent mail, newest first:
{
"data": [
{ "id": 91, "occurred_at": "2026-07-27T09:12:04+00:00", "outcome": "delivered",
"from": "rfq@northgatesupply.com", "to": "sales@example.net",
"smtp_code": "2.0.0", "smtp_response": "250 2.0.0 OK" }
],
"address": "sales@acme.com",
"window": { "retention_days": 30, "max_events": 200 }
}
outcome is one of delivered, deferred (temporary failure, still retrying), failed (the recipient's server rejected it) and blocked, the last meaning our spam filter stopped the message before forwarding, so it never reached the recipient at all. Treating blocked as a bounce would send someone chasing the receiving server for a problem that happened on ours.
limit (1–200, default 100) is the only parameter. The window is the plan's retention, 30 days on Agency, 7 elsewhere, and nothing older exists to ask for, because forwarded events are pruned.
Shared (team) mailboxes
A shared mailbox has its own team inbox. can_read is the Read permission; can_send is Send as. Opening shared folders needs Read. Sending through the Message API from the shared inbox needs both. There is no shared password or separate sign-in, and no member roles.
GET /api/v1/mailboxes and GET /api/v1/mailboxes/{id} now return mailbox_type ("user" or "shared") and a boolean is_shared; shared mailboxes also include shared_member_count. Use those fields to tell a team inbox apart from a normal one before calling the member endpoints.
| Endpoint | Method | Scope Required | What it does |
|---|---|---|---|
/api/v1/mailboxes/{id}/members |
GET | mailboxes:read |
List members of a shared mailbox (each: member_mailbox_id, email, can_read, can_send, access (read_send, read_only, send_only)) |
/api/v1/mailboxes/{id}/members |
POST | mailboxes:write |
Add a member, body {member_mailbox_id, can_read?, can_send?}. Both permissions default to true. |
/api/v1/mailboxes/{id}/members/{member} |
PATCH | mailboxes:write |
Change access; {can_read?, can_send?, custom_label?}. Read can be turned off only when send-only access is available. |
/api/v1/mailboxes/{id}/members/{member} |
DELETE | mailboxes:write |
Remove a member; at least one person must keep Read. |
/api/v1/shared-mailboxes |
POST | mailboxes:create |
Create a shared mailbox, body {domain_id, local_part, display_name, member_mailbox_ids[], storage_shared?, storage_mb?} |
/api/v1/mailboxes/{id}/convert-to-shared |
POST | mailboxes:write |
Convert an existing mailbox into a shared one, body {member_mailbox_ids[]} (rotates the old password so it can no longer sign in; returns 202 conversion_pending with automatic retry if backend synchronization is not yet confirmed) |
/api/v1/mailboxes/{id}/convert-to-regular |
POST | mailboxes:write |
Convert a shared mailbox back to a regular one, body {password} (removes members and sets a fresh sign-in password) |
Converting back removes every member. Set a new webmail password, then create app passwords for mail apps on the Security tab.
Permission changes can return 422 no_access_left when both permissions would be off, 422 send_only_disabled when the requested Read change is unavailable, or 422 last_reader when the last active reader would lose access. Use Remove to end all access, and give another active member Read before removing the last reader. Each of these errors also returns shared_mailbox_id, member_id for an existing member, and a hint naming the request that fixes it.
The member endpoints reuse your existing mailboxes:read / mailboxes:write scopes. There is no separate shared-mailbox scope.
To discover native mail-app access, call GET /api/v1/mailboxes/{member_mailbox_id}/client-setup for a regular member mailbox. Its shared_mailboxes object reports durable native readiness, effective Send As readiness/reason, exact Inbox/Sent/Archive/Junk paths, and allowed operations. can_send is the assigned Send as permission, not proof that SMTP is currently ready. The endpoint never returns a password. Calling it with the shared mailbox id returns 422 direct_login_unavailable because the shared address cannot authenticate directly.
shared_mailboxes.items[] includes only memberships with Read when native folder access is enabled. delegated_send_addresses is a separate list of available team From addresses, each with id, email, and display_name; it can be empty. It grants no folder access and is not a sending-readiness check. These addresses are not Message API identities. Use the returned hosts, including for White Label, rather than copying a platform hostname.
Removing a member, changing can_read / can_send, or converting a shared mailbox to regular synchronizes mail-server permissions when native access is enabled. A 503 native_access_sync_failed response is retryable and guarantees that the membership, permission, or mailbox type was kept unchanged rather than partially applying the operation.
Available endpoints
Drive has its own reference and is not repeated here, see Drive API Overview. The account-level SMTP endpoints kept for backward compatibility are described under Per-domain SMTP routing rather than listed as current.
| Endpoint | Method | Scope Required |
|---|---|---|
/api/v1/domains |
GET | domains:read |
/api/v1/domains/{id} |
GET | domains:read |
/api/v1/domains/{id}/matching-addresses |
GET | domains:read |
/api/v1/domains/{id}/matching-addresses |
PUT | domains:write |
/api/v1/domains/{id}/matching-addresses |
DELETE | domains:write |
/api/v1/domains/{id}/dns-requirements |
GET | domains:dns:read |
/api/v1/domains/{id}/domain-connect/setup |
GET | domains:dns:read: one-time Cloudflare setup eligibility and blockers. A browser handoff URL is returned only when the credential also has cloudflare:write and the domain is eligible. Opening it requires an authorized TrekMail login and Cloudflare approval; this GET does not change DNS. |
/api/v1/domains/{id}/dns-recheck |
POST | domains:dns:recheck |
/api/v1/domains/{id}/spam-metrics |
GET | domains:read |
/api/v1/domains/{id}/spam-metrics/summary |
GET | domains:read |
/api/v1/domains/{id}/deliverability |
GET | domains:read |
/api/v1/domains/{id}/bounces |
GET | domains:read |
/api/v1/domains/{id}/signature |
GET | domains:read |
/api/v1/domains/{id}/forwarding-addresses |
GET | domains:read |
/api/v1/domains/{id}/forwarding-addresses/{addressId}/log |
GET | domains:read |
/api/v1/dns-checks/{id} |
GET | domains:dns:read |
/api/v1/mailboxes |
GET | mailboxes:read |
/api/v1/mailboxes |
POST | mailboxes:create |
/api/v1/mailboxes/{id} |
PATCH | mailboxes:write |
/api/v1/mailboxes/{id}/storage |
PATCH | mailboxes:write |
/api/v1/mailboxes/invites |
POST | mailboxes:invites:create |
/api/v1/mailboxes/invites:bulk |
POST | mailboxes:invites:create |
/api/v1/mailboxes:bulk |
POST | mailboxes:create |
/api/v1/mailboxes/{id}/forwarding |
GET | mailboxes:forwarding:read |
/api/v1/mailboxes/{id}/forwarding |
PUT | mailboxes:forwarding:write |
/api/v1/mailboxes/{id}/rules |
GET | mailboxes:rules:read |
/api/v1/mailboxes/{id}/rules |
POST | mailboxes:rules:write |
/api/v1/mailboxes/{id}/rules/{ruleId} |
GET | mailboxes:rules:read |
/api/v1/mailboxes/{id}/rules/{ruleId} |
PUT | mailboxes:rules:write |
/api/v1/mailboxes/{id}/rules/{ruleId} |
DELETE | mailboxes:rules:write |
/api/v1/mailboxes/{id}/rules/reorder |
PATCH | mailboxes:rules:write |
/api/v1/mailboxes/{id}/auto-reply |
GET | mailboxes:auto-reply:read |
/api/v1/mailboxes/{id}/auto-reply |
PUT | mailboxes:auto-reply:write |
/api/v1/mailboxes/{id}/sieve |
GET | mailboxes:rules:read |
/api/v1/mailboxes/{id}/sieve |
PUT | mailboxes:rules:write |
/api/v1/mailboxes/{id}:delete-intent |
POST | mailboxes:delete |
/api/v1/delete-intents/{id}:confirm |
POST | mailboxes:delete |
/api/v1/me |
GET | (any valid ops token) |
/api/v1/mailboxes/{id}/message-tokens |
POST | mailboxes:message-tokens:manage |
/api/v1/mailboxes/{id}/message-tokens |
GET | mailboxes:message-tokens:manage |
/api/v1/message-tokens/{id} |
DELETE | mailboxes:message-tokens:manage |
/api/v1/message-tokens/{id} |
PATCH | mailboxes:message-tokens:manage |
/api/v1/messages |
GET | messages:read (message token) |
/api/v1/messages/{uid} |
GET | messages:read (message token) |
/api/v1/messages/{uid} |
PATCH | messages:write (message token) |
/api/v1/messages/send |
POST | messages:send (message token) |
/api/v1/messages/deliveries/{requestId} |
GET | messages:read (message token) |
/api/v1/messages/sending-limits |
GET | messages:read (message token) |
/api/v1/messages/_ping |
GET | messages:read (message token, diagnostic) |
/api/v1/messages/{uid}/attachments/{index} |
GET | messages:read (message token) |
/api/v1/messages/{uid}/attachments |
GET | messages:read (message token) |
/api/v1/messages/{uid}/raw |
GET | messages:read (message token; returns raw_base64, encoding, content_type, size_bytes) |
/api/v1/messages/folders |
POST | messages:write (message token) |
/api/v1/messages/folders/{path} |
PATCH | messages:write (message token) |
/api/v1/messages/folders/{path} |
DELETE | messages:write (message token) |
/api/v1/messages/{uid}:spam |
POST | messages:write (message token) |
/api/v1/messages/{uid}:ham |
POST | messages:write (message token) |
/api/v1/messages/bulk |
POST | messages:write (message token) |
/api/v1/messages/folders:empty |
POST | messages:write (message token) |
/api/v1/messages/drafts |
POST | messages:write (message token); returns uid + uidvalidity |
/api/v1/messages/drafts/{uid} |
PUT | messages:write (message token); requires the draft's uidvalidity |
/api/v1/messages/scheduled |
POST | messages:send (message token) |
/api/v1/messages/scheduled |
GET | messages:read (message token) |
/api/v1/messages/scheduled/{id} |
PATCH | messages:send (message token) |
/api/v1/messages/scheduled/{id} |
DELETE | messages:send (message token) |
/api/v1/messages/contacts |
GET | messages:read (message token) |
/api/v1/messages/contacts |
POST | messages:write (message token) |
/api/v1/messages/contacts/{id} |
PATCH | messages:write (message token) |
/api/v1/messages/contacts/{id} |
DELETE | messages:write (message token) |
/api/v1/messages/contacts/import |
POST | messages:write (message token) |
/api/v1/messages/contacts/export |
GET | messages:read (message token) |
/api/v1/messages/contact-groups |
GET | messages:read (message token) |
/api/v1/messages/contact-groups/{id}/members |
GET | messages:read (message token) |
/api/v1/messages/external-accounts |
GET | messages:read (message token) |
/api/v1/messages/external-accounts |
POST | messages:write (message token) |
/api/v1/messages/external-accounts/{id} |
PATCH | messages:write (message token) |
/api/v1/messages/external-accounts/{id} |
DELETE | messages:write (message token) |
/api/v1/messages/external-accounts/detect |
POST | messages:read (message token) |
/api/v1/messages/external-accounts/test |
POST | messages:write (message token) |
/api/v1/messages/external-accounts/{id}/test |
POST | messages:write (message token) |
/api/v1/messages/_me |
GET | any message token (introspection) |
/api/v1/messages/calendar/events |
GET | messages:read (message token) |
/api/v1/messages/calendar/events |
POST | messages:write (message token) |
/api/v1/messages/calendar/events/{id} |
PATCH | messages:write (message token) |
/api/v1/messages/calendar/events/{id} |
DELETE | messages:write (message token) |
/api/v1/messages/{uid}/reply |
GET | messages:read (message token) |
/api/v1/messages/{uid}/reply-all |
GET | messages:read (message token) |
/api/v1/messages/{uid}/forward |
GET | messages:read (message token) |
/api/v1/messages/contact-groups |
POST | messages:write (message token) |
/api/v1/messages/contact-groups/{id} |
PATCH | messages:write (message token) |
/api/v1/messages/contact-groups/{id} |
DELETE | messages:write (message token) |
/api/v1/messages/contact-groups/{id}/members |
POST | messages:write (message token) |
/api/v1/messages/contact-groups/{id}/members |
DELETE | messages:write (message token) |
/api/v1/messages/identities |
GET | messages:read (message token) |
/api/v1/messages/identities |
POST | messages:write (message token) |
/api/v1/messages/identities/reply-policy |
PATCH | messages:write (message token) |
/api/v1/messages/identities/{id} |
PATCH | messages:write (message token) |
/api/v1/messages/identities/{id} |
DELETE | messages:write (message token) |
/api/v1/messages/templates |
GET | messages:read (message token) |
/api/v1/messages/templates |
POST | messages:write (message token) |
/api/v1/messages/templates/{id} |
PATCH | messages:write (message token) |
/api/v1/messages/templates/{id} |
DELETE | messages:write (message token) |
/api/v1/messages/blocked-senders |
GET | messages:read (message token) |
/api/v1/messages/blocked-senders |
POST | messages:write (message token) |
/api/v1/messages/blocked-senders/{id} |
DELETE | messages:write (message token) |
/api/v1/mailboxes/{id}/enable-imap |
POST | mailboxes:write (ops token) |
/api/v1/migrations/test-connection |
POST | migrations:write |
/api/v1/migrations |
GET | migrations:read |
/api/v1/migrations/{id} |
GET | migrations:read |
/api/v1/migrations |
POST | migrations:write |
/api/v1/migrations/{id}:cancel |
POST | migrations:write |
/api/v1/migrations/{id}:retry |
POST | migrations:write |
/api/v1/migrations/{id} |
DELETE | migrations:write |
/api/v1/migrations/bulk/preview |
POST | migrations:write |
/api/v1/migrations/bulk |
POST | migrations:write |
/api/v1/migrations/bulk |
GET | migrations:read |
/api/v1/migrations/bulk/{batch} |
GET | migrations:read |
/api/v1/migrations/bulk/{batch}:cancel |
POST | migrations:write |
/api/v1/migrations/bulk/{batch}:retry |
POST | migrations:write |
/api/v1/migrations/bulk/{batch}:resume |
POST | migrations:write |
/api/v1/migrations/bulk/{batch} |
DELETE | migrations:write |
/api/v1/migrations/bulk/{batch}/jobs/{job}/password |
PATCH | migrations:write |
/api/v1/account |
GET | account:read |
/api/v1/account |
PATCH | mailboxes:write (owner only, unrestricted token; MCP: update_account) |
/api/v1/sending-limits |
GET | account:read |
/api/v1/billing/status |
GET | billing:read |
/api/v1/billing/invoices |
GET | billing:read |
/api/v1/domains |
POST | domains:create |
/api/v1/domains/{id} |
DELETE | domains:delete |
/api/v1/domains/{id}/catch-all |
PATCH | domains:write |
/api/v1/domains/{id}/mail-hosting |
PATCH | domains:write |
/api/v1/domains/{id}/forwarding-addresses |
POST | domains:write |
/api/v1/domains/{id}/forwarding-addresses/{addressId} |
PATCH | domains:write |
/api/v1/domains/{id}/forwarding-addresses/{addressId} |
DELETE | domains:write |
/api/v1/domains/{id}/dkim:retry |
POST | domains:write |
/api/v1/domains/{id}/note |
PATCH | domains:write |
/api/v1/domains/{id}/signature |
PATCH | domains:write |
/api/v1/domains/{id}/branding |
GET | domains:read |
/api/v1/domains/{id}/branding |
PATCH | domains:write |
/api/v1/domains/{id}/branding/logo/{slot} |
PUT | domains:write |
/api/v1/domains/{id}/branding/logo/{slot} |
DELETE | domains:write |
/api/v1/domains/{id}/branding/verify-dns |
POST | domains:write |
/api/v1/domains/{id}/branding/preview |
POST | domains:write |
/api/v1/domains/{id}/branding |
DELETE | domains:write |
/api/v1/domains:bulk-add |
POST | domains:create |
/api/v1/mailboxes/{id} |
GET | mailboxes:read |
/api/v1/mailboxes/{id}/client-setup |
GET | mailboxes:read |
/api/v1/mailboxes/{id}/apple-mail-profile |
GET | mailboxes:read |
/api/v1/mailboxes/{id}/bounces |
GET | mailboxes:read |
/api/v1/mailboxes/{id}/password |
POST | mailboxes:write |
/api/v1/mailboxes/{id}/app-passwords |
GET | mailboxes:read |
/api/v1/mailboxes/{id}/app-passwords |
POST | mailboxes:write |
/api/v1/mailboxes/{id}/app-passwords/{appPasswordId}:rotate |
POST | mailboxes:write |
/api/v1/mailboxes/{id}/app-passwords/{appPasswordId} |
DELETE | mailboxes:write |
/api/v1/mailboxes/{id}:client-auth-mode |
POST | mailboxes:write |
/api/v1/mailboxes:client-auth-mode |
POST | mailboxes:write |
/api/v1/mailboxes/{id}/note |
PATCH | mailboxes:write |
/api/v1/mailboxes/{id}:pause |
POST | mailboxes:write |
/api/v1/mailboxes/{id}:restore |
POST | mailboxes:delete |
/api/v1/mailboxes/{id}:resume |
POST | mailboxes:write |
/api/v1/mailboxes/{id}:suspend-login |
POST | mailboxes:write |
/api/v1/mailboxes/{id}:resume-login |
POST | mailboxes:write |
/api/v1/mailboxes:login-access |
POST | mailboxes:write |
/api/v1/mailboxes:drive-access |
POST | mailboxes:write |
/api/v1/mailboxes/{id}/members |
GET | mailboxes:read |
/api/v1/mailboxes/{id}/members |
POST | mailboxes:write |
/api/v1/mailboxes/{id}/members/{member} |
PATCH | mailboxes:write |
/api/v1/mailboxes/{id}/members/{member} |
DELETE | mailboxes:write |
/api/v1/shared-mailboxes |
POST | mailboxes:create |
/api/v1/mailboxes/{id}/convert-to-shared |
POST | mailboxes:write |
/api/v1/mailboxes/{id}/convert-to-regular |
POST | mailboxes:write |
/api/v1/tickets |
GET | tickets:read |
/api/v1/tickets/{id} |
GET | tickets:read |
/api/v1/tickets/{id}/messages |
GET | tickets:read |
/api/v1/tickets |
POST | tickets:write |
/api/v1/tickets/{id}:mark-seen |
POST | tickets:write |
/api/v1/tickets/{id}/reply |
POST | tickets:write |
/api/v1/tickets/{id}:close |
POST | tickets:write |
/api/v1/domains/{id}/smtp |
GET | smtp:read |
/api/v1/domains/{id}/smtp |
PUT | smtp:write |
/api/v1/domains/{id}/smtp/profiles |
GET | smtp:read |
/api/v1/domains/{id}/smtp/profiles |
POST | smtp:write |
/api/v1/domains/{id}/smtp/profiles/{connectionId} |
PUT | smtp:write |
/api/v1/domains/{id}/smtp/profiles/{connectionId} |
DELETE | smtp:write |
/api/v1/domains/{id}/smtp:test |
POST | smtp:write |
/api/v1/domains/{id}/smtp:test-status/{jobId} |
GET | smtp:read |
/api/v1/smtp/default |
GET | smtp:read |
/api/v1/smtp/default |
PUT | smtp:write |
/api/v1/smtp (legacy, back-compat) |
GET | smtp:read |
/api/v1/smtp (legacy, back-compat) |
PUT | smtp:write |
/api/v1/smtp/{id} (legacy, back-compat) |
DELETE | smtp:write |
/api/v1/smtp:test (legacy, back-compat) |
POST | smtp:write |
/api/v1/smtp:test-status/{jobId} (legacy, back-compat) |
GET | smtp:read |
/api/v1/messages/{uid} |
DELETE | messages:write (message token) |
/api/v1/messages/{uid}:move |
POST | messages:write (message token) |
/api/v1/messages/folders |
GET | messages:read (message token) |
/api/v1/mailboxes/{id}/aliases |
GET | mailboxes:read |
/api/v1/mailboxes/{id}/aliases |
POST | mailboxes:write |
/api/v1/mailboxes/{id}/aliases/{aliasId} |
PATCH | mailboxes:write |
/api/v1/mailboxes/{id}/aliases/{aliasId} |
DELETE | mailboxes:write |
/api/v1/verify |
POST | verify:write |
/api/v1/verify/bulk |
POST | verify:write |
/api/v1/verify/bulk/{jobId} |
GET | verify:read |
/api/v1/verify/bulk/{jobId}/download |
GET | verify:read |
/api/v1/verify/credits |
GET | verify:read |
/api/v1/verify/bulk |
GET | verify:read |
/api/v1/verify/bulk/{jobId}/cancel |
POST | verify:write |
/api/v1/verify/bulk/{jobId} |
DELETE | verify:write |
/api/v1/cloudflare/validate-token |
POST | cloudflare:read |
/api/v1/cloudflare/zones |
POST | cloudflare:read |
/api/v1/cloudflare/connect |
POST | cloudflare:write (+ domains:create to create new domains) |
/api/v1/cloudflare/preview |
POST | cloudflare:read |
/api/v1/cloudflare/apply |
POST | cloudflare:write |
/api/v1/cloudflare/tokens |
GET | cloudflare:read |
/api/v1/cloudflare/tokens/{id} |
DELETE | cloudflare:delete |
The Cloudflare endpoints follow the same flow as the dashboard: validate a token, list zones, connect domains, preview the DNS changes, then apply them. Both /cloudflare/preview and /cloudflare/apply accept two optional per-domain controls:
included_records, an allowlist of which records to touch, keyed by domain ID:{ "123": ["mx_primary", "spf_record"] }. Records you leave out are skipped, so you can apply only MX and SPF and come back for DKIM later. Omit the field to apply every record.confirmed_conflicts, when preview flags a record that already exists with a different value, list its record ID here (same{ domain_id: [record_ids] }shape) to authorise replacing it.
The record IDs (mx_primary, spf_record, dkim_primary, dmarc_main, …) come straight from the preview response, so a typical agent calls preview first and feeds the IDs it wants back into apply:
POST /api/v1/cloudflare/apply
{
"domain_ids": [123],
"included_records": { "123": ["mx_primary", "spf_record"] },
"confirmed_conflicts": { "123": ["dmarc_main"] }
}
Per-domain SMTP routing and the account default
SMTP is configured per domain. Each domain picks one of three routes, managed platform sending, a saved SMTP profile (your own provider, reusable across domains), or "not configured", and a single account-wide default decides which route new domains start on.
Per-domain endpoints (smtp:read / smtp:write):
| Endpoint | Method | What it does |
|---|---|---|
/api/v1/domains/{id}/smtp |
GET | Current route: smtp_mode, effective_smtp_mode, profile, effective_profile |
/api/v1/domains/{id}/smtp |
PUT | Set the route, body {smtp_mode: platform|profile|not_configured|inherit, smtp_connection_id?} |
/api/v1/domains/{id}/smtp/profiles |
GET | List the account's saved SMTP profiles |
/api/v1/domains/{id}/smtp/profiles/{connectionId}/usage |
GET | List the exact domains and Send As addresses using a profile (no credentials) |
/api/v1/domains/{id}/smtp/profiles |
POST | Create a profile and use it for this domain |
/api/v1/domains/{id}/smtp/profiles/{connectionId} |
PUT | Update a profile (affects every domain using it) |
/api/v1/domains/{id}/smtp/profiles/{connectionId} |
DELETE | Delete a profile (domains using it are reassigned to the account default) |
/api/v1/domains/{id}/smtp:test |
POST | Test a route: returns {job_id, poll_url} |
/api/v1/domains/{id}/smtp:test-status/{jobId} |
GET | Poll a test job. Poll it under the domain you launched it for: another domain gets 404 not_found, even one this token can reach |
A few notes on the route body:
smtp_mode=platformselects managed sending;smtp_mode=profilerequiressmtp_connection_id;not_configuredclears the route.smtp_mode=inheritmakes the domain live-follow the account default, whenever the default changes, this domain changes with it. The web UI always writes concrete routes, but the backend still supportsinherit, which is whyGETreturnseffective_smtp_modeshowing whatinheritcurrently resolves to.- This route changes only its own domain. To make a route the account default (the dashboard's Make this the account default), use
PUT /api/v1/smtp/defaultbelow; addapply_to_all: trueto also switch every domain to it (Apply to all domains).
Account-wide default endpoints (smtp:read / smtp:write):
| Endpoint | Method | What it does |
|---|---|---|
/api/v1/smtp/default |
GET | Returns default_smtp_mode (null until you set one), effective_default_smtp_mode (the plan baseline used when unset), default_smtp_connection_id, and profile |
/api/v1/smtp/default |
PUT | Set the default, body {smtp_mode: platform|profile|not_configured, smtp_connection_id?, apply_to_all?} |
Deleting a profile that was the account default resets the default to the plan baseline.
A key limited to some domains or mailboxes gets 403 token_resource_constrained from PUT /api/v1/smtp/default, from the legacy PUT and DELETE /api/v1/smtp, and when it deletes a saved profile, because each of these changes the route of domains it may not reach. A key limited to some domains gets the same answer when it edits a profile that a domain outside its list also uses.
Legacy endpoints. The account-level GET/PUT /api/v1/smtp (and DELETE /api/v1/smtp/{id}, POST /api/v1/smtp:test, GET /api/v1/smtp:test-status/{jobId}) remain for backward compatibility but no longer control per-domain routing: use the per-domain and /smtp/default endpoints above. The legacy MCP tools get_smtp_config / update_smtp_config are deprecated for the same reason.
White Label branding, clients, and team access
Branding is configured per domain with branding:read / branding:write. A domain runs its own brand (mode=custom), inherits the account default (mode=inherit), or is off. An active White Label trial or paid add-on is required. After cancellation, the owner keeps read-only recovery access during the displayed grace window. Read the domain's dns_records and publish those returned records exactly. Do not derive hostnames or CNAME targets from an example.
| Endpoint | Method | What it does |
|---|---|---|
/api/v1/domains/{id}/branding |
GET | Read branding: mode, white_label_addon_active, brand, hosts, the dns_records to create, cname_target, and mail_zone |
/api/v1/domains/{id}/branding |
PATCH | Partial-merge update: mode, name, primary_color/accent_color, dashboard_enabled/dashboard_label, webmail_enabled/webmail_label, mail_zone_enabled, support_email, support_url, sender_email, scope |
/api/v1/domains/{id}/branding/logo/{slot} |
PUT | Upload a base64 logo (slot = light|dark|favicon; PNG/JPG, ICO for favicon, ≤1 MB, no SVG). Default scope=domain requires custom mode; explicit scope=account_default on an inherit domain requires an unconstrained token. |
/api/v1/domains/{id}/branding/logo/{slot} |
DELETE | Remove a logo slot. Uses the same domain/account-default scope rules; DELETE takes scope as a query parameter. |
/api/v1/domains/{id}/branding/verify-dns |
POST | Queue DNS verification for the branded hosts and the brand's mail zone |
/api/v1/domains/{id}/branding/preview |
POST | Create a short-lived preview URL (422 no_brand if branding has not been set) |
/api/v1/domains/{id}/branding?scope=domain|all |
DELETE | Clear branding for this domain or the whole account |
PATCH is a partial merge, so omitted fields are preserved. If branding is currently off, pass mode to re-enable it. A custom sender_email must be on a domain with a verified DKIM key. mail_zone_enabled serves mail apps and DAV sync under the brand's own domain. It belongs to the brand rather than a single domain, so it needs mode=custom or scope=account_default; an inherit domain returns 422 inherited_brand. Read mail_zone.dns_status, mail_zone.client_hosts_status, mail_zone.records, mail_zone.dav_url, and mail_zone.dav_ready to track provisioning and use only a ready DAV address. For the full agent workflow, see the White Label Branding API and MCP Guide.
The account-level White Label surface adds 18 routes under /api/v1/white-label: status and setup progress, a live access catalog, member listing and lifecycle actions, client storage limits, account activity, and per-member action/sign-in history. It uses members:read, members:write, and activity:read. Access is always the intersection of account entitlement, the person's current membership, the credential grant, and any domain constraint. See Manage White Label Teams with API and MCP for the route table and state transitions.
The OpenAPI specification is available at /api/openapi.json for import into Postman, Insomnia, or code generators.
Quick fixes
- 401 "unauthenticated": Check that the
Authorization: Bearer <token>header is present and the token has not been revoked or expired. - 403 "plan_api_disabled": The requested scope isn't on your plan. Nano covers Email Verifier (and Drive if you've bought the Drive Storage Add-on). Upgrade to Starter or higher for the rest of the API.
- 403 "token_scope_blocked_by_plan": Your token has scopes that are not available on your current plan. Revoke the token and create a new one with allowed scopes.
- 403 "scope_blocked_by_entitlement": A stored White Label grant is unavailable because the add-on is inactive or the operation is a write during grace. Reactivate White Label, then retry with the same credential.
- 403 "scope_blocked_by_membership": The current member role is narrower than the requested action. Ask the owner to change it; reauthorizing alone cannot widen membership.
- 422 "missing_idempotency_key": Add an
Idempotency-Keyheader to the write operation named by the endpoint reference. - 403 "mailbox_sending_paused": Sending from that mailbox has been stopped because its outbound mail stopped looking like its owner, usually a password in the wrong hands. Reading, listing and every other endpoint still work; only sending is refused, and retrying will not clear it. The mailbox password needs changing, after which support switches sending back on. See Why Can't I Send Email?.
- 403 "account_sending_on_hold": Sending from the whole account is held while TrekMail reviews it. Receiving and every other endpoint keep working; retrying will not clear it. Contact support to have the hold reviewed.
- 403 "mailbox_send_disabled": Sending is switched off for this one mailbox. Reading and every other endpoint keep working; retrying will not clear it. Usually the mailbox falls outside the plan: its domain is beyond the number of domains the plan includes, or the mailbox is beyond the plan's mailboxes per domain (the oldest domains and mailboxes keep sending). This happens after a move to a smaller plan. It can also be a mailbox created before its domain finished setup. Upgrading turns sending back on at once; after you remove the extra domains or mailboxes, or once the domain finishes setup, it comes back within a day. If none of this applies, contact support.
- 403 "account_email_unverified": Sending is blocked until the account owner verifies their sign-in email. Accounts registered on or after 19 February 2026 have 7 days to do it, and the same 7 days start again when a verified sign-in email is replaced with a new one. Reading and every other endpoint keep working; retrying will not clear it. Open the link in the verification email, or use Resend verification email in the dashboard banner; sending works again as soon as the address is verified. See Why Can't I Send Email?.
- 429 rate limited: Wait for the duration in the
Retry-Afterheader before retrying. The per-mailbox daily API cap also answersrate_limited, but withretryableset tofalse,dimensionset toapi_mailbox_daily, and aresets_attime: retrying sooner will not work, so wait until then. - 429 "sending_limit_exceeded": A sending allowance is used up.
dimensionsays which one andresets_atsays when it renews. Retrying earlier cannot work, soretryableisfalse.GET /api/v1/messages/sending-limitsshows inremaining_todayhow many more recipients the mailbox can reach today. When the lowest allowance is the account-wide total,limit_todayisnullandremaining_todayisnulluntil that total is spent (then0); an ops token reads the total fromGET /api/v1/sending-limits. Ifdimensionisrecipients_per_message, split the message into smaller ones instead. In the first 24 hours after the account's first payment the causes includenew_payer, andGET /api/v1/sending-limitsgives the moment the plan's full limits apply infull_limits_at. See Sending Limits per Plan.
Sending email: body, headers, deliverability
POST /api/v1/messages/send takes the request shape {to, subject, body: {text, html}, attachments, reply_to_message_id, headers}.
body.textandbody.htmlare both optional, but at least one is required. If you provide onlybody.textwe auto-generate an HTML alternative using<p>paragraphs (blank lines split paragraphs; single newlines become<br>) so the message renders as ordinary email in every modern client. If you need monospace, send the literal<pre>...</pre>inbody.html.headersis an optional object of user-supplied outbound headers. The whitelist isList-Unsubscribe,List-Unsubscribe-Post,Reply-To, and anyX-*custom tracking header. Other names (From,Subject,Message-Id,Authentication-Results, etc.) are platform-managed and rejected with422. Values containing CR/LF are also rejected (header injection protection). Values are capped at 998 chars per RFC 2822.- For bulk/automation use cases, see the Bulk-sender deliverability headers section for
List-Unsubscribesetup and the account-wideauto_list_unsubscribetoggle.
A 202 means the message was accepted, not yet sent. Sending allowances are checked again when the message leaves the queue, so check the outcome with GET /api/v1/messages/deliveries/{request_id}, using the request_id from the 202. Any message token for the same mailbox can check it, not only the token that sent the message; an unknown request_id returns 404. If a limit stopped it, status is failed and failure names the limit (dimension) and when it renews (resets_at). A failed send can be sent again later with the same content. If you pass your own X-Request-Id, use a new one for it, since the old one only returns the original delivery. A scheduled message that reaches a limit when it is due is held as limit_reached and kept for 7 days: list these with GET /api/v1/messages/scheduled?status=limit_reached, send one later by giving it a new scheduled_for with PATCH /api/v1/messages/scheduled/{id}, or remove it with DELETE.
Related articles
Jump to nearby guides that continue the workflow.