Overview
AXO Email is business email on your own domain, plus a built-in calendar, file hub, contacts, and admin console. This guide walks through everything the product does — pick what you need, or read it top-to-bottom once and you'll never open it again.
__axoStartTour() in the browser console.
Create an account
Visit signup and complete the five-step wizard:
- Account — your name, the email you use today (where account info goes), a password (10+ characters), and the domain you want to use for business email.
- Plan — Starter, Business, or Enterprise, billed monthly or yearly. You can change this later.
- Setup path — DIY (free) or Concierge ($49 one-time, we handle the DNS work).
- Payment — your card is required and billed today for your subscription. The $49 concierge fee, if you opt in, is billed separately on the same day.
- Done — we show DNS records if you picked DIY, or a timeline if you picked Concierge.
Connect your domain
Email needs a handful of DNS records on your domain for us to send, receive, and pass modern deliverability checks. Your setup panel lists every record with its exact value and a copy button; if you registered or transferred the domain through us, we publish them for you and there is nothing to add. For a domain you keep at your own registrar, add:
- MX —
@→mail.yourdomain.com, priority 10. Routes incoming mail to us under your own name. - A —
mail→ the address shown in your setup panel. This is wheremail.yourdomain.compoints; it is also the host your mail apps connect to. - SPF (TXT) —
@→ the exact value shown in your setup panel. Authorizes us to send on your behalf. - DKIM (TXT) — we generate per-domain signing keys and show you the records. Signs outgoing mail cryptographically.
- DMARC (TXT) —
_dmarc→v=DMARC1; p=noneto start; tighten top=quarantineonce everything passes. Tells receivers what to do with unauthenticated mail claiming to be from your domain.
Our setup guide has copy-to-clipboard buttons for each value, plus registrar-specific instructions (GoDaddy, Namecheap, Cloudflare, Route 53, more).
First sign-in
Head to signin, enter your credentials, and you'll land in Inbox. The URL bar shows #inbox; you can change folders just by typing a new hash (#sent, #drafts, #label/work) — refreshes keep state.
Verify your email
The first time you send, we ask you to verify the admin email you entered. You'll get a 6-digit code by SMS — paste it into verify-email. The code auto-submits once you've typed all six digits. If it's lost, "Resend" has a 60-second cooldown.
Inbox basics
The webmail app has three panes:
- Sidebar — folders (Inbox, Starred, Snoozed, Sent, Drafts, Archive, Spam, Trash), your custom labels, storage meter, and links to Contacts / Admin / Settings.
- Message list — one row per message with sender, timestamp, subject, preview snippet, star, attachment icon, and label chips.
- Reading pane — the selected message with full actions (star, archive, snooze, delete, more) and reply/reply-all/forward row at the bottom.
Click a sender's name to open a quick card with contact details and recent messages; click a label chip in the list head to jump to that label.
Compose, reply, forward
Click New (or press c) to open the compose panel. It slides in from the bottom-right.
- To / Cc / Bcc — click "Cc · Bcc" to expand. Autocomplete from your Contacts pulls in as you type.
- Subject & body — body is a rich-text editor with Bold (⌘B), Italic (⌘I), Underline (⌘U), bullet/numbered lists, and inline links.
- From address — bottom-right picker lets you choose which of your aliases to send from.
- Pop out — the arrow icon in the compose header expands to full screen.
Replying preserves the quoted thread. Reply-all pre-fills Cc with everyone from the original. Forward pre-fills the subject with "Fwd:" and leaves To empty.
Attachments
Three ways to attach:
- Click the paperclip in the compose toolbar.
- Drag and drop files onto the compose body.
- Use the image icon to insert an inline image.
Each attachment appears as a chip below the editor with filename, size, and a remove button. Icons are color-coded by type (PDF red, image purple, doc blue, sheet green, archive gray).
Drafts & autosave
We save your draft 1.2 seconds after you stop typing. The compose header shows "Saving…" then "Draft saved". Close the panel and the draft lives in Drafts — click it there to continue editing (same draft ID updates, no duplicates).
The trash icon in compose discards the draft (confirmation prompt if there's any content).
Send, undo, schedule
Click Send (or press ⌘⏎) and a toast appears with a 5-second Undo button. Click it to reopen the composer with the exact content restored — no message is actually sent until the 5 seconds expire.
For scheduled send, click the caret next to Send:
- Tomorrow morning · Tomorrow afternoon · Monday morning (dates computed live)
- Or pick a custom date & time
Archive, snooze, spam, trash
All actions are available in the reading pane header, via keyboard shortcuts, or via bulk selection. Here's what each does:
- Archive (e) — moves out of inbox, stays searchable forever.
- Snooze (z) — pops a popover with presets (Later today, Tomorrow morning, This weekend, Next week) or a custom datetime. The message moves to Snoozed with a wake time; view it any time, or let it return to Inbox at wake time.
- Delete (#) — moves to Trash, auto-purged after 30 days.
- Report spam — from the ⋮ more menu. Moves to Spam and helps train our filters.
When viewing a message in Trash, Spam, or Archive, a Restore to Inbox button replaces Archive in the header. For messages already in Trash, Delete becomes Delete permanently.
Multi-select & bulk actions
Each row in the list has a checkbox. Selecting one or more reveals the Bulk actions bar above the list with: Archive, Delete, Spam, Mark unread/read, Star, Apply label, Clear selection. The select-all checkbox in the list head toggles all visible items.
Folders & labels
Built-in folders are fixed: Inbox, Starred, Snoozed, Sent, Drafts, Archive, Spam, Trash. Labels you create yourself (via the next to LABELS in the sidebar) are like Gmail labels — a message can have multiple, they don't move the message out of its folder.
Drag a message onto a folder or label in the sidebar to apply it. If you've selected multiple, all move.
Filters & rules
Filters are rules that run on incoming mail automatically. Create them in Settings → Filters. Each filter has:
- Conditions — one or more rows like "subject contains 'invoice'". You choose "match all" (AND) or "match any" (OR) across conditions.
- Actions — any combination of: apply label, move to folder, mark as read, skip inbox, star, forward to, delete.
- Apply to existing messages — optional; runs the rule once against your current mail.
Conversation threading
When enabled (Settings → Account → Interface), messages with the same normalized subject group into a thread. Each thread row shows a small chip with the message count. Disable threading if you prefer flat lists.
Smart banners
The reading pane surfaces helpful actions at the top of certain messages:
- Unsubscribe banner (amber) — appears on messages from likely mailing lists. One-click Unsubscribe + Block sender.
- Event detection banner (blue) — appears when we detect a date/time and a meeting keyword. "Add to calendar" opens the event modal pre-filled.
- Shared-inbox banner (purple) — appears when the message was sent to a shared address. Shows assignment picker, status pills, and internal notes.
Search basics
The search field is always accessible: press / to focus it. Typing anything searches across From, To, Subject, preview, and message body. Empty the field to restore the full folder view.
Search operators
Prefix a keyword with a colon to narrow results:
| Operator | Example | What it does |
|---|---|---|
from: | from:ada | Sender name, email, or domain contains the value |
to: | to:[email protected] | Recipient matches |
subject: | subject:"Q2 proposal" | Subject contains exact phrase (use quotes for spaces) |
label: | label:receipts | Message has the label |
has: | has:attachment | Message has attachments |
is: | is:starred · is:unread | State flags |
folder: | folder:sent | Scope to a specific folder |
before: | before:2026-04-01 | Date cutoff (ISO date) |
after: | after:2026-01-01 | Date start |
not: | not:newsletter | Exclude messages containing the term |
Combine freely: from:ada has:attachment after:2026-04-01.
Visual filter builder
Not a fan of typing operators? Click the funnel icon inside the search bar to open the Filter builder:
- Text fields: From, To, Subject, Has the words, Doesn't have
- Date range: After / Before
- Selects: In folder, Label
- Checkboxes: Has attachment, Starred, Unread
Click Apply filters and we build the operator query for you. Active filters show as dismissible chips above the message list — click × on any chip to remove just that filter.
Saved searches
The bookmark icon opens your saved searches. To save a query:
- Build it in the filter panel, then click Save search and name it.
- Or, with a query active in the search bar, click Save in the chip row.
Saved searches persist in your browser. Hover any saved row to rename or delete it.
Calendar — views & navigation
Open Calendar from the sidebar or the #calendar hash. Four views (Month, Week, Day, Agenda) via the toggle in the header. Use the prev/next arrows or the Today button to navigate.
Create & edit events
Click any empty calendar cell to start a new event pre-dated to that day. Click an existing event to edit. The event modal covers:
- Title, start & end, all-day toggle
- Location (Zoom link, room name, address)
- Attendees (comma-separated; they'll get invites)
- Notes, color (6 options), reminder (at time, 5 / 15 / 30 min, 1 hr, 1 day)
Event detection from email
When a message contains a meeting keyword and a date/time, the reading pane shows an "Add to calendar" banner. Clicking it opens the event modal with the subject as the title, the message snippet as notes, and the sender as an attendee. Adjust and save.
Files — browse & search
The Files hub (sidebar or #files) aggregates every attachment across every message you've sent or received, plus anything you've uploaded directly.
- Type filters with live counts: All, Images, PDFs, Documents, Spreadsheets, Other.
- Search matches file name, sender name, and sender email.
- Grid vs list toggle — grid shows a thumbnail preview by type; list shows name, shared-by, size, date in columns.
Upload & share
The Upload button opens a file picker (multi-select supported). Uploads appear at the top of the list immediately. Click any file to open the preview modal with:
- Download — original file.
- Share link — copies a public link to clipboard.
- Open original message — jumps back to the email the attachment came from (hidden for direct uploads).
Manage contacts
Contacts is a two-pane view: searchable list on the left, detail on the right. The button opens the contact modal (first, last, email, company, phone, notes). The detail pane shows their recent messages in your inbox — click one to jump back to mail.
Compose autocomplete
Typing a name or email in To / Cc / Bcc pulls matching contacts from your address book. Tab or click a suggestion to insert it. Autocomplete prefers contacts you email most often.
Shared inboxes
A shared inbox is an alias (like support@ or hello@) routed to multiple teammates. Admins create and configure them in Admin → Shared inboxes.
Assignment & status
Open a message in a shared inbox and you'll see a purple banner at the top:
- Assignment picker — assign the conversation to one teammate so you don't double-reply.
- Status pills — Open, Waiting, Closed. Color-coded; only one active at a time.
Internal notes
Below the status row, add internal notes visible only to teammates. Use @name to mention someone — they'll get notified. Notes stay with the thread forever and never go to the sender.
Create templates
Settings → Templates lets you create reusable drafts with a name, shortcut, subject, and body. We ship four defaults: /intro, /followup, /thanks, /welcome.
Variables
Four variables get substituted when the template is inserted:
{name}— recipient's name (derived from email local-part, title-cased){first_name}— just the first segment{my_name}— your display name{date}— today's date
Shortcut expansion
Two ways to use a template:
- Click the templates icon in compose — searchable popover.
- Type the shortcut in the compose body followed by a space (e.g.
/intro) and we replace it with the template content.
Account & appearance
Settings → Account covers:
- Profile — display name, profile photo, primary email (read-only; contact support to change).
- Regional — timezone, language, date format.
- Appearance — theme (Dark, Light, Match system), conversation view, dense layout, preview snippets.
Preferences persist in your browser via localStorage; log in from a new device and the interface reverts to defaults until you set them there too.
Security & 2FA
Settings → Security:
- Password — change requires your current password. We show the last-changed date.
- Authenticator 2FA — toggling on opens a modal with a QR code and a 6-digit verify field. Use Bitwarden, iCloud Keychain, or any TOTP app.
- SMS backup — optional second factor for when you lose your authenticator.
- Recovery codes — 10 codes, each valid once. Download them as .txt or copy all. Regenerate anytime (invalidates the previous batch).
- Active sessions — list of signed-in devices with revoke per row and a "Sign out of all other sessions" button.
- Delete account — permanent. Type
delete my accountto confirm. Separate from subscription cancellation.
Forwarding
Auto-forward incoming mail to another address:
- Enable the top toggle and enter a verified destination.
- Choose Everything or Only messages that match a filter.
- Decide what happens to the original: keep in inbox, mark as read, archive, or delete.
Signature
Plain signature with a basic formatting toolbar. Toggle "Insert before quoted text on replies" if you prefer your signature to appear above the thread rather than below it.
Vacation responder
Auto-reply when you're away. Set a date range, subject, body, and who receives it (everyone, only your contacts, or only people outside your company). The responder disables itself on the end date.
Aliases
Extra addresses that deliver to your mailbox and that you can send from. Add billing@, hello@, invoices@ — one mailbox, many identities. Mail to an alias lands in your inbox; pick the alias as the From address when you reply, and set one as your default sender if you like. To let teammates share an address, point the alias at a shared mailbox instead — a mailbox can be shared, an alias only delivers to one mailbox.
Safe & blocked senders
Two chip lists plus three phishing-protection toggles:
- Safe senders — always bypass spam. Add entries like
[email protected]or@acme.co(whole domain). - Blocked senders — straight to Spam. Same format.
- Warn on unknown senders — banner on first message from a new address.
- Scan links at click-time — we check URLs against threat feeds when you click.
- Block external images by default — kills tracking pixels.
Three behavior toggles:
- Sign every outgoing message (recommended).
- Encrypt when the recipient has a public key we know about.
- Require encryption for replies to already-encrypted threads.
Import public keys for contacts in the "Known public keys" section.
Billing
Three summary cards: Current plan, Next invoice, Payment method. Each has a primary action:
- Change plan — opens the plan picker with monthly/yearly toggle. Changes are prorated on your next invoice.
- View invoices — list of past invoices with PDF download.
- Update card — card form. Card processing is handled by a PCI-compliant payment processor; no card data ever lands in AXO's systems.
The Danger zone below has "Cancel subscription". We recommend cancellation over deletion — cancellation keeps your data recoverable for 30 days.
Admin — overview
Admin console (sidebar or #admin) is available to users with the Owner or Admin role. Seven tabs.
Overview shows four stat cards (mailboxes, storage, messages, avg response) and an onboarding checklist (domain verified, billing set, invite team, start migration).
Users & mailboxes
Table of users with role, status, storage and last activity. A teammate is a person with a mailbox: to invite someone, pick the mailbox on your account they will own and tell us where to send the invitation; they set a password and sign in with that mailbox address. You can also let them open other mailboxes you share with them. That is the difference between a mailbox and an alias — a mailbox can be shared with teammates, an alias only delivers to yours. Per-user actions: change access, reset password, remove.
Shared inboxes
Create and manage shared mailboxes. Each row shows the address, the teammates who can open it, open-message count and average response time. Click Manage to change who can see and respond; add aliases that deliver into it under Domains → Mail aliases.
Domains
Every domain you've connected, each with a DNS-health panel (MX, SPF, DKIM, DMARC) color-coded green/amber/red. Re-check DNS on demand. Add domain kicks off a verification flow.
Compliance & retention
Retention policy runs auto-deletion across all mailboxes in the org:
- Enable toggle + duration (default 365 days; caps by plan — Starter 1 year, Business 7 years, Enterprise unlimited).
- "Preserve sent mail separately" exempts sent.
- "Block external forwarding" prevents users from auto-forwarding to non-company domains.
Audit log
Admin actions are recorded — sign-ins, role changes, mailbox creation, password changes, integration tokens, DNS changes. Visible from Admin → Audit Log. Each entry shows who did what, when, and from which IP.
API & webhooks
AXO Email exposes a REST API for programmatic management of customers, domains, mailboxes, aliases, subscriptions, migrations, and webhooks. Plus an HMAC-signed webhook system that pushes lifecycle events to URLs you control — useful for building admin panels, billing reconciliation, customer-facing dashboards, or any app that needs to react to email outcomes without polling.
Base URL: https://app.axoemail.com/api/v1/company. Auth: bearer token in the Authorization header. Tokens are created from Admin → Settings → API Tokens — displayed once at creation, save immediately. Default rate limit: 60 requests/minute per token.
Two kinds of token. A management token reaches the endpoints in this section. A sending token reaches only the email sending API. They are separate credentials and neither can do the other's job: a sending token cannot read or change customers, domains, mailboxes or webhooks, and a management token cannot send mail. Both are limited to your own account's data. Give each of your applications only the token it actually needs, and rotate anything wider.
Available resources
- Customers —
GET / POST / PUT / DELETE /customers. CRUD for the people whose mail you host. - Domains —
GET / POST /domains,POST /domains/{id}/verify-dns,GET /domains/check,POST /domains/purchase(buy a domain — see Buying domains). Every response lists the DNS records to publish, including the DKIM records once they are issued, plus adkimblock with their status. - Mailboxes —
GET / POST / PUT / DELETE /mailboxes,POST /mailboxes/{id}/change-password,POST /mailboxes/{id}/import(IMAP migration from any provider). - Aliases —
GET / POST / PUT / DELETE /aliases. - Subscriptions —
GET / POST / PUT /subscriptions,POST /subscriptions/{id}/cancel,POST /subscriptions/{id}/reactivate. - Packages (read-only) —
GET /packages. - Usage —
GET /usage,GET /usage/domain/{id}. - Billing —
GET /billing,POST /billing/setup-session,GET /invoices. How your account is billed and what it has used. See Account billing. - Webhooks —
GET / POST / PUT / DELETE /webhooks,POST /webhooks/{id}/rotate-secret,POST /webhooks/{id}/resume. - Sub-senders —
GET / POST / DELETE /tenants,GET /tenants/{id}/metrics. An isolated sender reputation for each provider, brand or client you send for. See Sub-senders. - Suppressions (read-only) —
GET /suppressions, narrow with?tenant=,?reason=,?search=.
Webhook events
Subscribe to one or more event types when creating a webhook. AXO Email currently fires 24 distinct events. Subscribe only to what you act on — you can change the list at any time with PUT /api/v1/company/webhooks/{id}:
| Event | Fires when |
|---|---|
email.bounced | The recipient MTA rejected the message (Permanent or Transient) |
email.complained | The recipient flagged the message as spam |
email.delivered | The recipient's mail server accepted the message |
email.failed | The send permanently failed inside AXO (suppression, inactive domain, retries exhausted) |
email.sent | AXO accepted the message and handed it off for delivery. The first event of a normal send; carries the message_id every later event uses |
tenant.ip_pool_changed | A sub-sender moved between the shared IPs and the dedicated pool; data.reason is requested (we applied your request) or non_payment |
tenant.paused | A sub-sender's sending was paused because its reputation dropped; sends that name it fail with email.failed until it resumes |
tenant.resumed | A paused sub-sender may send again |
domain.dns_failed | A previously-verified domain falls out of compliance |
domain.dns_verified | All required DNS records pass verification |
domain.registered | A domain you bought through the API is registered; data.expires_at, data.dns_managed, data.tenants |
domain.registration_failed | The registration could not be completed; nothing was registered and we have been alerted |
domain.renewal_failed | The yearly renewal payment was declined (card accounts); update the card or the domain will lapse |
domain.renewed | A domain you bought through the API was renewed for another year |
mailbox.created | A new mailbox is provisioned on a domain |
mailbox.deleted | A mailbox is deleted |
mailbox.quota_warning | A mailbox crosses 80% of its quota |
mailbox.suspended | A mailbox is suspended (subscription lapse, abuse, etc.) |
subscription.activated | A subscription becomes active |
subscription.cancelled | A subscription is cancelled |
subscription.expired | A subscription period ends |
subscription.upgraded | A subscription is upgraded to a higher tier |
migration.completed | An IMAP migration finishes successfully |
migration.failed | An IMAP migration ends in failure |
Creating a subscription
curl -X POST https://app.axoemail.com/api/v1/company/webhooks \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "production-events",
"url": "https://yourapp.com/webhooks/axo",
"events": ["email.sent", "email.failed", "email.delivered", "email.bounced", "email.complained"]
}'
The response carries the webhook's signing secret once — store it; it is never shown again (POST /webhooks/{id}/rotate-secret issues a new one). A token that is not scoped to a single company must add "company_id" to the body.
Payload shape
Every event arrives as a POST with this envelope:
{
"event": "email.bounced",
"timestamp": "2026-05-08T18:00:00+00:00",
"data": {
"message_id": "010f019008e92c34-...-000000",
"email_id": "550e8400-e29b-41d4-a716-446655440000",
"tenant": "provider-one",
"from_email": "[email protected]",
"recipients": ["[email protected]"],
"reason": "Bounce (Permanent/General): [email protected]",
"bounce_type": "Permanent",
"bounce_sub_type": "General",
"details": { "...": "the delivery report as received" },
"event_at": "2026-05-08T17:59:42.123Z"
}
}
data.email_id is the id from your send response; data.message_id is the delivery-side id, present from email.sent onward and null on email.failed. data.tenant is the sub-sender slug when the send named one, otherwise null.
The shape of each event
Events come in six families. Everything in a family shares the fields shown in its sample, and each event adds a few of its own. Every event carries data.event_at; order by that, never by arrival.
Message events
One per message, then one per recipient outcome. email_id is the id from your send response and message_id is the delivery-side id, present from email.sent onward. Events in this family: email.sent, email.failed, email.delivered, email.bounced, email.complained.
{
"event": "email.sent",
"timestamp": "2026-09-16T18:00:00+00:00",
"data": {
"message_id": "010f019008e92c34-6a1b-...-000000",
"email_id": "550e8400-e29b-41d4-a716-446655440000",
"tenant": "acme-spa",
"from_email": "[email protected]",
"recipients": [
"[email protected]"
],
"event_at": "2026-09-16T17:59:42+00:00",
"sent_at": "2026-09-16T17:59:41+00:00"
}
}
| Event | Adds to the shape above |
|---|---|
email.sent | sent_at |
email.failed | sent_at, reason |
email.delivered | smtp_response, processing_time_ms, details |
email.bounced | reason, bounce_type, bounce_sub_type, details |
email.complained | reason, feedback_type, details |
Mailbox events
A mailbox on one of your domains was created, removed, filled up or switched off. Events in this family: mailbox.created, mailbox.deleted, mailbox.quota_warning, mailbox.suspended.
{
"event": "mailbox.created",
"timestamp": "2026-09-16T18:00:00+00:00",
"data": {
"mailbox_id": 918,
"email": "[email protected]",
"domain": "acmespa.com",
"event_at": "2026-09-16T17:59:42+00:00",
"quota_bytes": 53687091200
}
}
| Event | Adds to the shape above |
|---|---|
mailbox.created | quota_bytes |
mailbox.deleted | the shared fields above, nothing extra |
mailbox.quota_warning | percent_used, used_bytes, quota_bytes |
mailbox.suspended | reason |
Domain events
Verification of a domain you added, and the lifecycle of a domain you bought through the API. Events in this family: domain.dns_verified, domain.dns_failed, domain.registered, domain.registration_failed, domain.renewed, domain.renewal_failed.
{
"event": "domain.dns_verified",
"timestamp": "2026-09-16T18:00:00+00:00",
"data": {
"domain_id": 42,
"domain": "acmespa.com",
"event_at": "2026-09-16T17:59:42+00:00",
"verified": true,
"dkim_status": "verified",
"tenants": [
"acme-spa"
]
}
}
| Event | Adds to the shape above |
|---|---|
domain.dns_verified | verified, dkim_status, tenants |
domain.dns_failed | verified, dkim_status, tenants |
domain.registered | registered, expires_at, auto_renew, dns_managed, tenants, error |
domain.registration_failed | registered, expires_at, auto_renew, dns_managed, tenants, error |
domain.renewed | renewed, expires_at, error |
domain.renewal_failed | renewed, expires_at, error |
Sub-sender events
Reputation pauses mirrored from the sending platform, and moves between the shared IPs and the dedicated pool. Events in this family: tenant.paused, tenant.resumed, tenant.ip_pool_changed.
{
"event": "tenant.paused",
"timestamp": "2026-09-16T18:00:00+00:00",
"data": {
"tenant": "acme-spa",
"tenant_id": 7,
"name": "Acme Spa",
"event_at": "2026-09-16T17:59:42+00:00",
"sending_status": "DISABLED",
"previous_status": "ENABLED",
"changed_at": "2026-09-16T17:59:42+00:00",
"source": "provider"
}
}
| Event | Adds to the shape above |
|---|---|
tenant.paused | sending_status, previous_status, changed_at, source |
tenant.resumed | sending_status, previous_status, changed_at, source |
tenant.ip_pool_changed | ip_pool, previous_ip_pool, reason, changed_at |
Subscription events
The plan behind one of your domains changed state. Events in this family: subscription.activated, subscription.cancelled, subscription.expired, subscription.upgraded.
{
"event": "subscription.activated",
"timestamp": "2026-09-16T18:00:00+00:00",
"data": {
"subscription_id": 31,
"domain": "acmespa.com",
"customer": "Acme Spa",
"package": "Business",
"status": "active",
"event_at": "2026-09-16T17:59:42+00:00"
}
}
| Event | Adds to the shape above |
|---|---|
subscription.activated | the shared fields above, nothing extra |
subscription.cancelled | the shared fields above, nothing extra |
subscription.expired | the shared fields above, nothing extra |
subscription.upgraded | the shared fields above, nothing extra |
Migration events
An IMAP import you started with POST /mailboxes/{id}/import finished. Events in this family: migration.completed, migration.failed.
{
"event": "migration.completed",
"timestamp": "2026-09-16T18:00:00+00:00",
"data": {
"migration_id": 12,
"mailbox": "[email protected]",
"status": "completed",
"messages_done": 4210,
"error": null,
"event_at": "2026-09-16T17:59:42+00:00"
}
}
| Event | Adds to the shape above |
|---|---|
migration.completed | the shared fields above, nothing extra |
migration.failed | status, error |
Delivery, retries and ordering
- Timeout. We wait up to 10 seconds for your 2xx. Do the work in your own queue and answer immediately.
- Retries. A non-2xx, a timeout or a connection error is retried twice more, about 10 seconds and then 1 minute and then 5 minutes after the first attempt. After that the event is dropped and logged on our side.
- At least once, not exactly once. A delivery you already processed can arrive again. Dedupe on
(data.message_id, event)for message events and on(data.*_id, event, data.event_at)for the rest. - No ordering guarantee. Events are dispatched independently, so
email.deliveredcan reach you beforeemail.sent. Order bydata.event_at, never by arrival. - One request per event per endpoint. A message to three recipients produces one
email.sentand then one delivery-side event per recipient. - Unknown events. We add events over time. Ignore any
eventyou do not recognise instead of failing the request, or you will start rejecting deliveries the day we add one. - We pause an endpoint that keeps failing. After 10 consecutive failed deliveries we stop sending to it and email your account contact. Events that fire while it is paused are not stored and are not replayed. One successful delivery clears the counter on its own; to start again after a pause, call
POST /api/v1/company/webhooks/{id}/resume.GET /api/v1/company/webhooksshowspaused,failure_countandfailure_thresholdfor every endpoint.
Verifying the signature
Every request includes X-Webhook-Signature (hex HMAC-SHA256 of the raw request body, keyed with your secret) and X-Webhook-Signature-Algorithm: sha256. Reject any request whose signature doesn't match.
// PHP
$expected = hash_hmac('sha256', $rawBody, $secret);
if (!hash_equals($expected, $request->header('X-Webhook-Signature'))) {
return response('', 401);
}
// Node.js
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected, 'hex'),
Buffer.from(req.get('X-Webhook-Signature'), 'hex'))) {
return res.status(401).send();
}
Verify against the raw request body, not parsed-then-re-encoded JSON — formatting differences (whitespace, key order) will produce a different hash.
2xx within ~10 seconds (queue the work in your app if needed). Dedupe by (message_id, event) — duplicates are possible during retries. Be tolerant of unknown event types — log and 200; new events ship without breaking existing subscribers.
Open and click tracking is by design app-side
AXO Email does not provide email.opened or email.clicked as webhook events, even though the underlying mail relay technically supports it. Reasoning:
- No content modification. Email body is byte-exact what you submitted. No injected pixels, no link rewrites by AXO.
- No third-party redirector domain visible to your recipients (no redirect domain in your links other than your own).
- Per-app data ownership. Your tracker DB is yours alone — AXO doesn't sit between you and your engagement data.
- Brand-consistent. Your tracker host (e.g.
track.yourdomain.com) lives in your stack.
The pattern: inject your own pixel and rewrite <a href> links to a redirect endpoint you control, before submitting the message to AXO. DKIM stays valid because AXO signs after your modifications. Detailed walkthrough is available on request — contact your account manager.
Email sending API
Separate from the management API above, the transactional send API hands finished messages to AXO for delivery. Base URL: https://app.axoemail.com/api/v1. Auth: a sending token in the Authorization header. This is a different credential from your management token and is meant for the application that sends your mail: it reaches only the endpoints in this section, and it can send only as domains registered to your own account. The from_email domain must be one you've registered, activated, and verified. Ask your account manager to issue one.
POST /emails/send
Queues a single message. Returns 202 Accepted immediately; delivery happens asynchronously.
curl -X POST https://app.axoemail.com/api/v1/emails/send \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8f14e45f-ceea-467d-9c1b-1a2b3c4d5e6f" \
-d '{
"to": ["[email protected]"],
"subject": "Your receipt",
"text": "Plain-text body",
"html": "<p>HTML body</p>",
"from_email": "[email protected]",
"from_name": "Your Co Billing",
"reply_to": "[email protected]"
}'
Request fields — to (required, 1–50 addresses), cc/bcc (optional, ≤20 each), subject (required, ≤200 chars, no CR/LF), text and/or html (body), from_email (a verified sending domain), from_name (optional display name, ≤100 chars, no CR/LF), reply_to (optional), scheduled_at (optional ISO-8601 in the future — omit or null to send now), attachments (optional — see below). Two optional additions: tenant, a sub-sender slug (see Sub-senders), and list_unsubscribe (see Unsubscribe headers).
Attachments
Attach files by adding an attachments array. Each entry carries the file inline as base64 — there is no separate upload step and no URL fetching:
"attachments": [
{
"filename": "invoice.pdf",
"content": "JVBERi0xLjQKJ...base64-encoded file bytes...",
"content_type": "application/pdf"
}
]
Per-attachment fields — filename (required, ≤255 chars, no CR/LF), content (required, standard base64 of the raw file bytes), content_type (optional MIME type used as the attachment's label; if omitted it's detected from the bytes).
Limits — up to 10 attachments per message, 6 MB per file, and 6 MB total per message (decoded). The whole request body must stay under 12 MB; remember base64 inflates the bytes by ~37%, so a 6 MB file is ~8.2 MB on the wire. Oversized files return 422; an over-limit request body returns 413.
Blocked types — executable and script attachments are rejected by both file extension (.exe, .js, .bat, .vbs, .hta, …) and detected content, regardless of the declared content_type. Ordinary document, image, and archive types (PDF, CSV, XLSX, PNG, ZIP, …) are accepted.
On /emails/batch, attachments are per message — nest them under each entry as messages[].attachments, not at the top level.
Success — 202 Accepted:
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "queued",
"scheduled_at": null,
"tenant": null
}
}
A scheduled send returns "status": "scheduled" with scheduled_at echoed back.
Success contract — key on the body, not the exact code. Treat any 2xx whose body contains data.id with a status of queued or scheduled as success. The accept code is 202 across /emails/send, /emails/template, /emails/batch, and /emails/{id}/retry — but keying on the body keeps your client robust even if an intermediary normalizes the status code.
Response & error codes
| Code | Meaning |
|---|---|
202 | Accepted & queued (or scheduled). Body as above. |
400 | Malformed Idempotency-Key (must be 1–255 printable ASCII characters). |
409 | Idempotency conflict — the key was reused with a different payload, or the original request is still in flight (see below). |
413 | Request body too large — exceeds the 12 MB limit (usually oversized attachments). Body: { "error": "Request body too large", "max_request_bytes": 12582912 }. |
422 | Validation error, an unregistered/inactive/unverified from_email domain, one or more suppressed recipients, or an invalid attachment (shapes below). |
429 | Rate limit exceeded — includes a Retry-After header (seconds) plus X-RateLimit-Limit/X-RateLimit-Remaining. Back off and retry. |
Unregistered / unverified sending domain (422):
{ "error": "The from_email domain is not registered in the platform" }
Suppressed recipients (422) — one or more addresses are on the hard-bounce / complaint suppression list for that domain:
{
"error": "One or more recipient addresses are suppressed",
"suppressed_addresses": [
{ "email": "[email protected]", "reason": "hard_bounce", "since": "2026-06-01T12:00:00+00:00" }
]
}
Invalid attachment (422) — attachment_errors names each failing attachment by index and filename; on /emails/batch each entry also carries a message_index:
{
"error": "One or more attachments are invalid",
"attachment_errors": [
{ "index": 1, "filename": "big.pdf", "reason": "Attachment exceeds the per-file limit of 6291456 bytes" }
]
}
Rate limits
/emails/send, /emails/template, and /emails/batch are limited to 30 requests/minute per token. Exceeding it returns 429 with a Retry-After header — honour it and back off. (The 60/min figure in the management-API section is the default for the management endpoints, not for sending.)
Idempotency-Key — safe retries
Supply an Idempotency-Key header (any unique string ≤255 chars — a UUID is ideal) to make retries safe. AXO records the first response for that key and replays it on any retry within 24 hours instead of sending again — so a network timeout or ambiguous failure can be retried with zero double-send risk.
- A replayed response carries
Idempotent-Replayed: truewith the original status and body. - Reusing a key with a different request body returns
409— use a fresh key per distinct message. - Retries must send a byte-identical body to replay (the key is matched against a hash of the raw request). A request still in flight returns
409— retry shortly to get the cached result. - Keys are scoped to your token and expire after 24 hours. Without the header, behaviour is unchanged.
Template & batch
POST /emails/template sends from a saved template with {{variable}} substitution; POST /emails/batch sends many messages in one call. Both use the same 202 accept contract, the same error codes, and accept the same Idempotency-Key header and attachments (on /emails/batch, per message under messages[].attachments). POST /emails/{id}/retry re-queues a previously-failed send and also returns 202.
Message status
GET /emails/{id} (full record) and GET /emails/{id}/status (lightweight) return one of: queued, scheduled, processing, sent (handed off for delivery), delivered (the recipient's server accepted it), bounced, complained, failed. Treat sent and delivered as success; delivered arrives when the delivery confirmation does, usually within seconds, and the same transition fires the email.delivered webhook.
Sub-senders
If you send on behalf of several providers, brands or clients under one verified domain, give each a sub-sender. Each sub-sender has its own sender reputation, kept separately from every other one and from the rest of your account, and is paused on its own if that reputation drops — one bad list cannot stop the others. Create them with the management API, then name one on each send:
POST /api/v1/company/tenants {"name": "Provider One"} → {"data": {"slug": "provider-one", "sending_status": "ENABLED", ...}}
POST /api/v1/emails/send {..., "tenant": "provider-one"}
tenantaccepts the slug or the numeric id and is valid on/emails/send,/emails/template, and per message on/emails/batch. It must belong to the company that owns thefrom_emaildomain, otherwise the request is refused with422.- Every
email.*webhook payload carriesdata.tenant, and suppressions raised by a sub-sender's bounces and complaints are tagged with it, so everything attributes per sub-sender on your side. GET /api/v1/company/tenants/{id}/metrics?days=30returns counts ofsent,failed,queued,delivered,bounced,complainedand the correspondingratesfor a 1–90 day window, plusdaily: one entry per day with the same counts and abounce_rate/complaint_rate, so a bad day is visible before it becomes a pause.- Own-domain sending. A sub-sender may send from any of your verified domains that is attached to it. Domains verified when the sub-sender was created are attached automatically. To let a sub-sender send from its own domain: add the domain (
POST /api/v1/company/domainswith"tenant": "provider-one", or attach an existing one withPOST /api/v1/company/tenants/{id}/domains {"domain": "their-domain.com"}), publish the DNS records the response returns, then pollPOST /api/v1/company/domains/{id}/verify-dnsuntilverification.verifiedistrue— or listen fordomain.dns_verified.GET /tenants/{id}listsdomainswithreadyper domain.DELETE /tenants/{id}/domains/{domainId}detaches. - Dedicated IP pool (Premium). A sub-sender can send from a dedicated IP pool instead of the shared platform IPs, for $50 per month per sub-sender. Request it with
POST /api/v1/company/tenants/{id}/ip-pool {"pool": "dedicated"}(or"shared"to move back); we apply the switch once your account has a card on file, andGET /tenants/{id}showsip_poolandip_pool_requested. The pool is billed to your account like every other paid option — see Account billing. The first month is billed when a sub-sender moves onto the pool, then $50 per month per sub-sender; moving back to shared drops the charge from the next cycle. If your account bills by card and the charge cannot be collected after our retries, the sub-sender returns to the shared pool automatically and you receivetenant.ip_pool_changedwithreason: "non_payment"; sending is never interrupted by this. Warm-up runs for about 45 days on our side. A dedicated IP only helps once a sub-sender sends on the order of a thousand messages a day; below that the shared pool delivers better, and we'll say so. - A send that names a sub-sender must use a
from_emailon a domain attached to it and verified; otherwise the request is refused with422and a message saying which step is missing. Sends are never silently switched to another domain. sending_statusonGET /tenants/{id}isENABLED,DISABLED(paused) orREINSTATED. Transitions firetenant.paused/tenant.resumed; expect up to an hour between the change and the event. Sends that name a paused sub-sender fail immediately withemail.failed.GET /api/v1/company/suppressions?tenant=provider-onelists the addresses that sub-sender may no longer send to.
Sends without tenant are unaffected. Sub-senders are enabled per account — if POST /tenants answers 503, ask support to switch them on.
Unsubscribe headers
For marketing mail, add a list_unsubscribe object to the send and AXO sets the standard List-Unsubscribe header — and, with one_click, the List-Unsubscribe-Post: List-Unsubscribe=One-Click header (RFC 8058) that major mailbox providers use for their one-click unsubscribe button:
"list_unsubscribe": {
"url": "https://yourapp.com/u/<token>",
"mailto": "[email protected]",
"one_click": true
}
url must be https; one_click requires url; supply at least one of url / mailto. The one-click POST goes straight to your endpoint, so there is no unsubscribe event to subscribe to. Same shape per message on /emails/batch.
Domains & DKIM records
POST /api/v1/company/domains registers a sending domain and returns dns_records: every record to publish, each with type, name (relative, @ for the apex), fqdn, value, purpose and, for MX, priority. A dkim block reports mode, status and just the signing records. The DKIM records are issued shortly after creation, usually within a minute: GET /api/v1/company/domains/{id} shows them once they exist, POST .../{id}/verify-dns re-checks, and the domain.dns_verified / domain.dns_failed events tell you when the domain is ready — subscribe to those instead of polling.
A domain you add this way stays at its own registrar: GET /domains/{id} reports registration.status: "external" and we never renew or bill it. Domains you buy through us (next section) report pending, registered or failed, with expires_at and dns_managed.
Buying domains for customers
When a customer wants their own custom email and doesn't have a domain, buy one for them through the API. The domain is registered in your account's name, its DNS for mail is published on our edge automatically (no records for anyone to copy), it is enrolled for sending, and it renews yearly on your account.
GET /api/v1/company/domains/check?domain=acmespa.com
{ "data": { "domain": "acmespa.com", "available": true, "purchasable": true, "reason": null,
"price": { "registration_cents_per_year": 1995, "renewal_cents_per_year": 1995,
"privacy_cents_per_year": 700, "currency": "usd" },
"years": { "min": 1, "max": 10 }, "already_registered_here": false } }
POST /api/v1/company/domains/purchase
{ "domain": "acmespa.com", "years": 1, "privacy": true, "customer_id": 12, "tenant": "acme-spa" }
202 { "data": { ...domain... },
"registration": { "status": "pending", "auto_renew": true, "dns_managed": false },
"billing": { "result": "invoiced", "total_cents": 1995, "invoice_number": "AXO-2026-000123", "invoice_due_date": "2026-10-11" } }
years1–10 (default 1).privacy(defaulttrue) keeps the registrant details out of public WHOIS; turning it off requirescustomer_id, whose postal address becomes the public registrant.customer_id(optional) files the domain under one of your customers;tenant(optional) attaches it to a sub-sender so it can send from it as soon as it is ready.- Price is per year at our published rate;
checktells you the exact figure before you commit.purchasable: falsecomes with areason(taken,premium,too_expensive, …). billing.resultispaid,invoicedorrecordeddepending on how your account is billed (next section). A declined card returns402 payment_declinedand nothing is registered.- Registration completes in the background: you receive
domain.registered(withexpires_atanddns_managed) ordomain.registration_failed. Thendomain.dns_verifiedfollows once the records propagate, exactly as for a domain you added yourself. - Renewals happen automatically each year and are billed to your account the same way; you receive
domain.renewed, ordomain.renewal_failedif a card payment was declined.
Mailboxes for customers
Sell a business mailbox at a customer's domain as an add-on. Everything runs under your company token; the mailbox belongs to a domain, and the domain carries the customer (customer_id) and any sub-senders it is attached to, so each mailbox lands on the right customer in your billing.
Order of operations. A domain hosts mail once it has a plan: POST /api/v1/company/subscriptions {"customer_id", "domain_id", "package_id", "price_id"} (plans and prices from GET /api/v1/company/packages; the plan sets the mailbox size and alias allowance — the amount you pay is your account rate, not the listed price). Then create mailboxes:
POST /api/v1/company/mailboxes
{ "domain_id": 42, "local_part": "front", "password": "at least 12 characters", "name": "Front desk",
"quota_bytes": 10737418240 } // optional; defaults to the plan size
201 { "data": { "id": 918, "email": "[email protected]", "name": "Front desk", "quota_bytes": 10737418240,
"used_bytes": 0, "is_active": true, "type": "regular", "domain": { ... } },
"client_settings": { "webmail_url": "https://mail.axoemail.com",
"imap": { "host": "mail.axoemail.com", "port": 993, "security": "ssl" },
"smtp": { "host": "mail.axoemail.com", "port": 465, "security": "ssl" },
"username": "the full mailbox address" },
"tenants": ["acme-spa"] }
- Passwords. You set the initial password on create (minimum 12 characters) and hand it to the user; the password is never returned.
POST /mailboxes/{id}/change-password {"password"}sets a new one at any time — that is also the reset path for a user who forgot theirs. There is no self-service reset-by-email for mailbox users today. - No cap per domain. Create as many mailboxes on a customer's domain as they buy; you are billed per active mailbox at your account rate.
quota_bytesmay be raised or lowered withPUT /mailboxes/{id}up to the plan size. - Which domains qualify. Any domain of yours with a plan. A domain bought through us is ready as soon as it is registered. A domain the customer brings needs the records in
dns_recordsfromPOST /domains— that list already includes the MX and the mail-host A record for receiving, alongside the sending records;verification.mx_verifiedonGET /domains/{id}is the "can receive" flag,verification.verifiedthe "can send" flag. - Plans and sizes.
GET /packageslists them. Your per-mailbox rate is tiered by plan;GET /billing→rates.mailbox_month_cents_by_plangives the figure for each. - Lifecycle.
PUT /mailboxes/{id} {"is_active": false}pauses a mailbox: sign-in stops immediately and inbound mail is refused so the sender gets a bounce (nothing is held). Setting it active again restores it.DELETEremoves the address and archives the contents; the address can be reused straight away, and the archived data is kept for the platform's recovery window (currently 90 days) before it is purged.mailbox.suspendedis only sent when the platform pauses a mailbox itself (payment enforcement) and is reversed automatically when payment is restored. - Import.
POST /mailboxes/{id}/import {"source_host", "source_port" (default 993, SSL), "source_email", "source_password", "target_password", "search_filter"?}copies mail from the user's old mailbox in the background;GET /mailboxes/{id}/importreportsstatus,messages_total,messages_done,bytes_transferred;migration.completed/migration.failedfire at the end. - Webhooks.
mailbox.createdandmailbox.deletedcarrymailbox_id,email,domain(created addsquota_bytes);mailbox.quota_warningcarriesmailbox_id,email,percent_used,used_bytes,quota_bytes;mailbox.suspendedcarriesmailbox_id,email,reason.
In GET /company/billing, current_usage.mailboxes_by_domain lists every domain with its customer_id, attached tenants and active mailbox count, so you can put each seat on the right customer's invoice.
Account billing
Your company account is billed for what it uses on the platform — mailboxes and hosted domains at the rates agreed with you, sub-senders on the dedicated pool, and one-off purchases such as domains. GET /api/v1/company/billing shows all of it:
billing_mode— how your account settles:card(charged monthly to the card on file),invoice(a monthly invoice is emailed tobilling_emailwithpayment_terms_days), ortracked(usage is recorded under a separate agreement and no invoice is produced).rates— your monthly per-mailbox rate for each plan (mailbox_month_cents_by_plan, tiered by mailbox size), your per-domain rate and the dedicated-pool rate.current_usage— the open period: itslines(kind, quantity, unit and total),pending_one_offs, andnext_close_at.open_invoices(withoverdue) and the last twelvestatements, each with its lines and, where one was produced, the invoice number and status.GET /api/v1/company/invoiceslists invoices in full.cardandcard_required— for card-billed accounts,POST /api/v1/company/billing/setup-session {"success_url", "cancel_url"}returns a hosted card form URL (https URLs only; card data never touches AXO). Paid options are refused with402 card_requireduntil a card is stored.
Usage is counted nightly and each monthly period closes automatically. Card accounts are charged at the close and receive a receipt; if a charge cannot be collected we retry over the following days and tell you by email before anything is withdrawn. Invoice accounts receive the invoice and are never interrupted. Nothing about how your customers' mailboxes work depends on the mode.
Using the migration wizard
Open from Admin → Overview ("Start migration") or the shortcut __openMigrationModal(). Four steps:
- Pick your old provider (Gmail, Google Workspace, Outlook, Microsoft 365, Fastmail, any IMAP).
- Enter old-account credentials or an app password if you have 2FA. We only request read access.
- Watch the live event stream as we copy mail, contacts, and (optionally) calendar events.
- Done — we also set up 30-day forwarding from your old account.
Typical migration of a 50 GB mailbox completes in under 3 hours. Read/unread state and folder structure are preserved.
Concierge migration
The $49 Concierge add-on at signup includes white-glove migration. Our team handles DNS, imports mail, configures your devices, and confirms when you're ready to send. Enterprise customers get Concierge free.
Install as a PWA
The web app is a Progressive Web App. On supported browsers (Chrome, Edge, Safari) you'll see an install banner at the bottom-right of mail — click Install to add AXO to your home screen or dock.
Once installed, the app gets an offline cache of the shell and most recent mail, plus push notifications (you'll be asked to grant permission on first install).
Native mail apps
AXO works with any IMAP/SMTP client. See our setup guide for step-by-step on iOS Mail, Android Gmail, Apple Mail, Outlook, Thunderbird, and generic IMAP. Server settings:
- IMAP:
imap.axoemail.com· port 993 · SSL/TLS - SMTP:
smtp.axoemail.com· port 465 · SSL/TLS
Keyboard shortcuts
| Key | Action |
|---|---|
| j / k | Next / previous message |
| Enter | Open selected |
| c | Compose new message |
| r | Reply |
| e | Archive |
| # | Delete |
| s | Star / unstar |
| z | Snooze |
| / | Focus search |
| g i | Go to Inbox |
| g s | Go to Starred |
| g t | Go to Sent |
| g d | Go to Drafts |
| g , | Go to Settings |
| ? | Show shortcut help |
| Esc | Close modal / back |
| ⌘B / ⌘I / ⌘U | Bold / Italic / Underline (in compose) |
| ⌘⏎ | Send message (in compose) |
Search operators — full reference
| Operator | Accepts | Example |
|---|---|---|
from: | string | from:[email protected] |
to: | string | to:support@ |
subject: | string (quote phrases) | subject:"board deck" |
label: | label key | label:receipts |
folder: | folder name | folder:archive |
has: | attachment | has:attachment |
is: | starred, unread | is:unread |
not: | string | not:newsletter |
before: | ISO date | before:2026-01-01 |
after: | ISO date | after:2025-12-01 |
File type support
The Files hub groups attachments by category:
- Images: png, jpg, jpeg, gif, webp, svg, heic
- PDFs: pdf
- Documents: doc, docx, odt, txt, md, rtf
- Spreadsheets: xls, xlsx, csv, ods, numbers
- Other: zip, rar, 7z, tar, gz, mp3, wav, mp4, mov, avi, fig, everything else
Max attachment size is 50 MB per file, 100 MB per message total (Enterprise: 200 MB / 500 MB).