Documentation

Everything you need to master AXO Email.

Start with Getting started, skim the feature guides, or jump straight to keyboard shortcuts. Use ⌘K to search these docs.

Try "keyboard shortcuts", "migration", "filter rules", "shared inboxes".

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.

Five-minute tour. On your first sign-in, a built-in tour spotlights the Compose button, sidebar, search, Contacts, and avatar menu. Skip it or take it — you can relaunch it anytime by running __axoStartTour() in the browser console.

Create an account

Visit signup and complete the five-step wizard:

  1. 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.
  2. Plan — Starter, Business, or Enterprise, billed monthly or yearly. You can change this later.
  3. Setup path — DIY (free) or Concierge ($49 one-time, we handle the DNS work).
  4. 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.
  5. 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.
  • Amail → the address shown in your setup panel. This is where mail.yourdomain.com points; 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)_dmarcv=DMARC1; p=none to start; tighten to p=quarantine once 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).

Propagation time. DNS changes usually propagate within 15 minutes but can take up to 48 hours depending on your registrar's TTL. You'll see a live "Verified" chip in Admin → Domains once each record is detected.

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:

OperatorExampleWhat it does
from:from:adaSender 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:receiptsMessage has the label
has:has:attachmentMessage has attachments
is:is:starred · is:unreadState flags
folder:folder:sentScope to a specific folder
before:before:2026-04-01Date cutoff (ISO date)
after:after:2026-01-01Date start
not:not:newsletterExclude 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 account to 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

  • CustomersGET / POST / PUT / DELETE /customers. CRUD for the people whose mail you host.
  • DomainsGET / 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 a dkim block with their status.
  • MailboxesGET / POST / PUT / DELETE /mailboxes, POST /mailboxes/{id}/change-password, POST /mailboxes/{id}/import (IMAP migration from any provider).
  • AliasesGET / POST / PUT / DELETE /aliases.
  • SubscriptionsGET / POST / PUT /subscriptions, POST /subscriptions/{id}/cancel, POST /subscriptions/{id}/reactivate.
  • Packages (read-only) — GET /packages.
  • UsageGET /usage, GET /usage/domain/{id}.
  • BillingGET /billing, POST /billing/setup-session, GET /invoices. How your account is billed and what it has used. See Account billing.
  • WebhooksGET / POST / PUT / DELETE /webhooks, POST /webhooks/{id}/rotate-secret, POST /webhooks/{id}/resume.
  • Sub-sendersGET / 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}:

EventFires when
email.bouncedThe recipient MTA rejected the message (Permanent or Transient)
email.complainedThe recipient flagged the message as spam
email.deliveredThe recipient's mail server accepted the message
email.failedThe send permanently failed inside AXO (suppression, inactive domain, retries exhausted)
email.sentAXO 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_changedA sub-sender moved between the shared IPs and the dedicated pool; data.reason is requested (we applied your request) or non_payment
tenant.pausedA sub-sender's sending was paused because its reputation dropped; sends that name it fail with email.failed until it resumes
tenant.resumedA paused sub-sender may send again
domain.dns_failedA previously-verified domain falls out of compliance
domain.dns_verifiedAll required DNS records pass verification
domain.registeredA domain you bought through the API is registered; data.expires_at, data.dns_managed, data.tenants
domain.registration_failedThe registration could not be completed; nothing was registered and we have been alerted
domain.renewal_failedThe yearly renewal payment was declined (card accounts); update the card or the domain will lapse
domain.renewedA domain you bought through the API was renewed for another year
mailbox.createdA new mailbox is provisioned on a domain
mailbox.deletedA mailbox is deleted
mailbox.quota_warningA mailbox crosses 80% of its quota
mailbox.suspendedA mailbox is suspended (subscription lapse, abuse, etc.)
subscription.activatedA subscription becomes active
subscription.cancelledA subscription is cancelled
subscription.expiredA subscription period ends
subscription.upgradedA subscription is upgraded to a higher tier
migration.completedAn IMAP migration finishes successfully
migration.failedAn 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"
    }
}
EventAdds to the shape above
email.sentsent_at
email.failedsent_at, reason
email.deliveredsmtp_response, processing_time_ms, details
email.bouncedreason, bounce_type, bounce_sub_type, details
email.complainedreason, 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
    }
}
EventAdds to the shape above
mailbox.createdquota_bytes
mailbox.deletedthe shared fields above, nothing extra
mailbox.quota_warningpercent_used, used_bytes, quota_bytes
mailbox.suspendedreason

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"
        ]
    }
}
EventAdds to the shape above
domain.dns_verifiedverified, dkim_status, tenants
domain.dns_failedverified, dkim_status, tenants
domain.registeredregistered, expires_at, auto_renew, dns_managed, tenants, error
domain.registration_failedregistered, expires_at, auto_renew, dns_managed, tenants, error
domain.renewedrenewed, expires_at, error
domain.renewal_failedrenewed, 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"
    }
}
EventAdds to the shape above
tenant.pausedsending_status, previous_status, changed_at, source
tenant.resumedsending_status, previous_status, changed_at, source
tenant.ip_pool_changedip_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"
    }
}
EventAdds to the shape above
subscription.activatedthe shared fields above, nothing extra
subscription.cancelledthe shared fields above, nothing extra
subscription.expiredthe shared fields above, nothing extra
subscription.upgradedthe 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"
    }
}
EventAdds to the shape above
migration.completedthe shared fields above, nothing extra
migration.failedstatus, 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.delivered can reach you before email.sent. Order by data.event_at, never by arrival.
  • One request per event per endpoint. A message to three recipients produces one email.sent and then one delivery-side event per recipient.
  • Unknown events. We add events over time. Ignore any event you 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/webhooks shows paused, failure_count and failure_threshold for 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.

