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
- Aug 3, 2026
The TrekMail API lets you manage domains, mailboxes, forwarding, DNS, email migrations, and full webmail operations — read, send, draft, schedule, folder management, contacts, calendar, identities, templates, and blocked senders — from any HTTP client or AI agent. Every request uses a bearer token, every response is JSON, and every action is audited.
What you get
- REST API v1 with 244 registered routes and a JSON request/response format.
- Bearer token authentication — no cookies, no sessions.
- Idempotency keys on mutating requests to prevent duplicate operations.
- Per-token rate limiting with
Retry-Afterheaders. - Audit log visible in your dashboard under AI Agents & API → Audit Log.
- MCP server with a 235-tool catalog: 173 infrastructure/operations tools and 62 message tools. Hosted HTTP MCP exposes at most 234 because local file upload is intentionally stdio-only. Only tools allowed by the current credential and safety settings are registered, keeping narrow project connections compact.
- Dual-token architecture — separate ops tokens for infrastructure and message tokens for full email operations (read, send, draft, schedule, contacts, calendar, identities, templates, folders, and more).
- Outbound deliverability + bounce insights — pull the same sent/delivered/hard/soft rollup your dashboard shows, plus per-recipient hard/soft bounces with the receiver's SMTP status code and response. See Deliverability and Bounces.
- Mailbox storage usage —
list_mailboxesandget_mailboxnow returnused_mb,quota_mb,allocation_mb, andis_pooled, so an agent can spot mailboxes approaching their limit without dashboard access. - Per-domain White Label branding — configure a domain's brand identity, logos, and branded dashboard/webmail hosts, read back the CNAME records to create, and verify DNS, entirely over the API or MCP. See White Label Branding API and MCP Guide.
Drive API and file automation
Drive is now part of the public API surface. The Drive API exposes 43 REST endpoints for Account Drive and mailbox Drive: spaces, usage, folder browsing, uploads, file and folder management, Trash, bulk actions, public share links, sync-device password management (rclone / Cyberduck / X-Plore / DAVx⁵ / Documents / FolderSync), 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_ |
Infrastructure tools — domains, DNS, mailboxes, invites, forwarding, delete intents, migrations, SMTP, tickets, account, billing, Cloudflare |
| Message token | tm_msg_ |
Full webmail API — messages, folders, attachments, drafts, scheduled send, spam/ham reporting, bulk actions, contacts, contact groups, calendar, compose helpers (reply/forward), identities, templates, 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 and the full Drive API + 42 MCP tools come with it.
- Starter — Full Drive, full Email Verifier, and read-only across the rest (domains, mailboxes, migrations, tickets, etc.). Writes for those run from the dashboard.
- Pro / Agency — Everything: read, write, create, delete across every family, plus message tokens for email read/send.
- 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. TrekMail stores only the SHA-256 hash — the plaintext cannot be recovered.
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.
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 additionally validate the sum of allocations across the batch and reject the entire batch with 422 storage_pool_exceeded if it would over-commit. 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.
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": 300, "recipients_per_address": 15 },
"delivery": { "active": true, "requires_plan": "agency",
"paused_until": null, "paused_reason": null }
}
limits.maxis per domain and depends on the plan — 300 on Agency, 25 elsewhere.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 is a team inbox such as support@ or sales@ that members open through their own regular mailbox account — in Webmail and, when native access is enabled, as a delegated IMAP folder. There is no shared password or separate login. Access is flat: every member can read, and a single can_send flag controls whether that member can reply as the address (true) or is read-only (false). There are 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) |
/api/v1/mailboxes/{id}/members |
POST | mailboxes:write |
Add a member — body {member_mailbox_id, can_send?} (can_send defaults to true) |
/api/v1/mailboxes/{id}/members/{member} |
PATCH | mailboxes:write |
Toggle a member's reply access — body {can_send} |
/api/v1/mailboxes/{id}/members/{member} |
DELETE | mailboxes:write |
Remove a member (a shared mailbox always keeps at least one) |
/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) |
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 Can reply 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.
Removing a member, changing 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}/dns-requirements |
GET | domains:dns:read |
/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/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/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/_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) |
/api/v1/messages/drafts/{uid} |
PUT | messages:write (message token) |
/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/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}/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}/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}/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 |
/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?, set_account_default?, apply_to_all?} |
/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 |
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.set_account_default: trueis the API equivalent of the dashboard's Make this the account default toggle (new domains start on this route).apply_to_all: trueis the Apply to all domains button (a one-time switch of every domain to this route).
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.
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.
Per-domain White Label branding
Branding is configured per domain (domains:read / domains:write). A domain runs its own brand (mode=custom), inherits the account default (mode=inherit), or is off. Settings always save; the branded dashboard.yourbrand.com / mail.yourbrand.com hosts only go live once the White Label add-on is active (without it they stay draft). The branded hosts point a CNAME at wl.trekmail.net.
| 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, and cname_target |
/api/v1/domains/{id}/branding |
PATCH | Partial-merge update — mode, name, primary_color/accent_color, dashboard_enabled/dashboard_label, webmail_enabled/webmail_label, 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 (422 white_label_inactive without the add-on) |
/api/v1/domains/{id}/branding/preview |
POST | Mint a short-lived preview URL (422 preview_disabled or no_brand) |
/api/v1/domains/{id}/branding?scope=domain|all |
DELETE | Clear branding for this domain or the whole account |
PATCH is a partial merge — omitted fields are preserved. If branding is currently off you must pass mode to re-enable it, and a custom sender_email must be on a domain with a verified DKIM key. The full walkthrough — including the autonomous "set brand → read CNAMEs → apply_cloudflare_dns → verify → poll" agent flow — is in the White Label Branding API and MCP Guide.
Internal support endpoints (HMAC auth)
These endpoints use HMAC-SHA256 authentication (not bearer tokens) and are used by the AI support pipeline:
| Endpoint | Method | Auth |
|---|---|---|
/api/v1/support/tickets/{id} |
GET | HMAC |
/api/v1/support/reply |
POST | HMAC |
/api/v1/support/messages/{id} |
PUT | HMAC |
/api/v1/support/messages/{id} |
DELETE | HMAC |
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.
- 422 "missing_idempotency_key": Add an
Idempotency-Keyheader to POST and PUT requests. - 429 rate limited: Wait for the duration in the
Retry-Afterheader before retrying.
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.
Related articles
Jump to nearby guides that continue the workflow.