Authentication
Two credential types, one authorization model.
| Credential | Prefix | Issued by | For |
|---|---|---|---|
| API key | mxa_live_ |
App → Account → API | Servers, scripts, automation platforms |
| OAuth access token | mxa_oat_ |
auth.mxaura.ai (OAuth 2.1) |
MCP clients and browser apps acting for a signed-in user |
Both are sent the same way:
Authorization: Bearer <credential>(x-api-key: <credential> is also accepted for API keys.)
Both are graded by the same scopes, the same rate limits, and the same tenant boundary: a credential sees exactly one account's data. That boundary is enforced in the database access layer for every statement, not by the API code checking ids.
API keys
Lifecycle
| Action | Where | Notes |
|---|---|---|
| Create | Account → API → Create key | Shown once. Choose show-once (default) or retrievable (can be revealed again later; audited). |
| Rotate | Account → API → Rotate | Issues a new key; the old one keeps working for a grace period and answers with X-MXA-Key-Status: rotated_grace so you can find lingering callers. |
| Revoke | Account → API → Revoke | Immediate. The edge cache is flushed the moment you click. |
| Expire | Set at creation | A key past expires_at answers 401 key_expired. |
| Suspend | Platform | A suspended key answers 401 with the reason. |
Keys are stored as a SHA-256 hash. mxAURA cannot show you a show-once key again; rotate it.
Per-key settings (Account → API → Settings)
- Which addresses may use this key. an optional allowlist of IPv4/IPv6
addresses or CIDRs. Empty means any address, which is the default. With
entries, a request from anywhere else gets
403 ip_not_allowed. - How many requests per minute. 60, 120 (default), 300 or 600. See rate limits.
Keys and browsers
API keys are refused from browsers: a request carrying an Origin header
gets 403 browser_origin. A key in JavaScript is a key in every visitor's
hands. Browser apps use OAuth tokens instead.
OAuth 2.1 (MCP clients and browser apps)
https://auth.mxaura.ai is a standards-compliant OAuth 2.1 authorization
server:
| Endpoint | Purpose |
|---|---|
/.well-known/oauth-authorization-server |
Discovery (RFC 8414) |
/register |
Dynamic Client Registration (RFC 7591). Any client may register itself |
/authorize |
Authorization code with PKCE S256 mandatory (plain rejected) |
/token |
authorization_code and refresh_token grants |
Consent rides the user's mxAURA session: they sign in to the app (with 2FA if enabled), see the requesting client and the scopes, and approve. The token that results is bound to the account they were signed into.
- Access tokens live 1 hour; refresh tokens 30 days.
- Refresh tokens rotate on use; a used refresh token cannot be replayed. To cut off a client, remove the connection in the client itself; its access token expires within the hour and it cannot refresh. (An in-app "connected apps" page for revoking from mxAURA's side is on the roadmap.)
- Tokens are graded by the public API exactly like keys, with the same scopes, limits and caps.
Most MCP clients handle all of this for you: give them
https://mcp.mxaura.ai/mcp and they discover the rest. See the
MCP guide.
Scopes
Scopes are family.verb with three verbs:
readcovers GET.writecovers POST, PUT and PATCH — creating and changing things.writeimpliesread.deletecovers DELETE, and is held apart because it cannot be undone.deletedoes not implywriteorread.
full (stored as *) grants every read and write scope — and no
delete scope. Destruction is a per-family opt-in that an account owner
grants to a specific key in the app; an automation never inherits it by
having "full access". This is deliberate: an AI agent holding a full key and
one bad prompt should not be able to permanently remove a hub.
Deleting a container — something whose dependents cease to exist with it —
is additionally traced. DELETE /hubs/{id}, DELETE /templates/{id} and
DELETE /blocks/kinds/{id} first consult GET /{resource}/{id}/associations
and refuse with 409 has_associations while anything is listed, returning the
list in the error so you know exactly what to remove first. There is no force
option; the trace is the unlock. Reference deletes (a message in a pool, a
pool on a campaign) are not traced — the cascade self-heals — though deleting
a pool attached to a running campaign returns a warning.
| Family | Covers |
|---|---|
components |
messages, headers, summaries, signatures, senders, templates |
brandings |
brandings |
pools |
pools and their composed messages |
campaigns |
campaigns, campaign pools, campaign simulation |
flows |
flows, flow blueprints |
enrollments (write only) |
enrolling and unenrolling contacts in flows. This sends real email |
dispatch (read only) |
the dispatch log and activity feed |
hubs |
hubs, layouts, stats, the hub's own settings |
articles |
articles, authors, content types |
publishing |
placing, publishing, scheduling, featuring articles; opening a preview window |
subscribers |
subscriptions (embed keys), site keys, assignments. Who may embed |
identity |
the messaging identity (frameworks, sources, advisor) |
folders |
folders |
media |
the account's own media library |
webhooks |
outbound event endpoints |
ai (write only) |
anything that calls the model and spends budget: hero images, SEO, drafts, advisor generation |
The hubs family is split on purpose: a key for a content team gets
articles.write + publishing.write without the ability to mint embed keys
(subscribers.write) or spend model budget (ai.write).
The support profile
Every read family, nothing that sends, publishes or spends:
components.read brandings.read pools.read campaigns.read flows.read
hubs.read articles.read identity.read dispatch.readIt is the default grant for an OAuth client that does not ask for
specific scopes, and a client can request it by name with scope=support.
It is what a help agent should hold: enough to answer "why didn't Darren get
the email". Never enough to send one, and never enough to spend: since v3.1.892 even
ask_help needs ai.write.
Not available over the API (on purpose)
Accounts, users, profiles, billing, HubSpot connections/OAuth, notifications,
import/export and platform administration are session-only surfaces in the
app. One destructive action is withheld regardless of scope: deleting the
messaging identity (everything composes from it) — the identity family has
no delete tier at all. Deleting a hub is available, behind hubs.delete
and the association trace described under Scopes. Rotating a client's site
key is likewise app-only: it revokes every live embed at once.