Skip to main content
Contacts implement RFC 9610 (urn:ietf:params:jmap:contacts), with cards as JSContact objects (RFC 9553). The same data is served over CardDAV.

Objects and methods

Behaviours worth knowing

  • The server owns uid. A client-supplied uid on create is rejected (invalidProperties) — the server mints it, which is what makes the CardDAV mapping unambiguous.
  • addressBookIds is optional on create: an omitted membership files the card into the default address book.
  • Version history is first-class. ContactCard/getHistory lists prior versions of a card, restoreVersion brings one back — edits from any protocol (JMAP or CardDAV) are versioned the same way.
  • Round-trip is lossless. Name and address components map completely between vCard (3.0/4.0) and JSContact in both directions; vCard properties without a JSContact model are preserved rather than dropped. A CardDAV PUT that would duplicate an existing UID from another source is refused, never silently forked.
  • Deprecation is advertised honestly. The account capability object announces the AddressBook-model posture so clients can adapt, rather than discovering behaviour by trial.

Tenant contact directories

An organization can publish shared contact sets — “Clients”, “Staff” — maintained on the admin plane. The design choice that shapes the whole surface: a directory is not a container members file cards into. Members never see a container at all. What a member sees is their own address book, where the entries arrive as read-only cards with source: "organization" and directorySource: "tenant:<directoryId>" — the same plane an external LDAP or SCIM connector feeds. A client that already renders organization-sourced contacts needs no new screen to show them. Who may write what. Directory objects themselves are gated on the tenant-admin role. Entries are gated on that directory’s editorIds, which are principal ids: naming a group is valid and normal, and membership is resolved through the grantee expansion at the access check rather than stored expanded, so adding someone to a group grants them editing without touching the directory. Two behaviours worth knowing before you build on it:
  • mayEdit is computed for you, and you cannot compute it yourself. It follows the caller’s tenant role plus the server-side group expansion, so two accounts legitimately read different values for the same directory — and deriving it from editorIds is impossible, since that list is blind to group membership by design. This is exactly why it is declared in the published canon rather than left for a client to infer from the TypeScript type.
  • The server’s bookkeeping is not yours to write. card holds the JSContact card and nothing else: projection keys such as source or directoryId inside it are refused, not quietly overwritten.
Directory content is organization data, not personal data held on behalf of a member: it is purged by deleting entries, and it does not travel with an individual account’s erasure. Strictness rules are identical to the mail surface: unknown ids in notFound, invalidProperties over coercion, accountNotFound, stateMismatch.