Building a CardDAV client

What is this document?

As server developers, we get a lot of questions on how to interact with a CardDAV server. This document explains how to integrate correctly with a CardDAV server.

This document (should) apply for any CardDAV server, not just SabreDAV.

Clients

Before you build your own client, there's a chance there's already a client available for your programming language.

We've developed a PHP client that does some DAV-related stuff and makes it a tad easier. More information can be found on this wiki.

CalConnect DEVGUIDE maintains an updated list of CardDAV clients.

High-level protocol

CardDAV is defined by rfc6352. CardDAV is heavily inspired by its counterpart CalDAV, and is mostly regarded as simpler.

CardDAV builds on WebDAV. WebDAV itself extends HTTP.

Some operations will be very familiar if you already have experience with HTTP services (GET, PUT and DELETE), but a number of new methods have been added to this list (PROPFIND, PROPPATCH, REPORT, MKCOL, MKCALENDAR, ACL).

Most HTTP clients should just support methods they don't know about. So it's smart to simply use a stock HTTP client for your platform, if your platform does not already have a specialized CardDAV client.

vCards

Every contact is submitted as a vCard. Every compliant CardDAV client or Server must support vCard 3.0 (rfc2425 and rfc2426). vCard 2.1 is way too old and should always be rejected.

vCard 4.0 (rfc6350) also exists though, and is in many respects a massive improvement over vCard 3.0. vCard 4 must now always be encoded as UTF-8, and many inconsistencies and problems have been fixed.

However, compliant servers must specifically advertise that they support vCard 4.0, and clients must be willing to send vCard 3.0 if the server does not support it.

The current SabreDAV server supports vCard 4 and jCard, and will automatically convert in between vCard 3, 4 and jCard on demand.

One thing we specifically want to warn people for, is that even though the vCard format seems easy to parse and generate, there are a lot of little rules that make it complicated.

A simple vCard may look like this:

BEGIN:VCARD
VERSION:4.0
FN:Evert Pot
N:Pot;Evert;;;
END:VCARD

Don't fall into the trap of thinking every line is simply in the format propertyname colon propertyvalue.

There's:

Why did I write this list? Because if you're going to parse and generate vCards, you should either:

vCard parsers, per language

Language Library
PHP sabre/vobject
Java ez-vcard
Ruby vcard

CalConnect DEVGUIDE maintains an updated list of CardDAV libraries with additional entries.

Know of any other good vCard parsers? Let me know so I can list them.

XML

CardDAV servers also use XML for various things:

Retain full vCards!

In most cases, when integrating with foreign API's, you will figure out the remote data model, and write code to map that to the data model in your application. This tends to be some mapping code that is bidirectional and simply converts one data model (such as json or xml) to something local (such as an mvc model, database record or object property).

When integrating with CardDAV, it is not quite as simple.

The problem with simply mapping the vcard to your local data model, is that there is an potentially a lot of information to map. vCards can contain all sorts of information, and even allow applications to define new properties, and parameters on top of existing properties.

vCards can contain lot of different information, information about information, and application-specific information and you must support all of this.

If your data model is simpler than the vCard data model, this inherently means that data can get lost during conversion. E.g.: mapping back and forward, and reversing this again tends to be a 'lossy' process.

To illustrate, lets look at the protocol from a very high level. Simplisticly we will be doing a GET request (or equivalent) and later on a PUT request to update a vcard.

You must absolutely make sure that none of the information you received in a GET is lost when you perform the PUT.

Almost every client on the planet will even embed custom non-standard data in vCards. If you discard this data when performing PUT, you are destroying your users data.

So a common trick that implementors use AND WE DON'T RECOMMEND is

  1. Go through all the properties of a vCard
  2. Map the properties you support to a local data model
  3. Store all the properties that are not supported by the local data model in a separate place.

Then when the vCard is uploaded again with PUT, the 'unknown' properties are stitched back in.

We consider this to be a bad idea, because it ignores several vCard features:

Our recommendation

  1. Download the vCard
  2. Retain the entire vCard and store it locally, or at least in some lossless way
  3. Parse the vCard and populate your models with the information that is relevant to you.
  4. Keep a reference to which vCard property maps to what information in the model.

Now when something changes in a model (e.g.: a user changes an email address in your UI.)

  1. Model receives change (email address updated)
  2. Find the property in the vCard that originally mapped to the information in the model.
  3. Update the value in the vCard.
  4. Upload the vCard.

