Skip to main content
JMAP (JSON Meta Application Protocol) is the API OxiMail speaks for mail, calendars, contacts, tasks, files, and chat. This page covers the Core layer from RFC 8620: how a client discovers the server, authenticates, batches method calls into one HTTP request, chains those calls with back-references, opts in to capabilities, and receives push notifications. Everything here is what OxiMail actually exposes at v0.30.0. The per-domain method sets build on top of it: see JMAP Mail, Calendars, Contacts, Tasks, Files, Sharing, and Chat.

The session resource

A JMAP client starts by fetching the session resource. It is the single discovery document that tells the client everything it needs: which capabilities the server supports, which accounts the user can access, and the URLs for every other operation.
GET /.well-known/jmap
Authorization: Bearer <token>
The session endpoint is authenticated — you must present a valid token to fetch it. The response is a JSON object (RFC 8620 §2) with these fields:
FieldWhat it carries
capabilitiesThe server-level capability map: each supported capability URN mapped to its configuration object (limits, options).
accountsEvery account this user can access, keyed by account id. Each entry has name, isPersonal, isReadOnly, and its own accountCapabilities.
primaryAccountsFor each capability URN, the account id that is “primary” for it.
usernameThe authenticated user’s name (their email).
apiUrlThe endpoint to POST method calls to: /jmap.
uploadUrlThe blob upload endpoint template: /jmap/upload/{accountId}.
downloadUrlThe blob download URL template: /jmap/download/{accountId}/{blobId}/{name}?type={type}.
eventSourceUrlThe push (EventSource) URL template: /jmap/eventsource/?types={types}&closeafter={closeafter}&ping={ping}.
stateAn opaque string that changes whenever the session object itself changes (e.g. an account is added).
The Core capability object also advertises the server’s hard limits, which a client should respect before sending a request:
LimitValue at v0.30.0
maxSizeUpload50 MB
maxConcurrentUpload4
maxSizeRequest10 MB
maxConcurrentRequests8
maxCallsInRequest64
maxObjectsInGet500
maxObjectsInSet500
A user can see more than one account in the session. Besides their personal account, any folder, calendar, or address book shared with them via JMAP Sharing appears as an extra account entry (keyed shared:{ownerId}) carrying only the shared capabilities, and possibly isReadOnly: true.

Authentication

OxiMail uses Bearer tokens (RFC 6750). Obtain a token by posting credentials to the login endpoint:
POST /auth/login
Content-Type: application/json

{ "email": "alice@example.com", "password": "..." }
The response returns the token and the account id:
{ "accessToken": "...", "accountId": "..." }
Send that token on every subsequent request:
Authorization: Bearer <token>

Token lifetime and refresh

Tokens expire 24 hours after they are issued. To stay logged in without re-entering a password, refresh the token:
POST /auth/refresh
Authorization: Bearer <current-token>
If the token is still valid, or expired less than 7 days ago, the server deletes the old token and returns a fresh one with a new 24-hour expiry. The response shape matches login: { "accessToken": "...", "accountId": "..." }. Past the 7-day grace period the refresh fails with 401 and the user must log in again.

EventSource and the access_token query parameter

The browser EventSource API cannot set custom request headers, so it cannot send Authorization: Bearer. To support push in the browser, OxiMail accepts the token as a query parameter on the EventSource endpoint (per RFC 6750 §2.3):
GET /jmap/eventsource/?types=*&access_token=<token>
This is the only endpoint where the token may travel in the URL; everywhere else, use the Authorization header.

Making requests

All method calls go to a single endpoint as one batched POST. The request body (RFC 8620 §3.3) has three parts:
POST /jmap
Authorization: Bearer <token>
Content-Type: application/json
{
  "using": [
    "urn:ietf:params:jmap:core",
    "urn:ietf:params:jmap:mail"
  ],
  "methodCalls": [
    ["Mailbox/get", { "accountId": "a1", "ids": null }, "c0"],
    ["Email/get", { "accountId": "a1", "ids": ["e1", "e2"] }, "c1"]
  ]
}
  • using declares which capabilities this request relies on. A method whose capability is not listed in using is rejected with unknownCapability — the method is never run “anyway”.
  • methodCalls is an ordered array. Each call is a 3-element array: [methodName, arguments, callId]. The callId is your own label, echoed back so you can match responses to calls.
The response mirrors that shape with a methodResponses array, in the same order, each tagged with the matching callId:
{
  "methodResponses": [
    ["Mailbox/get", { "accountId": "a1", "state": "...", "list": [ ... ] }, "c0"],
    ["Email/get", { "accountId": "a1", "state": "...", "list": [ ... ], "notFound": [] }, "c1"]
  ]
}
A single bad id never disappears silently. Anything an /get call cannot find comes back in notFound; OxiMail never drops unparseable or unknown ids on the floor.

