Using the JMAP API
Serverless Inbox implements JMAP (RFC 8620) and JMAP for Mail (RFC 8621). All JMAP traffic goes through a single HTTP endpoint.
Endpoint URL
Section titled “Endpoint URL”The JMAP API is served at jmap.<domainName>. The Mailbox CDK construct provisions the ACM certificate and Route 53 record automatically.
If the optional prefix property is set, a label is inserted between the service name and the base domain:
| CDK props | JMAP endpoint |
|---|---|
domainName: "example.com" | https://jmap.example.com |
domainName: "example.com", prefix: "mailbox" | https://jmap.mailbox.example.com |
Authentication
Section titled “Authentication”The JMAP API requires a Bearer JWT issued by the Cognito User Pool. The token must carry the jmapApi audience and at least the MAIL_READ scope for read operations; MAIL_WRITE and MAIL_SEND are required for mutation and send operations.
Machine-to-machine (headless) access
Section titled “Machine-to-machine (headless) access”# The exact scope strings depend on your IDP plugin configuration.# Check the CloudFormation outputs or ask your administrator for the configured scopes.TOKEN=$(curl -s -X POST \ "https://<cognito-domain>.auth.<region>.amazoncognito.com/oauth2/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=<JmapHeadlessClientId>" \ -d "client_secret=<JmapHeadlessClientSecret>" \ -d "scope=<mail-read-scope> <mail-write-scope> <mail-send-scope>" \ | jq -r '.access_token')The headless client credentials are available as CloudFormation outputs (JmapHeadlessClientId and JmapHeadlessClientSecret).
Session discovery
Section titled “Session discovery”Before making method calls, fetch the JMAP session object. It describes the capabilities the server supports, the accounts the authenticated user has access to, and the API URL to use for calls.
curl -s "https://jmap.mail.example.com/.well-known/jmap" \ -H "Authorization: Bearer $TOKEN" | jq .The response includes an apiUrl (the endpoint for method calls) and the primaryAccounts map — you need the account ID for almost every method call.
{ "capabilities": { "urn:ietf:params:jmap:core": { "maxCallsInRequest": 32, "..." : "..." }, "urn:ietf:params:jmap:mail": { "mayCreateTopLevelMailbox": true, "...": "..." }, "https://serverlessinbox.com/jmap/websocket": { "maxSubscriptions": 10 } }, "accounts": { "acc-abc123": { "name": "alice@example.com", "isPersonal": true, "isReadOnly": false, "accountCapabilities": { "urn:ietf:params:jmap:core": {}, "urn:ietf:params:jmap:mail": {}, "urn:ietf:params:jmap:submission": {} } } }, "primaryAccounts": { "urn:ietf:params:jmap:core": "acc-abc123", "urn:ietf:params:jmap:mail": "acc-abc123", "urn:ietf:params:jmap:submission": "acc-abc123" }, "apiUrl": "https://jmap.mail.example.com/jmap", "uploadUrl": "https://jmap.mail.example.com/upload/{accountId}", "downloadUrl": "https://cdn.mail.example.com?accountId={accountId}&blobId={blobId}&type={type}&name={name}", "webSocketUrl": "wss://ws.mail.example.com"}Request format
Section titled “Request format”All JMAP method calls are sent as a single POST to the apiUrl:
POST /jmapAuthorization: Bearer <token>Content-Type: application/json
{ "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"], "methodCalls": [ ["<MethodName>", { ...arguments... }, "<clientId>"] ]}Each element of methodCalls is a 3-tuple:
- Method name — e.g.
Email/query,Mailbox/get - Arguments — the method’s argument object
- Client ID — an arbitrary string you choose (e.g.
"r1") to identify this call when referencing its result
The response is:
{ "methodResponses": [ ["<MethodName>", { ...response... }, "<clientId>"] ]}Result references
Section titled “Result references”A key JMAP feature is the ability to use the output of one method call as the input to a later call in the same request. This avoids a round-trip. To reference a result, prefix the argument key with # and provide a ResultReference object:
{ "resultOf": "<clientId of the earlier call>", "name": "<method name of the earlier call>", "path": "/path/to/value"}Example: list and fetch emails in one request
Section titled “Example: list and fetch emails in one request”This example:
- Queries the 10 most recent emails in the Inbox
- Fetches the subject, from, and preview of those emails — in the same HTTP request, without needing the IDs from step 1 first
# Substitute your accountId from the session objectACCOUNT_ID="acc-abc123"INBOX_ID="mailbox-inbox-id" # obtain via Mailbox/get
curl -s -X POST "https://jmap.mail.example.com/jmap" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"], "methodCalls": [ ["Email/query", { "accountId": "'"$ACCOUNT_ID"'", "filter": { "inMailbox": "'"$INBOX_ID"'" }, "sort": [{ "property": "receivedAt", "isAscending": false }], "limit": 10 }, "q1"],
["Email/get", { "accountId": "'"$ACCOUNT_ID"'", "#ids": { "resultOf": "q1", "name": "Email/query", "path": "/ids" }, "properties": ["subject", "from", "preview", "receivedAt"] }, "g1"] ] }' | jq '.methodResponses[1][1].list'Example: fetch all mailboxes
Section titled “Example: fetch all mailboxes”curl -s -X POST "https://jmap.mail.example.com/jmap" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"], "methodCalls": [ ["Mailbox/get", { "accountId": "'"$ACCOUNT_ID"'", "ids": null }, "m1"] ] }' | jq '.methodResponses[0][1].list[] | {id, name, role}'Example: incremental sync with Email/changes
Section titled “Example: incremental sync with Email/changes”After your initial sync (via Email/get with ids: null), use the state value from the response to poll for changes incrementally:
curl -s -X POST "https://jmap.mail.example.com/jmap" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"], "methodCalls": [ ["Email/changes", { "accountId": "'"$ACCOUNT_ID"'", "sinceState": "<state-from-previous-response>" }, "c1"],
["Email/get", { "accountId": "'"$ACCOUNT_ID"'", "#ids": { "resultOf": "c1", "name": "Email/changes", "path": "/created" }, "properties": ["subject", "from", "mailboxIds", "receivedAt"] }, "g1"] ] }' | jq .If hasMoreChanges is true in the changes response, call again with the returned newState until it is false.
WebSocket push
Section titled “WebSocket push”Serverless Inbox supports JMAP push over WebSocket (RFC 8887) for real-time change notifications. The session object includes a webSocketUrl field (e.g. wss://ws.mail.example.com) pointing to the WebSocket endpoint — using the ws subdomain with the same prefix logic as the other endpoints. See the WebSocket Events reference for details.