| Internet-Draft | JMAP Object History | October 2026 |
| Gondwana | Expires 19 April 2027 | [Page] |
The JMAP base protocol (RFC8620) provides methods for synchronizing the current state of data objects between client and server. This extension adds the ability to retrieve historical versions of objects, including objects that have been destroyed, by extending the standard Foo/get method.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 19 April 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
JMAP ([JMAP-CORE] — JSON Meta Application Protocol) provides a standard set of methods for each data type: Foo/get, Foo/changes, Foo/set, Foo/query, and Foo/queryChanges. These methods operate on the current state of objects. The Foo/changes method tells a client which objects have changed, but does not provide the previous values.¶
In many applications it is useful to access historical versions of objects. For example, a user may want to see previous versions of a contact record ([JMAP-CONTACTS]) or recover a deleted email ([JMAP-MAIL]).¶
This extension adds history support to the standard Foo/get
method. When the urn:ietf:params:jmap:object-history capability
is included in the request's using array, Foo/get gains
additional parameters that allow the client to request previous
versions of objects, including objects that have been destroyed.¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
The capabilities object is returned as part of the JMAP Session object; see [JMAP-CORE], Section 2.¶
This document defines an additional capability URI.¶
The capability urn:ietf:params:jmap:object-history being present
in the "accountCapabilities" property of an account represents
support for retrieving historical object versions in that account.
Servers that include the capability in one or more
"accountCapabilities" properties MUST also include the property in
the "capabilities" property.¶
The value of this property in the JMAP session "capabilities" property MUST be an empty object.¶
The value of this property in an account's "accountCapabilities" property is an object that MUST contain the following information on server capabilities for that account:¶
maxHistoryDuration: "UnsignedInt|null"¶
The number of seconds after replacement for which the server
intends to retain old versions, or null if the server does
not have a time-based retention policy. This is a statement
of server policy, not a guarantee: retention is best effort,
and the server MAY discard versions sooner (for example, due
to storage limits). A version whose replaced timestamp is
more than this many seconds ago has probably been discarded.
Clients that need to be certain an object remains available
should not destroy it.¶
supportedTypes: "String[]"¶
The names of the data types, from the "JMAP Data Types" registry [IANA-JMAP] or private types, for which the server supports the history extensions in this account. A server MAY support history for only a subset of the types it supports, and MAY retain history for only some objects of a supported type (for example, only for some folders). Clients MUST ignore type names they do not recognise.¶
When this capability is included in the request's using array,
the standard Foo/get method ([JMAP-CORE], Section 5.1) accepts
the following additional request arguments:¶
includeReplaced: "Boolean" (default: false)¶
If true, the server returns previous versions of the
requested objects in addition to the current version.
Each previous version appears as a separate entry in the
list array, with the same id as the current version.
Every entry includes an objectHistory property (see
below). Entries with the same id MUST be ordered by
objectHistory.version (ascending). Entries for
different objects MAY be interleaved.¶
includeDestroyed: "Boolean" (default: false)¶
If true, the server also returns objects that have been
destroyed. If includeReplaced is false, only the final
version before destruction is returned. If includeReplaced
is true, all retained historical versions of destroyed
objects are returned. For destroyed objects, every entry
has a non-null objectHistory.replaced value, since every
version has been superseded (the final version was replaced
by destruction).¶
historyAfter: "UTCDate|null" (default: null)¶
If supplied, only versions with an objectHistory.replaced
value strictly after this date are returned, plus the
current (live) version which has a null replaced value.
If null, no time-based filter is applied.¶
historyBefore: "UTCDate|null" (default: null)¶
If supplied, only versions with an objectHistory.replaced
value strictly before this date are returned. The current
(live) version, which has a null replaced value, is not
returned. If null, no time-based filter is applied.¶
A client can use this to page backwards through history: it
passes the oldest replaced value from the previous response
as historyBefore in the next request. When several ids are
requested, the client passes the oldest replaced value from
the whole response; any id with no remaining versions is
returned in notFound. Clients SHOULD pass the replaced
value exactly as received, rather than parsing and
re-serialising it, since re-serialising at a lower precision
could cause versions to be skipped.¶
historySkip: "UnsignedInt" (default: 0)¶
The number of entries to skip for each id, counting from the
most recent (the highest version number), before any entries
are returned. The current version, if it would otherwise be
returned, counts as one entry. For example, a client that
wants only versions older than the 20 most recent passes
historySkip: 20; any id with 20 or fewer matching entries
is returned in notFound.¶
A client can also use this to page backwards through history,
by adding the number of entries returned for an id to
historySkip in the next request. This is less robust than
historyBefore: if a new version is recorded between
requests, the window shifts and one entry is returned again.¶
historyLimit: "UnsignedInt|null" (default: null)¶
If supplied, the maximum number of entries to return in the
list array for each id, including the current version if
any. For each id, the server MUST return the most recent
entries (those with the highest version numbers) remaining
after historySkip is applied, not the oldest. If null, the
server returns all remaining retained versions, subject to
its own limits.¶
The server applies includeReplaced, includeDestroyed,
historyAfter, and historyBefore to select the matching
versions of each id, then skips historySkip entries, then
returns at most historyLimit of the remaining entries.¶
If the ids argument is null, the server returns all objects of
the type as per [JMAP-CORE], Section 5.1. If includeDestroyed
is true, this includes every destroyed object for which the server
still retains any version.¶
The "maxObjectsInGet" limit and the "requestTooLarge" error
([JMAP-CORE], Section 5.1) apply to the number of distinct ids,
not to the number of entries in the list array. Each id can
have up to historyLimit entries.¶
If, after applying includeReplaced, includeDestroyed,
historyAfter, historyBefore, and historySkip, no versions of a
requested id remain to be returned, that id is included in the
notFound array.¶
When includeReplaced or includeDestroyed is true, each object
in the list array includes an additional property:¶
objectHistory: "ObjectHistoryInfo"¶
An ObjectHistoryInfo object with the following properties:¶
version: "UnsignedInt" A per-object monotonically increasing number that provides ordering among the versions of a single object within a response. Version numbers MAY have gaps (e.g., a server may use internal sequence numbers such as modseq values). The current (live) version of an object always has the highest version number. Servers SHOULD return consistent version numbers across requests, but clients MUST NOT rely on this — version numbers exist primarily to provide ordering within a single response.¶
replaced: "UTCDate|null"
The date and time at which this version was superseded,
either by a subsequent update or by destruction. If null,
this is the current live version of the object. For
destroyed objects, every version has a non-null replaced
value — the final version's replaced is the time of
destruction. The replaced values of different versions
of the same object MUST be distinct, so that historyBefore
and historyAfter partition the history without ambiguity;
a server that records two versions with the same timestamp
can collapse them into a single version. The value MAY
include fractional seconds ([JMAP-CORE], Section 1.4),
but servers SHOULD NOT use more than millisecond precision,
as many client date libraries cannot represent more.¶
hasOlder: "Boolean"
If true, the server retains at least one older version of
this object than this one, whether or not that version was
returned in this response. A client can check hasOlder
on the oldest entry returned for an id to learn whether
more history is available, and retrieve it using
historyBefore or historySkip. The server MAY return
false even if older versions exist (e.g., if it cannot
efficiently determine whether older versions remain). If
the server returns true, the client MUST NOT assume the
older versions will still be available in a subsequent
request, as the server may purge history at any time.¶
When includeReplaced and includeDestroyed are both false
(the default), the objectHistory property is not included in
the response, and the behaviour is identical to standard Foo/get.¶
The client MAY include "objectHistory" in the properties argument.
It is returned if and only if includeReplaced or includeDestroyed
is true, regardless of whether it is listed in properties.¶
Which changes produce a version is server dependent: a version is a snapshot of the object that the server recorded at some point. The server is not required to create a new version for every individual change to an object. For example, rapid successive updates may be collapsed into a single version, and a server may choose not to record a version for changes that only affect server-computed properties. The server MAY also prune old versions in any order, so there can be gaps in the version history available to the client. Clients MUST NOT assume that the version history is complete or contiguous.¶
The values of server-computed properties (for example, the counts on a Mailbox) in a historical version are also server dependent. The server SHOULD return the value the property had at the time of that version if it has that information.¶
If a historical version contains a reference to a blob (for
example, the blobId of an Email), that blob MUST be downloadable
for as long as the server returns that version. A server that has
discarded a blob referenced by a version MUST NOT return that
version.¶
If includeReplaced or includeDestroyed is true and the data type
is not listed in the supportedTypes capability property, the
server MUST reject the call with an "invalidArguments" error.¶
If the data type is supported but the server does not retain
history for a particular object, it MUST still return the current
version with an objectHistory property when requested. In this
case, the server returns only the current version, with a replaced
of null.¶
Requesting the history of a contact, including all versions:¶
[["ContactCard/get", {
"accountId": "abc",
"ids": ["contact1"],
"properties": ["name", "emails"],
"includeReplaced": true,
"historyLimit": 10
}, "0"]]
¶
The response includes three versions: the original creation, a name update, and the current version with an additional email address:¶
[["ContactCard/get", {
"accountId": "abc",
"state": "abc123",
"list": [
{
"id": "contact1",
"name": {"full": "Robert Smith"},
"emails": {
"e1": {"address": "bob@example.com"}
},
"objectHistory": {
"version": 1,
"replaced": "2026-02-20T14:15:00Z",
"hasOlder": false
}
},
{
"id": "contact1",
"name": {"full": "Bob Smith"},
"emails": {
"e1": {"address": "bob@example.com"}
},
"objectHistory": {
"version": 2,
"replaced": "2026-03-01T09:00:00Z",
"hasOlder": true
}
},
{
"id": "contact1",
"name": {"full": "Bob Smith"},
"emails": {
"e1": {"address": "bob@example.com"},
"e2": {"address": "bob@personal.example"}
},
"objectHistory": {
"version": 3,
"replaced": null,
"hasOlder": true
}
}
],
"notFound": []
}, "0"]]
¶
A client can use Email/changes to discover which emails have been destroyed, then use a backreference to fetch their final state. This is useful for displaying a "recently deleted" view:¶
[
["Email/changes", {
"accountId": "abc",
"sinceState": "state42"
}, "0"],
["Email/get", {
"accountId": "abc",
"#ids": {
"resultOf": "0",
"name": "Email/changes",
"path": "/destroyed"
},
"properties": ["subject", "from", "receivedAt"],
"includeDestroyed": true
}, "1"]
]
¶
The Email/get response returns the final state of each
destroyed email. The objectHistory.replaced value is the
time of destruction:¶
[["Email/get", {
"accountId": "abc",
"state": "state43",
"list": [
{
"id": "email42",
"subject": "Meeting notes",
"from": [{"name": "Alice",
"email": "alice@example.com"}],
"receivedAt": "2026-03-10T09:00:00Z",
"objectHistory": {
"version": 1,
"replaced": "2026-03-15T16:30:00Z",
"hasOlder": false
}
},
{
"id": "email57",
"subject": "Project update",
"from": [{"name": "Bob",
"email": "bob@example.com"}],
"receivedAt": "2026-03-12T14:00:00Z",
"objectHistory": {
"version": 1,
"replaced": "2026-03-15T16:31:00Z",
"hasOlder": false
}
}
],
"notFound": []
}, "1"]]
¶
Since includeReplaced was not set, only the final version of
each destroyed email is returned. The non-null replaced
value indicates when each email was destroyed.¶
To restore an earlier version of an existing object, the client uses a normal Foo/set update with the values from that version.¶
To restore a destroyed object, the client uses Foo/set to create a
new object with the values from the version to be restored. When
this capability is included in the request's using array, the
client MAY include the id of the destroyed object in the create
request. The server MAY honour this, in which case the created
object has that id and its history continues from the destroyed
object's history, so the destroyed versions become previous
versions of the restored object. If the server does not wish to
reuse the id, it MUST reject the creation with an
"invalidProperties" SetError listing the id property; the client
can then retry without an id to create the object with a new id.¶
The server MUST reject a create with an id that is not the id of
a destroyed object of the same type in the same account. Apart
from restoring a destroyed object in this way, the server MUST NOT
reuse the id of a destroyed object for which it retains any
history.¶
This section is not normative.¶
History is returned through the existing Foo/get method rather than
a new method. The entries in the list array are ordinary objects
of the data type, so a client can feed them to the same parser it
uses for current objects, and the existing properties argument
works unchanged. It also allows a client to fetch the final state
of many destroyed objects in a single call, using a backreference
to the destroyed list from Foo/changes, as shown in the examples
above.¶
The history metadata is embedded in each object as the
objectHistory property, rather than wrapping each object in a
separate container, for the same reason: the entries remain
ordinary objects. This follows the existing pattern of JMAP-specific
properties such as id being added to objects whose format is
defined elsewhere; the objectHistory name is reserved in the
relevant property registries (see Section 6).¶
Each entry is a full copy of the object rather than a patch against the previous version. Patches would save space, but the client would need every intermediate version to reconstruct any one of them, which conflicts with the server's freedom to prune versions, and the final version of a destroyed object would need to be returned in full anyway.¶
Every superseded version carries a replaced timestamp. The
server always knows, at least approximately, when it replaced a
snapshot it was keeping, and the timestamp is what a client needs
to present history to a user and to filter it with historyAfter.¶
IANA is requested to register the "Object History" Capability as follows:¶
Capability Name: urn:ietf:params:jmap:object-history¶
Intended use: common¶
Change Controller: IETF¶
Specification document: this document¶
Security and privacy considerations: this document, Security Considerations¶
This document adds a "Supports History" column to the "JMAP Data Types" registry [IANA-JMAP]. The value is "Yes" or "No", indicating whether the data type supports the history extensions defined in this document.¶
The initial values for existing registrations are:¶
| Type Name | Supports History |
|---|---|
| Core | No |
| PushSubscription | No |
| Mailbox | Yes |
| Thread | No |
| Yes | |
| EmailDelivery | No |
| SearchSnippet | No |
| Identity | Yes |
| EmailSubmission | No |
| VacationResponse | Yes |
| MDN | No |
| Quota | No |
| SieveScript | Yes |
| Principal | No |
| ShareNotification | No |
| AddressBook | Yes |
| ContactCard | Yes |
| Calendar | Yes |
| CalendarEvent | Yes |
| CalendarEventNotification | No |
| ParticipantIdentity | Yes |
The default value for this column is "No". New registrations that do not explicitly include a value are assumed not to support history.¶
This column records whether history is meaningful for a data type.
Whether a particular server supports history for a type is
advertised in the supportedTypes capability property.¶
IANA is requested to register the following property in the "JSContact Properties" registry defined in [JSCONTACT], Section 3, so that the name cannot be used for a property of a JSContact Card.¶
Property Name: objectHistory¶
Property Type: not applicable¶
Property Context: Card¶
Intended Usage: reserved¶
Since Version: 1.0¶
Change Controller: IETF¶
Reference: this document¶
IANA is requested to register the following property in the "JSCalendar Properties" registry defined in [JSCALENDAR], Section 8.2, so that the name cannot be used for a property of a JSCalendar object.¶
Property Name: objectHistory¶
Property Type: not applicable¶
Property Context: Event, Task, Group¶
Intended Usage: reserved¶
Change Controller: IETF¶
Reference: this document¶
Specifications that define a JMAP data type whose object format is
also governed by a separate property registry SHOULD reserve the
objectHistory property name in that registry.¶
All security considerations from [JMAP-CORE] apply to this document.¶
Retaining historical versions of objects can consume significant
server storage. Servers SHOULD impose reasonable limits on
history retention, either by duration (advertised via
maxHistoryDuration) or by total storage. Servers MAY
silently discard history entries when storage limits are
reached.¶
Historical versions of an object MUST respect the same access controls as the current version. A user who does not have permission to read an object MUST NOT be able to read its history.¶
Access rights can change over time, for example when an object is shared with a new user, or moved into a shared container. The server MUST NOT return a historical version to a user who could not have read that version at the time it was current. This requires the server to track changes to access rights. A server that does not keep a full history of access rights can take a conservative approach: record the point at which the access rights affecting an object (or its containing object, such as a Mailbox or AddressBook) last changed, for example as a modseq, and not return any version that was replaced before that point.¶
Similarly, for destroyed objects, the server MUST NOT reveal the existence or content of a destroyed object to a user who could not read it at the time it was destroyed.¶
Object history may reveal information that the user or an administrator intended to remove. For example, a contact record that was updated to remove a phone number will still have that phone number visible in its history. Servers SHOULD provide administrators with the ability to purge history for specific objects when required for privacy or compliance reasons.¶
When a server supports this extension, updating or destroying an object does not immediately remove the previous content from the server; it remains retrievable by anyone with read access to the object until the server discards it under its retention policy. Users may not expect this. This is not new in kind (servers commonly retain deleted data in backups, and many mail systems offer a "recently deleted" view), but this extension makes such data retrievable through the protocol.¶
Servers SHOULD make their retention policy visible to users, for
example through the maxHistoryDuration capability property and
the server's user interface. Clients that present a "destroy"
operation to users SHOULD NOT describe it as irreversible or
permanent when the server advertises this capability for that data
type. Whether, and how, a user can request that history be purged
before the end of the retention period is a matter of server
policy and is outside the scope of this document.¶
EDITOR: please remove this section before publication.¶
The source of this document exists on github at: https://github.com/brong/draft-gondwana-jmap-object-history/¶
draft-ietf-jmap-object-history-02¶
Replaced the top-level hasMoreHistory response property with a
per-entry objectHistory.hasOlder property, so the client can
tell which ids have more history.¶
Added historySkip, to skip the N most recent versions of each
id; ids with N or fewer versions are returned in notFound.¶
draft-ietf-jmap-object-history-01¶
Removed "Updates: 8620"; the JMAP Data Types registry is not defined by RFC 8620. The registry is now referenced by name rather than by its defining document.¶
Corrected the registry table to match the current registry.¶
historyLimit is now per id.¶
Defined behaviour for ids: null with includeDestroyed, and
clarified that "maxObjectsInGet" counts ids, not entries.¶
Ids with no matching versions are returned in notFound.¶
Which changes create a version, and the values of server-computed properties, are server dependent.¶
Blobs referenced by returned versions must be downloadable.¶
Expanded access control considerations for changing access rights.¶
Added supportedTypes to the account capability; requesting
history for an unsupported type is an error.¶
Reserved the objectHistory property name in the JSContact and
JSCalendar property registries.¶
Added acknowledgements.¶
Added a Design Rationale section (Foo/get rather than a new method, embedded metadata, full objects rather than patches, timestamps).¶
Added a Restoring Objects section: a client may ask to recreate a destroyed object with its old id, and the server may accept or reject; ids of destroyed objects are otherwise not reused.¶
maxHistoryDuration is now a best-effort statement of server policy.¶
Clarified that "objectHistory" may appear in properties.¶
Added Privacy Considerations.¶
Added historyBefore for paging backwards through history;
versions of the same object have distinct replaced values, with
at most millisecond precision.¶
Removed the audit use case from the introduction.¶
draft-ietf-jmap-object-history-00¶
Uploaded with working-group adopted name.¶
draft-gondwana-jmap-object-history-00¶
initial proposal¶
The author would like to thank Ben Bucksch, Mauro De Gennaro, Arnt Gulbrandsen, Neil Jenkins, and Robert Stepanek for their feedback on this document.¶
{backmatter}¶