In an ideal world, your vCard is your model though.

Regardless of how this issue is solved (there may be better suggestions, we would love to hear it), not ensuring that original vCard is kept as close to the original as possible is guaranteed to trigger bugs and edge-cases for all sorts of CardDAV clients.

Typical urls

Note that the following url structure is typical for SabreDAV, but may be different for other servers. All these urls should be discovered by a client, but listing these here helps with illustrating the examples that follow:

url description
http://dav.example.org/ Root
http://dav.example.org/principals/johndoe/ A principal url
http://dav.example.org/addressbooks/johndoe/ The addressbook home
http://dav.example.org/addressbooks/johndoe/contacts/ An addressbook
http://dav.example.org/addressbooks/johndoe/contacts/foobarapp-2357-aeaat34.vcf A vcard

Authentication

Servers typically use HTTP Digest or HTTP Basic authentication. Your client should already support these. The Google CardDAV API uses OAuth2.

Operations

Retrieving addressbook information

To receive information about a URL, we use the PROPFIND method. In this case we're going to ask for the addressbooks display name and a so-called 'ctag'.

PROPFIND /addressbooks/johndoe/contacts/ HTTP/1.1
Depth: 0
Content-Type: application/xml; charset=utf-8

<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/">
  <d:prop>
     <d:displayname />
     <cs:getctag />
  </d:prop>
</d:propfind>

The PROPFIND request is a HTTP request, defined by WebDAV. PROPFIND allows the client to fetch properties from an url.

CardDAV uses many properties like this, but in this case we just fetch the 'displayname', which is the human-readable name the user gave the addressbook, and the ctag. The ctag must be stored for subsequent requests.

The request will return something like:

HTTP/1.1 207 Multi-status
Content-Type: application/xml; charset=utf-8

<d:multistatus xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/">
    <d:response>
        <d:href>/addressbooks/johndoe/contacts/</d:href>
        <d:propstat>
            <d:prop>
                <d:displayname>My Address Book</d:displayname>
                <cs:getctag>3145</cs:getctag>
            </d:prop>
            <d:status>HTTP/1.1 200 OK</d:status>
        </d:propstat>
    </d:response>
</d:multistatus>

This multistatus response is very common for Cal and WebDAV. Many requests return an xml document in this exact format, so it is worthwhile writing a standard parser.

The response gives us back the user, the values for the 2 properties and the status.

It is possible that a server does not support the ctag. In that case it will likely return 404 Not Found for the ctag, and 200 OK for the displayname.

Example:

HTTP/1.1 207 Multi-status
Content-Type: application/xml; charset=utf-8

<d:multistatus xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/">
    <d:response>
        <d:href>/addressbooks/johndoe/contacts/</d:href>
        <d:propstat>
            <d:prop>
                <d:displayname>My Address Book</d:displayname>
            </d:prop>
            <d:status>HTTP/1.1 200 OK</d:status>
        </d:propstat>
        <d:propstat>
            <d:prop>
                <cs:getctag />
            </d:prop>
            <d:status>HTTP/1.1 404 Not Found</d:status>
        </d:propstat>
    </d:response>
</d:multistatus>

So take note from this last response. Here we display that the status, such as the 404 and the 200 are not related to the existence of the url (/addressbooks/johndoe/contacts). The status codes are re-used to return information about the individual properties.

Downloading objects

Now we download every single object in this addressbook. To do this, we use a REPORT method.

REPORT /addressbooks/johndoe/contacts/ HTTP/1.1
Depth: 1
Content-Type: application/xml; charset=utf-8

<card:addressbook-query xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
    <d:prop>
        <d:getetag />
        <card:address-data />
    </d:prop>
</card:addressbook-query>

This request will return a large xml object with all the vCards, and their etags.

This report will return a multi-status object again:

HTTP/1.1 207 Multi-status
Content-Type: application/xml; charset=utf-8

