3. Structured Data Exchange
The client may make an API request to the server to get or set structured data. This request consists of an ordered series of method calls. These are processed by the server, which then returns an ordered series of responses.3.1. Making an API Request
To make an API request, the client makes an authenticated POST request to the API resource, which is defined by the "apiUrl" property in the Session object (see Section 2). The request MUST be of type "application/json" and consist of a single JSON-encoded "Request" object, as defined in Section 3.3. If successful, the response MUST also be of type "application/json" and consist of a single "Response" object, as defined in Section 3.4.3.2. The Invocation Data Type
Method calls and responses are represented by the *Invocation* data type. This is a tuple, represented as a JSON array containing three elements: 1. A "String" *name* of the method to call or of the response. 2. A "String[*]" object containing named *arguments* for that method or response. 3. A "String" *method call id*: an arbitrary string from the client to be echoed back with the responses emitted by that method call (a method may return 1 or more responses, as it may make implicit calls to other methods; all responses initiated by this method call get the same method call id in the response).3.3. The Request Object
A *Request* object has the following properties: o using: "String[]" The set of capabilities the client wishes to use. The client MAY include capability identifiers even if the method calls it makes do not utilise those capabilities. The server advertises the set of specifications it supports in the Session object (see Section 2), as keys on the "capabilities" property.
o methodCalls: "Invocation[]" An array of method calls to process on the server. The method calls MUST be processed sequentially, in order. o createdIds: "Id[Id]" (optional) A map of a (client-specified) creation id to the id the server assigned when a record was successfully created. As described later in this specification, some records may have a property that contains the id of another record. To allow more efficient network usage, you can set this property to reference a record created earlier in the same API request. Since the real id is unknown when the request is created, the client can instead specify the creation id it assigned, prefixed with a "#" (see Section 5.3 for more details). As the server processes API requests, any time it successfully creates a new record, it adds the creation id to this map (see the "create" argument to /set in Section 5.3), with the server- assigned real id as the value. If it comes across a reference to a creation id in a create/update, it looks it up in the map and replaces the reference with the real id, if found. The client can pass an initial value for this map as the "createdIds" property of the Request object. This may be an empty object. If given in the request, the response will also include a createdIds property. This allows proxy servers to easily split a JMAP request into multiple JMAP requests to send to different servers. For example, it could send the first two method calls to server A, then the third to server B, before sending the fourth to server A again. By passing the createdIds of the previous response to the next request, it can ensure all of these still resolve. See Section 5.8 for further discussion of proxy considerations. Future specifications MAY add further properties to the Request object to extend the semantics. To ensure forwards compatibility, a server MUST ignore any other properties it does not understand on the JMAP Request object.
3.3.1. Example Request
{ "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ], "methodCalls": [ [ "method1", { "arg1": "arg1data", "arg2": "arg2data" }, "c1" ], [ "method2", { "arg1": "arg1data" }, "c2" ], [ "method3", {}, "c3" ] ] }3.4. The Response Object
A *Response* object has the following properties: o methodResponses: "Invocation[]" An array of responses, in the same format as the "methodCalls" on the Request object. The output of the methods MUST be added to the "methodResponses" array in the same order that the methods are processed. o createdIds: "Id[Id]" (optional; only returned if given in the request) A map of a (client-specified) creation id to the id the server assigned when a record was successfully created. This MUST include all creation ids passed in the original createdIds parameter of the Request object, as well as any additional ones added for newly created records. o sessionState: "String" The current value of the "state" string on the Session object, as described in Section 2. Clients may use this to detect if this object has changed and needs to be refetched. Unless otherwise specified, if the method call completed successfully, its response name is the same as the method name in the request.
3.4.1. Example Response
{ "methodResponses": [ [ "method1", { "arg1": 3, "arg2": "foo" }, "c1" ], [ "method2", { "isBlah": true }, "c2" ], [ "anotherResponseFromMethod2", { "data": 10, "yetmoredata": "Hello" }, "c2"], [ "error", { "type":"unknownMethod" }, "c3" ] ], "sessionState": "75128aab4b1b" }3.5. Omitting Arguments
An argument to a method may be specified to have a default value. If omitted by the client, the server MUST treat the method call the same as if the default value had been specified. Similarly, the server MAY omit any argument in a response that has the default value. Unless otherwise specified in a method description, null is the default value for any argument in a request or response where this is allowed by the type signature. Other arguments may only be omitted if an explicit default value is defined in the method description.3.6. Errors
There are three different levels of granularity at which an error may be returned in JMAP. When an API request is made, the request as a whole may be rejected due to rate limiting, malformed JSON, request for an unknown capability, etc. In this case, the entire request is rejected with an appropriate HTTP error response code and an additional JSON body with more detail for the client. Provided the request itself is syntactically valid (the JSON is valid and when decoded, it matches the type signature of a Request object), the methods within it are executed sequentially by the server. Each
method may individually fail, for example, if invalid arguments are given or an unknown method name is called. Finally, methods that make changes to the server state often act upon a number of different records within a single call. Each record change may be separately rejected with a SetError, as described in Section 5.3.3.6.1. Request-Level Errors
When an HTTP error response is returned to the client, the server SHOULD return a JSON "problem details" object as the response body, as per [RFC7807]. The following problem types are defined: o "urn:ietf:params:jmap:error:unknownCapability" The client included a capability in the "using" property of the request that the server does not support. o "urn:ietf:params:jmap:error:notJSON" The content type of the request was not "application/json" or the request did not parse as I-JSON. o "urn:ietf:params:jmap:error:notRequest" The request parsed as JSON but did not match the type signature of the Request object. o "urn:ietf:params:jmap:error:limit" The request was not processed as it would have exceeded one of the request limits defined on the capability object, such as maxSizeRequest, maxCallsInRequest, or maxConcurrentRequests. A "limit" property MUST also be present on the "problem details" object, containing the name of the limit being applied.3.6.1.1. Example
{ "type": "urn:ietf:params:jmap:error:unknownCapability", "status": 400, "detail": "The Request object used capability 'https://example.com/apis/foobar', which is not supported by this server." }
Another example: { "type": "urn:ietf:params:jmap:error:limit", "limit": "maxSizeRequest", "status": 400, "detail": "The request is larger than the server is willing to process." }3.6.2. Method-Level Errors
If a method encounters an error, the appropriate "error" response MUST be inserted at the current point in the "methodResponses" array and, unless otherwise specified, further processing MUST NOT happen within that method call. Any further method calls in the request MUST then be processed as normal. Errors at the method level MUST NOT generate an HTTP-level error. An "error" response looks like this: [ "error", { "type": "unknownMethod" }, "call-id" ] The response name is "error", and it MUST have a type property. Other properties may be present with further information; these are detailed in the error type descriptions where appropriate. With the exception of when the "serverPartialFail" error is returned, the externally visible state of the server MUST NOT have changed if an error is returned at the method level. The following error types are defined, which may be returned for any method call where appropriate: "serverUnavailable": Some internal server resource was temporarily unavailable. Attempting the same operation later (perhaps after a backoff with a random factor) may succeed. "serverFail": An unexpected or unknown error occurred during the processing of the call. A "description" property should provide more details about the error. The method call made no changes to the server's state. Attempting the same operation again is expected to fail again. Contacting the service administrator is likely necessary to resolve this problem if it is persistent.
"serverPartialFail": Some, but not all, expected changes described by the method occurred. The client MUST resynchronise impacted data to determine server state. Use of this error is strongly discouraged. "unknownMethod": The server does not recognise this method name. "invalidArguments": One of the arguments is of the wrong type or is otherwise invalid, or a required argument is missing. A "description" property MAY be present to help debug with an explanation of what the problem was. This is a non-localised string, and it is not intended to be shown directly to end users. "invalidResultReference": The method used a result reference for one of its arguments (see Section 3.7), but this failed to resolve. "forbidden": The method and arguments are valid, but executing the method would violate an Access Control List (ACL) or other permissions policy. "accountNotFound": The accountId does not correspond to a valid account. "accountNotSupportedByMethod": The accountId given corresponds to a valid account, but the account does not support this method or data type. "accountReadOnly": This method modifies state, but the account is read-only (as returned on the corresponding Account object in the JMAP Session resource). Further possible errors for a particular method are specified in the method descriptions. Further general errors MAY be defined in future RFCs. Should a client receive an error type it does not understand, it MUST treat it the same as the "serverFail" type.3.7. References to Previous Method Results
To allow clients to make more efficient use of the network and avoid round trips, an argument to one method can be taken from the result of a previous method call in the same request. To do this, the client prefixes the argument name with "#" (an octothorpe). The value is a ResultReference object as described below. When processing a method call, the server MUST first check the arguments object for any names beginning with "#". If found, the result reference should be resolved and the value used as the "real"
argument. The method is then processed as normal. If any result reference fails to resolve, the whole method MUST be rejected with an "invalidResultReference" error. If an arguments object contains the same argument name in normal and referenced form (e.g., "foo" and "#foo"), the method MUST return an "invalidArguments" error. A *ResultReference* object has the following properties: o resultOf: "String" The method call id (see Section 3.2) of a previous method call in the current request. o name: "String" The required name of a response to that method call. o path: "String" A pointer into the arguments of the response selected via the name and resultOf properties. This is a JSON Pointer [RFC6901], except it also allows the use of "*" to map through an array (see the description below). To resolve: 1. Find the first response with a method call id identical to the "resultOf" property of the ResultReference in the "methodResponses" array from previously processed method calls in the same request. If none, evaluation fails. 2. If the response name is not identical to the "name" property of the ResultReference, evaluation fails. 3. Apply the "path" to the arguments object of the response (the second item in the response array) following the JSON Pointer algorithm [RFC6901], except with the following addition in "Evaluation" (see Section 4): If the currently referenced value is a JSON array, the reference token may be exactly the single character "*", making the new referenced value the result of applying the rest of the JSON Pointer tokens to every item in the array and returning the results in the same order in a new array. If the result of applying the rest of the pointer tokens to each item was itself an array, the contents of this array are added to the output rather than the array itself (i.e., the result is flattened from an array of arrays to a single array). If the result of applying
the rest of the pointer tokens to a value was itself an array, its items should be included individually in the output rather than including the array itself (i.e., the result is flattened from an array of arrays to a single array). As a simple example, suppose we have the following API request "methodCalls": [[ "Foo/changes", { "accountId": "A1", "sinceState": "abcdef" }, "t0" ], [ "Foo/get", { "accountId": "A1", "#ids": { "resultOf": "t0", "name": "Foo/changes", "path": "/created" } }, "t1" ]] After executing the first method call, the "methodResponses" array is: [[ "Foo/changes", { "accountId": "A1", "oldState": "abcdef", "newState": "123456", "hasMoreChanges": false, "created": [ "f1", "f4" ], "updated": [], "destroyed": [] }, "t0" ]] To execute the "Foo/get" call, we look through the arguments and find there is one with a "#" prefix. To resolve this, we apply the algorithm above: 1. Find the first response with method call id "t0". The "Foo/ changes" response fulfils this criterion. 2. Check that the response name is the same as in the result reference. It is, so this is fine. 3. Apply the "path" as a JSON Pointer to the arguments object. This simply selects the "created" property, so the result of evaluating is: [ "f1", "f4" ].
The JMAP server now continues to process the "Foo/get" call as though the arguments were: { "accountId": "A1", "ids": [ "f1", "f4" ] } Now, a more complicated example using the JMAP Mail data model: fetch the "from"/"date"/"subject" for every Email in the first 10 Threads in the inbox (sorted newest first): [[ "Email/query", { "accountId": "A1", "filter": { "inMailbox": "id_of_inbox" }, "sort": [{ "property": "receivedAt", "isAscending": false }], "collapseThreads": true, "position": 0, "limit": 10, "calculateTotal": true }, "t0" ], [ "Email/get", { "accountId": "A1", "#ids": { "resultOf": "t0", "name": "Email/query", "path": "/ids" }, "properties": [ "threadId" ] }, "t1" ], [ "Thread/get", { "accountId": "A1", "#ids": { "resultOf": "t1", "name": "Email/get", "path": "/list/*/threadId" } }, "t2" ], [ "Email/get", { "accountId": "A1", "#ids": { "resultOf": "t2", "name": "Thread/get", "path": "/list/*/emailIds" }, "properties": [ "from", "receivedAt", "subject" ] }, "t3" ]]
After executing the first 3 method calls, the "methodResponses" array might be: [[ "Email/query", { "accountId": "A1", "queryState": "abcdefg", "canCalculateChanges": true, "position": 0, "total": 101, "ids": [ "msg1023", "msg223", "msg110", "msg93", "msg91", "msg38", "msg36", "msg33", "msg11", "msg1" ] }, "t0" ], [ "Email/get", { "accountId": "A1", "state": "123456", "list": [{ "id": "msg1023", "threadId": "trd194" }, { "id": "msg223", "threadId": "trd114" }, ... ], "notFound": [] }, "t1" ], [ "Thread/get", { "accountId": "A1", "state": "123456", "list": [{ "id": "trd194", "emailIds": [ "msg1020", "msg1021", "msg1023" ] }, { "id": "trd114", "emailIds": [ "msg201", "msg223" ] }, ... ], "notFound": [] }, "t2" ]] To execute the final "Email/get" call, we look through the arguments and find there is one with a "#" prefix. To resolve this, we apply the algorithm: 1. Find the first response with method call id "t2". The "Thread/ get" response fulfils this criterion.
2. "Thread/get" is the name specified in the result reference, so this is fine. 3. Apply the "path" as a JSON Pointer to the arguments object. Token by token: 1. "list": get the array of thread objects 2. "*": for each of the items in the array: a. "emailIds": get the array of Email ids b. Concatenate these into a single array of all the ids in the result. The JMAP server now continues to process the "Email/get" call as though the arguments were: { "accountId": "A1", "ids": [ "msg1020", "msg1021", "msg1023", "msg201", "msg223", ... ], "properties": [ "from", "receivedAt", "subject" ] } The ResultReference performs a similar role to that of the creation id, in that it allows a chained method call to refer to information not available when the request is generated. However, they are different things and not interchangeable; the only commonality is the octothorpe used to indicate them.3.8. Localisation of User-Visible Strings
If returning a custom string to be displayed to the user, for example, an error message, the server SHOULD use information from the Accept-Language header of the request (as defined in Section 5.3.5 of [RFC7231]) to choose the best available localisation. The Content- Language header of the response (see Section 3.1.3.2 of [RFC7231]) SHOULD indicate the language being used for user-visible strings. For example, suppose a request was made with the following header: Accept-Language: fr-CH, fr;q=0.9, de;q=0.8, en;q=0.7, *;q=0.5 and a method generated an error to display to the user. The server has translations of the error message in English and German. Looking at the Accept-Language header, the user's preferred language is French. Since we don't have a translation for this, we look at the
next most preferred, which is German. We have a German translation, so the server returns this and indicates the language chosen in a Content-Language header like so: Content-Language: de3.9. Security
As always, the server must be strict about data received from the client. Arguments need to be checked for validity; a malicious user could attempt to find an exploit through the API. In case of invalid arguments (unknown/insufficient/wrong type for data, etc.), the method MUST return an "invalidArguments" error and terminate.3.10. Concurrency
Method calls within a single request MUST be executed in order. However, method calls from different concurrent API requests may be interleaved. This means that the data on the server may change between two method calls within a single API request.4. The Core/echo Method
The "Core/echo" method returns exactly the same arguments as it is given. It is useful for testing if you have a valid authenticated connection to a JMAP API endpoint.4.1. Example
Request: [[ "Core/echo", { "hello": true, "high": 5 }, "b3ff" ]] Response: [[ "Core/echo", { "hello": true, "high": 5 }, "b3ff" ]]