blob: 2498640cb125b37bf0ec7d84a77b98b7a36eb49f [file] [log] [blame]
# JMAP: The Spec
<div id="last-update">Last updated 2016-01-15</div>
This is a specification. It is meant to be readable but it also has to be comprehensive, so it can be dense in places. If you want to get a quick idea of how JMAP works, you should probably read the [guide for client developers](client.html) first. This has lots of example exchanges and should give you a good feel for what JMAP is all about. The spec is heavier going; it attempts to document exactly what each method should do, and what should happen in a myriad of edge cases.
There are undoubtably edge cases that are not yet covered. If you find one, please email <editor@jmap.io> or make a pull request on GitHub if you have a proposed fix.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC2119](https://tools.ietf.org/html/rfc2119).
## The JSON API model
JSON is a text-based data interchange format as specified in [RFC7159](https://tools.ietf.org/html/rfc7159). The I-JSON format defined in [RFC7493](https://tools.ietf.org/html/rfc7493) is a strict subset of this, adding restrictions to avoid potentially confusing scenarios (for example, it mandates that an object MUST NOT have two properties with the same key). All data sent from the client to the server or from the server to the client MUST be valid I-JSON according to the RFC, encoded in UTF-8.
### The structure of an exchange
The client initiates an API request by sending the server a JSON array. Each element in this array is another array representing a method invocation on the server. The server will process the method calls and return a response consisting of an array in the same format. Each method call always contains three elements:
1. The **name** of the method to call, or the name of the response from the server. This is a `String`.
2. An `Object` containing *named* **arguments** for that method or response.
3. A **client id**: an arbitrary `String` to be echoed back with the responses emitted by that method call (as we'll see lower down, a method may return 1 or more responses, as some methods make implicit calls to other ones).
Example query:
[
["method1", {"arg1": "arg1data", "arg2": "arg2data"}, "#1"],
["method2", {"arg1": "arg1data"}, "#2"],
["method3", {}, "#3"]
]
The method calls MUST be processed sequentially, in order. Each API request
(which, as shown, may contain multiple method calls) receives a JSON
response in exactly the same format. The output of the methods MUST be added
to the array in the same order as the methods are processed.
Example response:
[
["responseFromMethod1", {"arg1": 3, "arg2": "foo"}, "#1"],
["responseFromMethod2", {"isBlah": true}, "#2"],
["anotherResponseFromMethod2", {"data": 10, "yetmoredata": "Hello"}, "#2"],
["aResponseFromMethod3", {}, "#3"]
]
### Errors
If the data sent as an API request is not valid JSON or does not match the structure above, an error will be returned at the transport level. For example, when using JMAP over HTTP, a `400 Bad Request` error will be returned at the HTTP level.
Possible errors for each method are specified in the method descriptions. If a method encounters an error, an `error` response must be inserted at the current point in the output array and, unless otherwise specified, no further processing must happen within that method.
Any further method calls in the request MUST then be processed as normal.
An `error` response looks like this:
["error", {
type: "unknownMethod"
}, "client-id"]
The response name is `error`, and it has a type property as specified in the method description. Other properties may be present with further information; these are detailed in the method descriptions where appropriate.
If an unknown method is called, an `unknownMethod` error (this is the type shown in the example above) MUST be inserted and then the next method call MUST be processed as normal.
If an unknown argument or invalid arguments (wrong type, or in violation of other specified constraints) are supplied to a method, an `invalidArguments` error MUST be inserted and then the next method call MUST be processed as normal.
### Vendor-specific extensions
Individual services will have custom features they wish to expose over JMAP. This may take the form of extra datatypes and/or methods not in the spec, or extra arguments to JMAP methods, or extra properties on existing data types (which may also appear in arguments to methods that take property names). To ensure compatibility with clients that don't know about a specific custom extension, and for compatibility with future versions of JMAP, the server MUST ONLY expose these extensions if the client explicitly opts in. Without opt-in, the server MUST just follow the spec and reject anything that does not conform to it as specified.
Any vendor extensions supported by the server are advertised to the client in the capabilities property on the Account object. The client opt-in happens at the transport layer (see the next section).
### JMAP over HTTPS
To make an API request over HTTP (support for other protocols may be added in future extensions to the spec), the client makes an authenticated POST request to the API URL; see the Authentication section of the spec for how to discover this URL and how to authenticate requests.
The request MUST have a content type of `application/json` and be encoded in utf-8.
The request MAY include an `X-JMAP-Version` header, the value of which is a number for the spec version the client would like to use (the list of versions supported by each account can be discovered by the client by inspecting the capabilities property of the Account object). If omitted, the server MUST presume a value equal to the *lowest* supported version common to all accounts.
The request MAY include an 'X-JMAP-Extensions' header, the value of which is a comma-separated list of 'vendor-extension-name:extension-version` the client would like to use. For example, "com.fastmail.message:1,com.fastmail.savedSearch:4". Any white-space should be ignored when evaluating this header.
The server will respond with one of the following HTTP response codes:
#### `200`: OK
The API request was successful. The response will be of type `application/json` and consists of the response to the API calls, as described above.
#### `400`: Bad Request
The request was malformed. For example, it may have had the wrong content type, or have had a JSON object that did not conform to the API calling structure (see *The structure of an exchange* above). The client SHOULD NOT retry the same request.
#### `401`: Unauthorized
The `Authorization` header was missing or did not contain a valid token. Reauthenticate and then retry the request. There is no content in the response.
#### `404`: Not Found
The API endpoint has moved. See the Authentication section of the spec for how to rediscover the current URL to use. There is no content in the response.
#### `412`: Precondition Failed
This means either the JMAP version specified in an `X-JMAP-Version` header, or an extension/version specified in an `X-JMAP-Extensions` header is not supported by one of the accounts used in a method.
After authentication, but before processing any methods, the server MUST scan the methods in the request and check the requested JMAP version and extensions are supported by all the accounts referenced by the methods (unknown account ids MUST just be skipped; these method calls will error out when processed). If there are any accounts that do not support the requested version and extensions, this HTTP error code is returned and the server MUST NOT process any of the methods.
#### `500`: Internal Server Error
Something has gone wrong internally, and the server is in a broken state. Don't automatically retry. There is no content in the response.
#### `503`: Service Unavailable
The server is currently down. Try again later with exponential backoff. There is no content in the response.
### 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 should return an `invalidArguments` error and terminate.
### Concurrency
To ensure the client always sees a consistent view of the data, the state accessed by a method call MUST NOT change during the execution of the method, except due to actions by the method call itself. The state MAY change in-between method calls (even within a single API request).
### The Number datatype
The JSON datatypes are limited to those found in JavaScript. A `Number` in JavaScript is represented as a signed double (64-bit floating point). However, except where explicitly specified, all numbers used in this API are unsigned integers <= 2^53 (the maximum integer that may be reliably stored in a double). This implicitly limits the maximum length of message lists in queries and the like.
### The Date datatypes
Where the API specifies `Date` as a type, it means a string in [RFC3339](https://tools.ietf.org/html/rfc3339) *date-time* format, with the *time-offset* component always `Z` (i.e. the date-time MUST be in UTC time) and *time-secfrac* always omitted. The "T" and "Z" MUST always be upper-case. For example, `"2014-10-30T14:12:00Z"`.
Where the API specifies `LocalDate` as a type, it means a string in the same format as `Date`, but with the `Z` omitted from the end. This only occurs in relation to calendar events. The interpretation in absolute time depends upon the time zone for the event, which MAY not be a fixed offset (for example when daylight saving time occurs).
### Use of `null`
Unless otherwise specified, a missing property in a request, response or object MUST be intepreted exactly the same as that property having the value `null`. If `null` is not a valid value for that property this would typically cause an error to occur. This rule does not apply to the [top-level datatypes](#data-model-overview), where a missing property usually indicates that the sender wants to leave the existing property value untouched (e.g. in a [*setFoos*](#setfoos) request or a [*getFooUpdates*](#getfooupdates) response).
### CRUD methods
JMAP defines various types of objects and provides a uniform interface for creating, retrieving, updating and deleting them. A **data type** is a collection of named, typed properties, just like the schema for a database table. Each row of the table is a **record**. For a `Foo` data type, records of that type would be fetched via a `getFoos` call and modified via a `setFoos` call. Delta updates may be fetched via a `getFooUpdates` call. These methods all follow a standard format as described below.
### getFoos
Objects of type **Foo** are fetched via a call to *getFoos*. Methods with a name starting with `get` MUST NOT alter state on the server.
This method may take some or all of the following arguments. The getter for a particular data type may not implement all of the arguments (for example if the data type only has two properties, there is little point in being able to just return one of them etc.); see the docs of the type in question. However, if one of the following arguments is available, it will behave exactly as specified below.
- **ids**: `String[]|null`
The ids of the Foo objects to return. If `null` then **all** records of the data type are returned.
- **properties**: `String[]|null`
If supplied, only the properties listed in the array are returned for each Foo object. If `null`, all properties of the object are returned. The id of the object is **always** returned, even if not explicitly requested.
- **sinceState**: `String|null`
The *state* argument from a *foos* response may be passed back to future *getFoos* calls as the *sinceState* argument. If the current state is the same, the server SHOULD skip fetching the records and return a result indicating there is no change (this is essentially like an ETag). Most types support the more sophisticated *getFooUpdates* call instead to allow for delta updates. However, for small collections of data that change infrequently, this might be used. If available, this argument is always optional.
The response to `getFoos` is called `foos`. It has the following arguments:
- **state**: `String`
A string representing the state on the server for **all** the data of this type. If the data changes, this string will change. It is used to get delta updates, if supported for the type.
- **list**: `Foo[]|null`
An array of the Foo objects requested. This is the **empty array** if no objects were requested (and *ids* argument was passed with an empty array), or none were found (the user has no objects of this type, or none of the ids given were valid). If *sinceState* was supplied and it is identical to the current state, this property is `null` (the client already has up to date data so the server may skip returning it).
- **notFound**: `String[]|null`
This array contains the ids passed to the method for records that do not exist, or `null` if all requested ids were found. It will always be `null` if the *ids* argument in the call was `null`.
The following error may be returned instead of the `foos` response:
`invalidArguments`: Returned if one of the arguments is of the wrong type, or otherwise invalid. A `description` property MAY be present on the response object to help debug with an explanation of what the problem was.
### getFooUpdates
When the state of the set of Foo records changes on the server (whether due to creation, updates or deletion), the *state* property of the *foos* response will change. The *getFooUpdates* call allows a client to efficiently update the state of any its Foo cache to match the new state on the server. It takes the following arguments:
- **sinceState**: `String`
The current state of the client. This is the string that was returned as the *state* argument in the *foos* response. The server will return the changes made since this state.
- **maxChanges**: `Number|null`
The maximum number of Foo ids to return in the response. The server MAY choose to clamp this value to a particular maximum or set a maximum if none is given by the client. If supplied by the client, the value MUST be a positive integer greater than 0. If a value outside of this range is given, the server MUST reject the call with an `invalidArguments` error.
- **fetchRecords**: `Boolean|null`
If `true`, after outputting the *fooUpdates* response, the server will make an implicit call to *getFoos* with the *changed* property of the response as the *ids* argument. If `false` or `null`, no implicit call will be made.
- **fetchRecordProperties**: `String[]|null`
If the *getFoos* method takes a *properties* argument, this argument is passed through on implicit calls (see the *fetchRecords* argument).
The response to *getFooUpdates* is called *fooUpdates*. It has the following arguments:
- **oldState**: `String`
This is the *sinceState* argument echoed back; the state from which the server is returning changes.
- **newState**: `String`
This is the state the client will be in after applying the set of changes to the old state.
- **hasMoreUpdates**: `Boolean`
If `true`, the client may call *getFooUpdates* again with the *newState* returned to get further updates. If `false`, *newState* is the current server state.
- **changed**: `String[]`
An array of Foo ids for records which have been created or changed but not destroyed since the oldState.
- **removed**: `String[]`
An array of Foo ids for records which have been destroyed since the old state.
The *maxChanges* argument (and *hasMoreUpdates* response argument) is available for data types with potentially large amounts of data (i.e. those for which there is a *getFooList* method available for loading the data in pages). If a *maxChanges* is supplied, or set automatically by the server, the server must try to limit the number of ids across *changed* and *removed* to the number given. If there are more changes than this between the client's state and the current server state, the update returned MUST take the client to an intermediate state, from which the client can continue to call *getFooUpdates* until it is fully up to date. The server MAY return more ids than the *maxChanges* total if this is required for it to be able to produce an update to an intermediate state, but it SHOULD try to keep it close to the maximum requested.
If a Foo record has been modified AND deleted since the oldState, the server should just return the id in the *removed* response, but MAY return it in the changed response as well. If a Foo record has been created AND deleted since the oldState, the server should remove the Foo id from the response entirely, but MAY include it in the *removed* response, and optionally the *changed* response as well.
The following errors may be returned instead of the *fooUpdates* response:
`invalidArguments`: Returned if the request does not include one of the required arguments, or one of the arguments is of the wrong type, or otherwise invalid. A *description* property MAY be present on the response object to help debug with an explanation of what the problem was.
`cannotCalculateChanges`: Returned if the server cannot calculate the changes from the state string given by the client. Usually due to the client's state being too old, or the server being unable to produce an update to an intermediate state when there are too many updates. The client MUST invalidate its Foo cache. The error object MUST also include a `newState: String` property with the current state for the type.
### setFoos
Modifying the state of Foo objects on the server is done via the *setFoos* method. This encompasses creating, updating and destroying Foo records. This has two benefits:
1. It allows the server to sort out ordering and dependencies that may exist if doing multiple operations at once (for example to ensure there is always a minimum number of a certain record type).
2. Only a single call is required to make all changes to a particular type, so the *ifInState* requirement will not be invalidated by a previous method call in the same request.
The *setFoos* method takes the following arguments:
- **ifInState**: `String|null`
This is a state string as returned by the *getFoos* method. If supplied, the string must match the current state, otherwise the method will be aborted and a `stateMismatch` error returned. If `null`, the change will be applied to the current state.
- **create**: `String[Foo]|null`
A map of *creation id* (an arbitrary string set by the client) to Foo objects (containing all properties except the id, unless otherwise stated in the specific documentation of the data type). If `null`, no objects will be created.
- **update**: `String[Foo]|null`
A map of id to a Foo object. The object may omit any property; only properties that have changed need be included. If `null`, no objects will be updated.
- **destroy**: `String[]|null`
A list of ids for Foo objects to permanently delete. If `null`, no objects will be deleted.
Each create, update or destroy is considered an atomic unit. It is permissible for the server to commit some of the changes but not others, however it is not permissible to only commit part of an update to a single record (e.g. update a *name* property but not a *count* property, if both are supplied in the update object).
If a create, update or destroy is rejected, the appropriate error should be added to the notCreated/notUpdated/notDestroyed property of the response and the server MUST continue to the next create/update/destroy. It does not terminate the method.
If an id given cannot be found, the update or destroy MUST be rejected with a `notFound` set error.
Some record objects may hold references to others (foreign keys). When records are created or modified, they may reference other records being created *in the same API request* by using the creation id prefixed with a `#`. The order of the method calls in the request by the client MUST be such that the record being referenced is created in the same or an earlier call. The server thus never has to look ahead. Instead, while processing a request (a series of method calls), the server MUST keep a simple map for the duration of the request of creation id to record id for each newly created record, so it can substitute in the correct value if necessary in later method calls.
The response to *setFoos* is called *foosSet*. It has the following arguments:
- **oldState**: `String|null`
The state string that would have been returned by *getFoos* before making the requested changes, or `null` if the server doesn't know what the previous state string was.
- **newState**: `String`
The state string that will now be returned by *getFoos*.
- **created**: `String[Foo]`
A map of the creation id to an object containing any **server-assigned** properties of the Foo object (including the id) for all successfully created records.
- **updated**: `String[]`
A list of Foo ids for records that were successfully updated.
- **destroyed**: `String[]`
A list of Foo ids for records that were successfully destroyed.
- **notCreated**: `String[SetError]`
A map of creation id to a SetError object for each record that failed to be created. The possible errors are defined in the description of the method for specific data types.
- **notUpdated**: `String[SetError]`
A map of Foo id to a SetError object for each record that failed to be updated. The possible errors are defined in the description of the method for specific data types.
- **notDestroyed**: `String[SetError]`
A map of Foo id to a SetError object for each record that failed to be destroyed. The possible errors are defined in the description of the method for specific data types.
A **SetError** object has the following properties:
- **type**: `String`
The type of error.
- **description**: `String|null`
A description of the error to display to the user.
Other properties may also be present on the object, as described in the relevant methods.
The following errors may be returned instead of the `foosSet` response:
`invalidArguments`: Returned if one of the arguments is of the wrong type, or otherwise invalid. A `description` property MAY be present on the response object to help debug with an explanation of what the problem was.
`stateMismatch`: Returned if an `ifInState` argument was supplied and it does not match the current state.