<d:multistatus xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
    <d:response>
        <d:href>/addressbooks/johndoe/contacts/abc-def-fez-123454657.vcf</d:href>
        <d:propstat>
            <d:prop>
                <d:getetag>"2134-314"</d:getetag>
                <card:address-data>BEGIN:VCARD
                    VERSION:3.0
                    FN:My Mother
                    UID:abc-def-fez-1234546578
                    END:VCARD
                </card:address-data>
            </d:prop>
            <d:status>HTTP/1.1 200 OK</d:status>
        </d:propstat>
    </d:response>
    <d:response>
        <d:href>/addressbooks/johndoe/contacts/someapplication-12345678.vcf</d:href>
        <d:propstat>
            <d:prop>
                <d:getetag>"5467-323"</d:getetag>
                <card:address-data>BEGIN:VCARD
                    VERSION:3.0
                    FN:Your Mother
                    UID:foo-bar-zim-gir-1234567
                    END:VCARD
                </card:address-data>
            </d:prop>
            <d:status>HTTP/1.1 200 OK</d:status>
        </d:propstat>
    </d:response>
</d:multistatus>

This addressbook only contained 2 contacts.

So after you retrieved and processed these, for each object you must retain:

In this case all urls ended with .vcf. This is often the case, but you must not rely on this. In this case the UID in the vCards was also identical to a part of the url. This too is often the case, but again not something you can rely on, so don't make any assumptions.

The url and the UID have no meaningful relationship, so treat both those items as separate unique identifiers.

Finding out if anything changed

To see if anything in an addressbook changed, we simply request the ctag again on the addressbook. If the ctag did not change, you still have the latest copy.

This is the purpose of the ctag. Every time anything in the address book changes, the ctag must also change.

If it did change, you should request all the etags in the entire addressbook again:

REPORT /addressbooks/johndoe/contacts/ HTTP/1.1
Depth: 1
Content-Type: application/xml; charset=utf-8

<card:addressbook-query xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
    <d:prop>
        <d:getetag />
    </d:prop>
</card:addressbook-query>

Note that this last request is extremely similar to a previous one, but we are only asking for the etag, not the address-data.

The reason for this, is that addressbooks can be rather huge. It will save a TON of bandwidth to only check the etag first.

HTTP/1.1 207 Multi-status
Content-Type: application/xml; charset=utf-8

<d:multistatus xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
    <d:response>
        <d:href>/addressbooks/johndoe/contacts/abc-def-fez-123454657.vcf</d:href>
        <d:propstat>
            <d:prop>
                <d:getetag>"2134-888"</d:getetag>
            </d:prop>
            <d:status>HTTP/1.1 200 OK</d:status>
        </d:propstat>
    </d:response>
    <d:response>
        <d:href>/addressbooks/johndoe/contacts/acme-12345.vcf</d:href>
        <d:propstat>
            <d:prop>
                <d:getetag>"9999-2344""</d:getetag>
            </d:prop>
            <d:status>HTTP/1.1 200 OK</d:status>
        </d:propstat>
    </d:response>
</d:multistatus>

Judging from this last request, 3 things have changed:

So based on those 3 points, we know that we need to remove a contact from the local addressbook, and fetch the vCards for both the new item, and the updated one.

To fetch the data for these, you can simply issue GET requests:

GET /addressbooks/johndoe/contacts/abc-def-fez-123454657.vcf

But that does not scale up well, in case a few hundred contacts have changed. It's better to batch the GET's together with multiget.

REPORT /addressbooks/johndoe/contacts/ HTTP/1.1
Depth: 1
Content-Type: application/xml; charset=utf-8

<card:addressbook-multiget xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
    <d:prop>
        <d:getetag />
        <card:address-data />
    </d:prop>
    <d:href>/addressbooks/johndoe/contacts/abc-def-fez-123454657.vcf</d:href>
    <d:href>/addressbooks/johndoe/contacts/acme-12345.vcf</d:href>
</card:addressbook-multiget>

This request will simply return a multi-status again with the address-data and etag.

A small note about writing code for this.

If you read this far and understood what's been said, you may have realized that it's a bit cumbersome to have a separate step for the initial sync, and subsequent updates.

It would totally be possible to skip the 'initial sync', and just use addressbook-query and addressbook-multiget REPORTS for the initial sync as well.

Updating a contact

Updating a vCard is rather simple:

PUT /addressbook/johndoe/contacts/some-contact.vcf HTTP/1.1
Content-Type: text/vcard; charset=utf-8
If-Match: "2134-314"

BEGIN:VCARD
....
END:VCARD

A response to this will be something like this:

HTTP/1.1 204 No Content
ETag: "2134-315"

The update gave us back the new ETag. SabreDAV returns this ETag on updates most of the time, but not always.