Back-references (result references)

The point of batching is that one call can feed the next within the same request, so you avoid a round trip. This is a back-reference, also called a result reference (RFC 8620 §3.7). Instead of a literal argument value, you pass an object with a # prefix on the argument name:
{
  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
  "methodCalls": [
    ["Email/query", { "accountId": "a1", "filter": { "inMailbox": "inbox" } }, "c0"],
    ["Email/get", {
      "accountId": "a1",
      "#ids": {
        "resultOf": "c0",
        "name": "Email/query",
        "path": "/ids"
      }
    }, "c1"]
  ]
}
The second call says: “for my ids argument, take the result of call c0 (which must be an Email/query), and pull the value at JSON Pointer /ids.” The server resolves the reference from the first call’s result before running the second. A back-reference must name the correct previous callId, the correct method name, and a valid path. If any of those is wrong, OxiMail returns invalidResultReference rather than substituting null or an empty list.

Capabilities

The server advertises what it can do through capability URNs in the session capabilities map. The client then opts in to the ones it intends to use by listing them in the request using array. OxiMail advertises two families.

Standard JMAP (v1)

The IETF-standardized capabilities, using the urn:ietf:params:jmap:* namespace — Core, Mail (RFC 8621), Submission, Vacation Response, Sieve (RFC 9661), Quota (RFC 9425), Principals / Sharing (RFC 9670), Contacts (RFC 9610), Calendars, Files, and WebSocket (RFC 8887).

OxiMail modernized JMAP (v2)

OxiMail also exposes a set of modernized capabilities under the urn:oximail:params:jmap:v2:* namespace. These are OxiMail’s modernized JMAP extensions (Internet-Drafts in progress) and cover, among others:
Capability URNWhat it adds
urn:oximail:params:jmap:v2:mailA modernized mail surface (import, send, cancel, reindex) layered on the classic Email methods.
urn:oximail:params:jmap:v2:threadsFirst-class Thread type with merge and convergence.
urn:oximail:params:jmap:v2:labelsFlat, shareable labels instead of nested folders.
urn:oximail:params:jmap:v2:rulesTyped mail filter rules with a per-rule execution time guarantee.
urn:oximail:params:jmap:v2:calendarFirst-class recurrence rules and attendees.
urn:oximail:params:jmap:v2:contactsA modernized contacts model with a documented mutability table.
urn:oximail:params:jmap:v2:streamingPush catch-up and streaming query support.
urn:oximail:params:jmap:v2:resource-limitsAdvertised conformance limits a client can check before sending.
See JMAP v2 modernized for the full surface.

Mixing v1 and v2 in one request

At v0.30.0 the v1 and v2 mail, contacts, and calendar surfaces are served by the same handlers through a dual-capability mechanism: a handler that advertises a v1 capability as its primary also accepts the matching v2 capability as an additional one. A request may therefore list both a v1 capability and its v2 counterpart in using at the same time; the wire shape returned is the union of v1 and v2 properties, and the client reads whichever it wants via the properties argument on /get.
The protocol still keeps a mutual-exclusion check in place for future use, but at v0.30.0 the set of mutually-exclusive v1/v2 pairs is empty: nothing is rejected for pairing a v1 capability with its v2 counterpart. If a future v3 surface reintroduces a hard split, a request that pairs the two excluded URNs would be rejected with unknownCapability.

Push: knowing when state changes

Every JMAP type tracks an opaque state string. When you call Foo/get, the response includes the current state. Later you can ask Foo/changes with the state you last saw, and the server tells you exactly which objects were created, updated, or destroyed since then. You never poll for full lists; you sync deltas. To learn when to call Foo/changes, OxiMail pushes a small StateChange notification whenever a type’s state advances. There are two transports, both advertised in the session:

EventSource (Server-Sent Events)

A standard SSE stream at the eventSourceUrl:
GET /jmap/eventsource/?types=*&access_token=<token>
The server streams StateChange events. Each event names the account and the collection whose state moved, plus the new state string. Use types=Email,Mailbox to filter to specific collections, ping= for keep-alive interval, and access_token= for browser authentication (see above). The stream is scoped to the authenticated account and organization — you only receive your own changes.

WebSocket

OxiMail also speaks JMAP over WebSocket (RFC 8887). The session advertises a wss://.../jmap/ws URL with supportsPush: true, so a client that already holds a WebSocket connection can both send method calls and receive push on the same channel. Either way, the pattern is the same: a push notification is a hint that some collection’s state changed; the client follows up with Foo/changes to fetch the actual delta.

Where to go next