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-supplieduidon create is rejected (invalidProperties) — the server mints it, which is what makes the CardDAV mapping unambiguous. addressBookIdsis optional on create: an omitted membership files the card into the default address book.- Version history is first-class.
ContactCard/getHistorylists prior versions of a card,restoreVersionbrings 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
PUTthat 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 withsource: "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:
mayEditis 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 fromeditorIdsis 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.
cardholds the JSContact card and nothing else: projection keys such assourceordirectoryIdinside it are refused, not quietly overwritten.
notFound, invalidProperties over coercion, accountNotFound, stateMismatch.