Best practices. Respond 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 fieldsto (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 fieldsfilename (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

CodeMeaning
202Accepted & queued (or scheduled). Body as above.
400Malformed Idempotency-Key (must be 1–255 printable ASCII characters).
409Idempotency conflict — the key was reused with a different payload, or the original request is still in flight (see below).
413Request body too large — exceeds the 12 MB limit (usually oversized attachments). Body: { "error": "Request body too large", "max_request_bytes": 12582912 }.
422Validation error, an unregistered/inactive/unverified from_email domain, one or more suppressed recipients, or an invalid attachment (shapes below).
429Rate 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: true with 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"}
  • tenant accepts 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 the from_email domain, otherwise the request is refused with 422.
  • Every email.* webhook payload carries data.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=30 returns counts of sent, failed, queued, delivered, bounced, complained and the corresponding rates for a 1–90 day window, plus daily: one entry per day with the same counts and a bounce_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/domains with "tenant": "provider-one", or attach an existing one with POST /api/v1/company/tenants/{id}/domains {"domain": "their-domain.com"}), publish the DNS records the response returns, then poll POST /api/v1/company/domains/{id}/verify-dns until verification.verified is true — or listen for domain.dns_verified. GET /tenants/{id} lists domains with ready per 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, and GET /tenants/{id} shows ip_pool and ip_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 receive tenant.ip_pool_changed with reason: "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_email on a domain attached to it and verified; otherwise the request is refused with 422 and a message saying which step is missing. Sends are never silently switched to another domain.
  • sending_status on GET /tenants/{id} is ENABLED, DISABLED (paused) or REINSTATED. Transitions fire tenant.paused / tenant.resumed; expect up to an hour between the change and the event. Sends that name a paused sub-sender fail immediately with email.failed.
  • GET /api/v1/company/suppressions?tenant=provider-one lists 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" } }
  • years 1–10 (default 1). privacy (default true) keeps the registrant details out of public WHOIS; turning it off requires customer_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; check tells you the exact figure before you commit. purchasable: false comes with a reason (taken, premium, too_expensive, …).
  • billing.result is paid, invoiced or recorded depending on how your account is billed (next section). A declined card returns 402 payment_declined and nothing is registered.
  • Registration completes in the background: you receive domain.registered (with expires_at and dns_managed) or domain.registration_failed. Then domain.dns_verified follows 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, or domain.renewal_failed if 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_bytes may be raised or lowered with PUT /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_records from POST /domains — that list already includes the MX and the mail-host A record for receiving, alongside the sending records; verification.mx_verified on GET /domains/{id} is the "can receive" flag, verification.verified the "can send" flag.
  • Plans and sizes. GET /packages lists them. Your per-mailbox rate is tiered by plan; GET /billingrates.mailbox_month_cents_by_plan gives 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. DELETE removes 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.suspended is 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}/import reports status, messages_total, messages_done, bytes_transferred; migration.completed / migration.failed fire at the end.
  • Webhooks. mailbox.created and mailbox.deleted carry mailbox_id, email, domain (created adds quota_bytes); mailbox.quota_warning carries mailbox_id, email, percent_used, used_bytes, quota_bytes; mailbox.suspended carries mailbox_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 to billing_email with payment_terms_days), or tracked (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: its lines (kind, quantity, unit and total), pending_one_offs, and next_close_at.
  • open_invoices (with overdue) and the last twelve statements, each with its lines and, where one was produced, the invoice number and status. GET /api/v1/company/invoices lists invoices in full.
  • card and card_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 with 402 card_required until 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:

  1. Pick your old provider (Gmail, Google Workspace, Outlook, Microsoft 365, Fastmail, any IMAP).
  2. Enter old-account credentials or an app password if you have 2FA. We only request read access.
  3. Watch the live event stream as we copy mail, contacts, and (optionally) calendar events.
  4. 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

KeyAction
j / kNext / previous message
EnterOpen selected
cCompose new message
rReply
eArchive
#Delete
sStar / unstar
zSnooze
/Focus search
g iGo to Inbox
g sGo to Starred
g tGo to Sent
g dGo to Drafts
g ,Go to Settings
?Show shortcut help
EscClose modal / back
⌘B / ⌘I / ⌘UBold / Italic / Underline (in compose)
⌘⏎Send message (in compose)

Search operators — full reference

OperatorAcceptsExample
from:stringfrom:[email protected]
to:stringto:support@
subject:string (quote phrases)subject:"board deck"
label:label keylabel:receipts
folder:folder namefolder:archive
has:attachmenthas:attachment
is:starred, unreadis:unread
not:stringnot:newsletter
before:ISO datebefore:2026-01-01
after:ISO dateafter: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).

Have more questions? Check the FAQ or contact support — we reply in 2 hours during business hours.