Documentation
Every mbag.ai mailbox is a real JMAP account on the underlying Stalwart
cluster. Most integrations never need to know that — the REST endpoints
(/api/v1/mailboxes/{id}/send, /messages/new, webhooks) cover the common
cases. This page is for the rest: anything that needs a raw JMAP method call
the REST layer doesn't wrap yet.
What the proxy does
/api/v1/jmap is a thin tenant-isolation proxy, not a general JMAP relay.
Each request authenticates to Stalwart as your own mailbox's principal,
using HTTP Basic auth resolved server-side from your API key — you never see
or handle a JMAP password. Stalwart's per-principal auth is the real
isolation boundary: a principal can only ever see its own account. As
defense in depth, the proxy also rewrites any accountId your request
supplies in each method call to your resolved principal's own account id
before forwarding, so a stale or guessed accountId can't leak into a call
by mistake.
If your API key owns more than one mailbox, tell the proxy which one to act
as with a top-level _mailbuttons object:
{
"_mailbuttons": { "account": "[email protected]" },
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
"methodCalls": [ ... ]
}account accepts either the mailbox's email address or its numeric id.
_mailbuttons is stripped before the request reaches Stalwart. If your key
owns exactly one mailbox, omit it — the proxy resolves it automatically.
The two endpoints
`POST /api/v1/jmap`
Forwards a raw JMAP request object per RFC 8620 (using, methodCalls,
optional createdIds) to your mailbox's Stalwart principal, and returns
Stalwart's methodResponses verbatim on success.
Each entry in methodCalls may be given as JMAP's native
[name, arguments, id] triple, or as an equivalent
{"name": ..., "arguments": ..., "id": ...} object — the proxy transforms
the object form into triples before forwarding. Use whichever is more
convenient for your client library; the wire example below uses the native
triple form.
`GET /api/v1/jmap/session`
Fetches the JMAP session object (RFC 8620 §2) for your mailbox's principal,
forwarded verbatim from Stalwart. Same mailbox-resolution rules as the POST
endpoint, but via a ?account= query parameter instead of _mailbuttons:
curl "https://mbag.ai/api/v1/jmap/[email protected]" \
-H "Authorization: Bearer $MBAG_API_KEY"account is only needed when your key owns more than one mailbox.
Worked example: `Email/query` + `Email/get`
A minimal, RFC-8620-standard request — list the 10 most recent email ids, then fetch a couple of fields for each, chained with a JMAP back-reference so you never have to round-trip the ids yourself:
{
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
"methodCalls": [
[
"Email/query",
{
"accountId": null,
"sort": [{ "property": "receivedAt", "isAscending": false }],
"limit": 10
},
"c1"
],
[
"Email/get",
{
"accountId": null,
"#ids": {
"resultOf": "c1",
"name": "Email/query",
"path": "/ids"
},
"properties": ["id", "subject", "from", "receivedAt"]
},
"c2"
]
]
}The proxy overwrites any supplied accountId with your mailbox's own — but
that injection is best-effort (it depends on a session fetch to Stalwart), so
don't rely on it as a feature: real clients should read the correct
accountId from GET /api/v1/jmap/session and send that.
curl -X POST https://mbag.ai/api/v1/jmap \
-H "Authorization: Bearer $MBAG_API_KEY" \
-H "Content-Type: application/json" \
-d @request.jsonThe response is Stalwart's own methodResponses array, unmodified — the
result of c1 is a list of email ids, and c2 resolves those ids into the
requested properties. Nothing about this shape is mbag-specific; it's
standard RFC 8620 core plus RFC 8621 mail, so any JMAP client library works
against this endpoint unmodified once it's pointed at
https://mbag.ai/api/v1/jmap with your API key as a bearer token instead of
JMAP's own auth scheme.
Errors
| Status | error |
Cause |
|---|---|---|
400 |
ambiguous_account |
Your API key owns multiple mailboxes and no _mailbuttons.account (or, for the session endpoint, ?account=) hint was given. |
403 |
account_not_found_or_not_owned |
The account hint doesn't match any mailbox this API key owns. |
404 |
no_mailbox |
This API key has no active mailbox. |
409 |
mailbox_not_provisioned |
The selected mailbox has no JMAP password yet — it hasn't finished provisioning in the mail server. |
401 |
— | Missing or invalid API key. |
502 |
— | The proxy couldn't reach Stalwart's JMAP endpoint. |
Any other non-2xx status is whatever Stalwart itself returned, passed through verbatim — its status code and JSON body — since the proxy's job ends at authentication and account resolution, not at interpreting JMAP semantics.
Raw JMAP vs. the REST endpoints
Raw JMAP is the full power of the underlying mail store: arbitrary queries, filters, and object types, not just the handful of operations the REST API wraps. That power comes with the standard's own complexity — method chaining, back-references, and RFC 8620/8621 semantics you're responsible for getting right yourself.
The governed REST endpoints
(send, webhooks, audit
querying) are still the recommended path for anything they cover: they're
simpler, and every send still passes through the same
policy gate either way. Reach for /api/v1/jmap when
you need something specific the REST layer doesn't expose yet — mailbox
folder management, custom search filters, and so on — not as a default way
to talk to your mailbox.