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.| Field | What it carries |
|---|---|
capabilities | The server-level capability map: each supported capability URN mapped to its configuration object (limits, options). |
accounts | Every account this user can access, keyed by account id. Each entry has name, isPersonal, isReadOnly, and its own accountCapabilities. |
primaryAccounts | For each capability URN, the account id that is “primary” for it. |
username | The authenticated user’s name (their email). |
apiUrl | The endpoint to POST method calls to: /jmap. |
uploadUrl | The blob upload endpoint template: /jmap/upload/{accountId}. |
downloadUrl | The blob download URL template: /jmap/download/{accountId}/{blobId}/{name}?type={type}. |
eventSourceUrl | The push (EventSource) URL template: /jmap/eventsource/?types={types}&closeafter={closeafter}&ping={ping}. |
state | An opaque string that changes whenever the session object itself changes (e.g. an account is added). |
Core capability object also advertises the server’s hard limits, which a client should respect before sending a request:
| Limit | Value at v0.30.0 |
|---|---|
maxSizeUpload | 50 MB |
maxConcurrentUpload | 4 |
maxSizeRequest | 10 MB |
maxConcurrentRequests | 8 |
maxCallsInRequest | 64 |
maxObjectsInGet | 500 |
maxObjectsInSet | 500 |
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:Token lifetime and refresh
Tokens expire 24 hours after they are issued. To stay logged in without re-entering a password, refresh the token:{ "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):
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:usingdeclares which capabilities this request relies on. A method whose capability is not listed inusingis rejected withunknownCapability— the method is never run “anyway”.methodCallsis an ordered array. Each call is a 3-element array:[methodName, arguments, callId]. ThecallIdis your own label, echoed back so you can match responses to calls.
methodResponses array, in the same order, each tagged with the matching callId:
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:
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 sessioncapabilities 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 theurn: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 theurn:oximail:params:jmap:v2:* namespace. These are OxiMail’s modernized JMAP extensions (Internet-Drafts in progress) and cover, among others:
| Capability URN | What it adds |
|---|---|
urn:oximail:params:jmap:v2:mail | A modernized mail surface (import, send, cancel, reindex) layered on the classic Email methods. |
urn:oximail:params:jmap:v2:threads | First-class Thread type with merge and convergence. |
urn:oximail:params:jmap:v2:labels | Flat, shareable labels instead of nested folders. |
urn:oximail:params:jmap:v2:rules | Typed mail filter rules with a per-rule execution time guarantee. |
urn:oximail:params:jmap:v2:calendar | First-class recurrence rules and attendees. |
urn:oximail:params:jmap:v2:contacts | A modernized contacts model with a documented mutability table. |
urn:oximail:params:jmap:v2:streaming | Push catch-up and streaming query support. |
urn:oximail:params:jmap:v2:resource-limits | Advertised conformance limits a client can check before sending. |
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 inusing 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 callFoo/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 theeventSourceUrl:
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 awss://.../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
- JMAP Mail — the mailbox and message methods.
- JMAP v2 modernized — the modernized surface in depth.
- Concepts — the data model and vocabulary behind these methods.
- Architecture at a glance — how the JMAP layer sits in the single binary.