mbag.ai

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.json

The 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.