There are cases where the CardDAV server must modify the vCard immediately after receiving it, for various reasons. In those situations an ETag will not be returned, and you should ideally issue a GET request immediately after to figure out how the server changed the contact.

Many clients skip the GET step though.

A few notes:

Don't change the UID

The UID and the url of the object are important to not change. Changing either will highly confuse other clients and the server should reject those changes (although they don't always).

Creating a contact

Creating a contact is almost identical, except that you (as a client) are responsible for determining the url of the object, and UID.

PUT /addressbooks/johndoe/contacts/somerandomstring.vcf HTTP/1.1
Content-Type: text/vcard; charset=utf-8

BEGIN:VCARD
VERSION:3.0
UID:some-other-random-string
....
END:VCARD

A response to this will be something like this:

HTTP/1.1 201 Created
ETag: "21345-324"

Similar to updating, an ETag is often returned, but there are cases where this is not true.

Deleting a contact

Deleting is simple enough:

DELETE /addressbooks/johndoe/contacts/132456762153245.vcf HTTP/1.1
If-Match: "2134-314"

Speeding up Sync with WebDAV-Sync

WebDAV-Sync is a protocol extension that is defined in rfc6578. Because this extension was defined later, some servers may not support this yet.

SabreDAV supports this since 2.0.

WebDAV-Sync allows a client to ask just for address books that have changed. The process on a high-level is as follows:

  1. Client requests sync-token from server.
  2. Server reports token 15.
  3. Some time passes.
  4. Client does a Sync REPORT on an addressbook, and supplied token 15.
  5. Server returns vcard urls that have changed or have been deleted and returns token 17.

As you can see, after the initial sync, only items that have been created, modified or deleted will ever be sent.

This has a lot of advantages. The transmitted xml bodies can generally be a lot shorter, and is also easier on both client and server in terms of memory and CPU usage, because only a limited set of items will have to be compared.

It's important to note, that a client should only do Sync operations, if the server reports that it has support for it. The quickest way to do so, is to request {DAV}sync-token on the addressbook you wish to sync.

Technically, a server may support 'sync' on one addressbook, and it may not support it on another, although this is probably rare.

Getting the first sync-token

Initially, we just request a sync token when asking for address book information:

PROPFIND /addressbooks/johndoe/contacts/ HTTP/1.1
Depth: 0
Content-Type: application/xml; charset=utf-8

<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/">
  <d:prop>
     <d:displayname />
     <cs:getctag />
     <d:sync-token />
  </d:prop>
</d:propfind>

This would return something as follows:

<d:multistatus xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/">
    <d:response>
        <d:href>/addressbooks/johndoe/contacts/</d:href>
        <d:propstat>
            <d:prop>
                <d:displayname>My Address Book</d:displayname>
                <cs:getctag>3145</cs:getctag>
                <d:sync-token>http://sabredav.org/ns/sync-token/3145</d:sync-token>
            </d:prop>
            <d:status>HTTP/1.1 200 OK</d:status>
        </d:propstat>
    </d:response>
</d:multistatus>

As you can see, the sync-token is a url. It always should be a url. Even though a number appears in the url, you are not allowed to attach any meaning to that url. Some servers may have use an increasing number, another server may use a completely random string.

Receiving changes

After a sync token has been obtained, and the client already has the initial copy of the addressbook, the client is able to request all changes since the token was issued.

This is done with a REPORT request that may look like this:

REPORT /addressbooks/johndoe/contacts/ HTTP/1.1
Host: dav.example.org
Content-Type: application/xml; charset="utf-8"

<?xml version="1.0" encoding="utf-8" ?>
<d:sync-collection xmlns:d="DAV:">
  <d:sync-token>http://sabredav.org/ns/sync/3145</d:sync-token>
  <d:sync-level>1</d:sync-level>
  <d:prop>
    <d:getetag/>
  </d:prop>
</d:sync-collection>

This requests all the changes since sync-token identified by http://sabredav.org/ns/sync/3145, and for the contacts that have been added or modified, we're requesting the etag.

The response to a query like this is another multistatus xml body. Example:

HTTP/1.1 207 Multi-Status
Content-Type: application/xml; charset="utf-8"

<?xml version="1.0" encoding="utf-8" ?>
<d:multistatus xmlns:d="DAV:">
    <d:response>
        <d:href>/addressbooks/johndoe/contacts/newcard.vcf</d:href>
        <d:propstat>
            <d:prop>
                <d:getetag>"33441-34321"</d:getetag>
            </d:prop>
            <d:status>HTTP/1.1 200 OK</d:status>
        </d:propstat>
    </d:response>
    <d:response>
        <d:href>/addressbooks/johndoe/contacts/updatedcard.vcf</d:href>
        <d:propstat>
            <d:prop>
                <d:getetag>"33541-34696"</d:getetag>
            </d:prop>
            <d:status>HTTP/1.1 200 OK</d:status>
        </d:propstat>
    </d:response>
    <d:response>
        <d:href>/addressbooks/johndoe/contacts/deletedcard.vcf</d:href>
        <d:status>HTTP/1.1 404 Not Found</d:status>
    </d:response>
    <d:sync-token>http://sabredav.org/ns/sync/5001</d:sync-token>
 </d:multistatus>

The last response reported two changes: newcard.vcf and updatedcard.vcf. There's no way to tell from the response whether those cards got created or updated, you, as a client can only infer this based on the vcards you are already aware of.

The entry with name deletedvcard.vcf got deleted as indicated by the 404 status. Note that the status element is here a child of d:response when in all previous examples it has been a child of d:propstat.

The other difference with the other multi-status examples, is that this one has a sync-token element with the latest sync-token.

Caveats

Note that a server is free to 'forget' any sync-tokens that have been previously issued. In this case it may be needed to do a full-sync again.

In case the supplied sync-token is not recognized by the server, a HTTP error is emitted. SabreDAV emits a 403.

Discovery

Ideally you will want to make sure that all the addressbooks in an account are automatically discovered. The best user interface would be to just have to ask for three items:

And the server should be as short as possible. This is possible with most servers.

If, for example a user specified 'dav.example.org' for the server, the first thing you should do is attempt to send a PROPFIND request to https://dav.example.org/. Note that you should try the https url before the http url.

This PROPFIND request looks as follows:

PROPFIND / HTTP/1.1
Depth: 0
Content-Type: application/xml; charset=utf-8

<d:propfind xmlns:d="DAV:">
  <d:prop>
     <d:current-user-principal />
  </d:prop>
</d:propfind>

This will return a response such as the following:

HTTP/1.1 207 Multi-status
Content-Type: application/xml; charset=utf-8

<d:multistatus xmlns:d="DAV:">
    <d:response>
        <d:href>/</d:href>
        <d:propstat>
            <d:prop>
                <d:current-user-principal>
                    <d:href>/principals/users/johndoe/</d:href>
                </d:current-user-principal>
            </d:prop>
            <d:status>HTTP/1.1 200 OK</d:status>
        </d:propstat>
    </d:response>
</d:multistatus>

A 'principal' is a user. The url that's being returned, is a url that refers to the current user. On this url you can request additional information about the user.

What we need from this url, is their 'addressbook home'. The addressbook home is a collection that contains all of the users' addressbooks.

To request that, issue the following request:

PROPFIND /principals/users/johndoe/ HTTP/1.1
Depth: 0
Content-Type: application/xml; charset=utf-8

<d:propfind xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
  <d:prop>
     <card:addressbook-home-set />
  </d:prop>
</d:propfind>

This will return a response such as the following:

HTTP/1.1 207 Multi-status
Content-Type: application/xml; charset=utf-8

<d:multistatus xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
    <d:response>
        <d:href>/</d:href>
        <d:propstat>
            <d:prop>
                <c:addressbook-home-set>
                    <d:href>/addressbooks/johndoe/</d:href>
                </c:addressbook-home-set>
            </d:prop>
            <d:status>HTTP/1.1 200 OK</d:status>
        </d:propstat>
    </d:response>
</d:multistatus>

Lastly, to list all the addressbooks for the user, issue a PROPFIND request with Depth: 1.

PROPFIND /addressbooks/johndoe/ HTTP/1.1
Depth: 1
Content-Type: application/xml; charset=utf-8

<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/">
  <d:prop>
     <d:resourcetype />
     <d:displayname />
     <cs:getctag />
  </d:prop>
</d:propfind>

In that last request, we asked for 3 properties.

The resourcetype tells us what type of object we're getting back. You must read out the resourcetype and ensure that it contains at least an addressbook element in the CardDAV namespace. Other items may be returned, including non-addressbooks, which your application should ignore.

Advanced discovery topics

Read the Service Discovery documentation