Guide
Contacts API: Google vs Microsoft vs CardDAV
You want to read and write contacts. Google replaced its Contacts API with the People API in 2019. Microsoft requires Azure AD for Graph. CardDAV is the open standard used by iCloud, Fastmail, and any self-hosted server. This guide compares all three on authentication, CRUD operations, search, delta sync, rate limits, and batch support — with a decision matrix and a unified CLI approach that handles all three with one command set.
Written by Caleb Geene Director, Site Reliability Engineering
CLI commands used in this guide: nylas contacts list, nylas contacts search, nylas contacts create, nylas contacts sync, and nylas contacts groups list.
What are the three contacts API options?
Contacts data is split across three incompatible API surfaces. The Google People API is Google's REST interface for Gmail contacts, replacing the deprecated Contacts API as of March 2019. The Microsoft Graph Contacts API is the only supported way to access Outlook contacts after Basic Auth deprecation in October 2022. CardDAV (RFC 6352) is the IETF open standard used by iCloud, Fastmail, and any self-hosted address book server.
The difference isn't just syntax. Each API uses a different data model, authentication flow, sync mechanism, and rate limit policy. A working Google People API integration won't read a single Outlook contact, and CardDAV XML responses require completely different parsing logic from both REST APIs. If your product serves users across multiple providers, you end up maintaining 3 separate codepaths, 3 OAuth flows, and 3 error-handling strategies.
The table below compares all three across the dimensions that matter when building a contacts integration.
| Feature | Google People API | Microsoft Graph | CardDAV (RFC 6352) | Nylas CLI |
|---|---|---|---|---|
| Auth method | OAuth 2.0 (Google Cloud Console) | OAuth 2.0 (Azure AD / Entra ID) | HTTP Basic or OAuth 2.0 (provider-specific) | API key + OAuth grant via auth login |
| Data format | JSON (structured fields) | JSON (OData) | XML (vCard 3.0/4.0) | Normalized JSON across all providers |
| CRUD | Full CRUD via REST | Full CRUD via REST | Full CRUD via HTTP (GET/PUT/DELETE) | Full CRUD via single CLI commands |
| Search | searchContacts endpoint (limited fields) | $search and $filter OData params | REPORT with XML filter (server-side search) | nylas contacts search --query |
| Groups / labels | Contact groups (up to 500 per account) | Categories (up to 250 per account) | Multiple address books via collections | nylas contacts groups list |
| Delta / sync | syncToken on connections.list (7-day expiry, unreliable deletes) | Delta queries via /contacts/delta | CTag / ETag for change detection | nylas contacts sync |
| Rate limits | 500 requests/day free tier; quota-based | 10,000 req/10 min per mailbox (default, varies by tenant) | Provider-specific (no RFC standard) | Nylas API limits apply |
| Batch operations | batchCreateContacts / batchUpdateContacts | JSON batching (up to 20 requests) | None (one request per vCard) | Handled server-side |
| Provider lock-in | Google only | Microsoft only | iCloud, Fastmail, self-hosted | All providers via one API |
How does the Google People API work?
The Google People API is Google's REST interface for contacts, replacing the deprecated Google Contacts API as of March 2019. It covers Gmail's 1.8 billion active users. The API returns structured JSON fields for name, email, phone, address, organization, and custom data. According to the People API reference, a single people.connections.list call returns up to 1,000 contacts per page.
Authentication uses OAuth 2.0 via Google Cloud Console. You request scopes like contacts.readonly or contacts (read-write), redirect users through a consent screen, and exchange the authorization code for access and refresh tokens. Google enforces quota limits: the free tier allows 500 requests per day, and each people.connections.list call costs 1 quota unit.
The People API also returns "other contacts" — people you've emailed but haven't explicitly added to your address book. These are accessible via the separate otherContacts.list endpoint with the contacts.other.readonly scope.
Here's a curl example listing the first 10 contacts from a Gmail account. You need a valid OAuth access token from Google's token endpoint, the contacts.readonly scope, and the response includes each contact's resourceName (a stable identifier like people/c123456789), etag, and an array of personFields.
# List contacts (requires People API OAuth token with contacts.readonly scope)
curl -s "https://people.googleapis.com/v1/people/me/connections?personFields=names,emailAddresses,phoneNumbers&pageSize=10" \
-H "Authorization: Bearer $GOOGLE_ACCESS_TOKEN" | \
jq '.connections[] | {
name: .names[0].displayName,
email: .emailAddresses[0].value,
phone: .phoneNumbers[0].value
}'
# Search contacts by name or email
curl -s "https://people.googleapis.com/v1/people:searchContacts?query=alice&readMask=names,emailAddresses" \
-H "Authorization: Bearer $GOOGLE_ACCESS_TOKEN" | \
jq '.results[].person.emailAddresses[0].value'
# Create a new contact
curl -s -X POST "https://people.googleapis.com/v1/people:createContact" \
-H "Authorization: Bearer $GOOGLE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"names":[{"givenName":"Alice","familyName":"Chen"}],"emailAddresses":[{"value":"alice@example.com"}]}'The People API supports incremental sync via syncToken on the connections.list endpoint — store the nextSyncToken from each response and pass it on subsequent calls to get only changed contacts. The limitation is that tokens expire after 7 days and don't reliably surface deletes. For address books with thousands of contacts, a full re-sync every 24 hours is the recommended fallback.
How does Microsoft Graph handle contacts?
Microsoft Graph Contacts is the REST API for Outlook address books. It covers Microsoft 365's 400 million commercial seats, according to Microsoft's Q2 FY2025 earnings. The Graph Contact resource maps to the IPM.Contact item type in Exchange stores and returns OData JSON with fields like givenName, emailAddresses, businessPhones, and categories.
Authentication requires registering an app in Azure AD (now Entra ID), then using either delegated permissions (user signs in) or application permissions (daemon, no user present). Application permissions like Contacts.ReadWrite require tenant admin consent before your app can access any mailbox. The permissions reference lists 4 contacts scopes: Read, ReadWrite, ReadShared, and ReadWriteShared. Default rate limits are 10,000 requests per 10 minutes per application per mailbox (varies by tenant).
Graph's standout feature for contacts is the delta query endpoint. Unlike the Google People API, Graph tracks changes server-side. Store the @odata.deltaLink from your last sync and use it to fetch only contacts that changed, were created, or were deleted since that point. This cuts sync payload from the full address book to a handful of changed records for mailboxes that see few changes per hour.
The following example lists contacts with selected OData fields, then runs a delta query. Both calls return the same JSON structure, so your parsing code doesn't need to change between a full sync and an incremental one.
# List contacts with specific fields (OData $select)
curl -s "https://graph.microsoft.com/v1.0/me/contacts?\$select=displayName,emailAddresses,businessPhones,categories&\$top=25" \
-H "Authorization: Bearer $GRAPH_ACCESS_TOKEN" | \
jq '.value[] | {name: .displayName, email: .emailAddresses[0].address}'
# Filter contacts by company name (OData $filter)
curl -s "https://graph.microsoft.com/v1.0/me/contacts?\$filter=companyName eq 'Stripe'&\$select=displayName,emailAddresses" \
-H "Authorization: Bearer $GRAPH_ACCESS_TOKEN" | \
jq '.value[].displayName'
# Delta query: get only contacts changed since last sync
# Store the deltaLink from the previous response and use it here
curl -s "https://graph.microsoft.com/v1.0/me/contacts/delta" \
-H "Authorization: Bearer $GRAPH_ACCESS_TOKEN" | \
jq '{changed: (.value | length), deltaLink: ."@odata.deltaLink"}'Graph also supports JSON batching: up to 20 sub-requests in one HTTP POST to https://graph.microsoft.com/v1.0/$batch. Creating 100 contacts in bulk takes 5 batch requests instead of 100 individual ones. Each sub-request in the batch can have different HTTP methods, so you can mix reads and writes in a single round-trip.
How does CardDAV work for contacts?
CardDAV is the IETF open standard for contacts access, defined in RFC 6352 (published 2011) as an extension of WebDAV (RFC 4918). It's used by iCloud, Fastmail, Nextcloud, and any self-hosted server running Radicale or Baikal. Every iCloud account exposes its contacts over CardDAV. Unlike the Google and Microsoft APIs, CardDAV carries no cloud vendor dependency — the same client code works against iCloud, Fastmail, and a server running on your laptop.
CardDAV represents contacts as vCard files (RFC 6350), stored as resources in collections (address books). You issue HTTP REPORT requests with XML bodies to query contacts, PUT requests to create or update them, and DELETE requests to remove them. The wire format is XML wrapping base64-encoded or plain-text vCard properties. A typical vCard looks like this:
# Discover address book URL (CardDAV principal lookup)
curl -s -X PROPFIND "https://contacts.icloud.com/" \
-u "apple-id@icloud.com:app-specific-password" \
-H "Depth: 0" \
-H "Content-Type: application/xml" \
-d '<?xml version="1.0"?><d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:carddav"><d:prop><d:current-user-principal/></d:prop></d:propfind>'
# List all vCards in an address book (REPORT)
curl -s -X REPORT "https://contacts.icloud.com/123456789/carddavhome/card/" \
-u "apple-id@icloud.com:app-specific-password" \
-H "Depth: 1" \
-H "Content-Type: application/xml" \
-d '<?xml version="1.0"?><c:addressbook-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:carddav"><d:prop><d:getetag/><c:address-data/></d:prop></c:addressbook-query>'
# Create a new vCard (PUT)
curl -s -X PUT "https://contacts.icloud.com/123456789/carddavhome/card/alice-chen.vcf" \
-u "apple-id@icloud.com:app-specific-password" \
-H "Content-Type: text/vcard" \
-d "BEGIN:VCARD
VERSION:3.0
FN:Alice Chen
N:Chen;Alice;;;
EMAIL;type=INTERNET;type=WORK:alice@example.com
TEL;type=CELL:+1-555-123-4567
END:VCARD"CardDAV's change detection uses ETags and CTag values. Each vCard resource has an ETag that changes when the resource is modified. Each address book collection has a CTag that changes when any resource inside it changes. RFC 6352 defines no standard delta endpoint — the typical sync loop is: fetch the collection's CTag; if it changed, fetch all ETags in the collection; compare to your local ETags; fetch only the vCards whose ETags differ. This is fast for address books under 10,000 contacts. Beyond that, the ETag comparison step itself gets expensive.
iCloud CardDAV has a documented quirk: it requires an app-specific password rather than the main Apple ID password, and the server URL includes the account's Contacts dataset identifier (a 9-digit number). Fastmail's CardDAV is simpler — authenticate with your Fastmail app password at https://carddav.fastmail.com.
How does authentication differ across the three?
Authentication is where the three diverge most sharply for a developer starting a new integration. Each API requires a different OAuth tenant, different scope names, and a different admin approval path — and that wiring, not the contact data model, is what usually takes the longest to get working.
Google People API: Create a project in Google Cloud Console, enable the People API, configure the OAuth consent screen, and generate credentials. Apps requesting sensitive scopes (anything beyond basic profile) go through a verification review that can take several weeks for new apps. You request exactly the scopes you need: https://www.googleapis.com/auth/contacts.readonly for read access, https://www.googleapis.com/auth/contacts for write. Access tokens expire after 3,600 seconds (per Google's OAuth2 documentation) and require a refresh token exchange.
Microsoft Graph: Register an app in Entra ID (formerly Azure AD), configure API permissions for Contacts.ReadWrite, and set redirect URIs. Delegated flows (user signs in) work for single-user apps. Application flows require an Azure AD tenant admin to grant consent before the app can touch any mailbox. This blocks SaaS onboarding: if your app uses application permissions, enterprise customers can't self-serve — their IT admin must approve the app first.
CardDAV: iCloud uses HTTP Basic authentication with app-specific passwords (the main Apple ID password is rejected). Apple requires two-factor authentication on the account before you can generate app passwords. Fastmail accepts app passwords or OAuth 2.0 via their developer API. Self-hosted servers like Radicale support HTTP Basic. There's no cloud console to configure — you either have credentials or you don't.
If you support users on Gmail, Outlook, and iCloud, you maintain 3 separate auth flows, 3 token stores, and 3 refresh strategies. Adding Yahoo (IMAP/CardDAV) or Exchange on-premises (EWS) means a 4th and 5th codepath before you've written a single line of business logic.
How do search and contact groups work?
Search quality and group support differ substantially across the three APIs. The Google People API's searchContacts endpoint searches name and email only — you can't search by company or custom fields via a single API call. Microsoft Graph's $filter OData parameter supports full field-level filtering: by companyName, jobTitle, emailAddresses/any(e:e/address eq 'x'), or any other indexed field. CardDAV REPORT filtering is limited to what the server implements — most servers support addressbook-query filtering on vCard properties, but full-text search is not guaranteed.
Groups on Google: The People API exposes contact groups via the contactGroups resource. You can list groups, create new ones, and add members. Gmail supports up to 500 contact groups per account, according to the People API reference. System groups like "Starred in Android" are read-only. Membership is bidirectional — groups added via the API show up in the Gmail Contacts web UI immediately.
Groups on Graph: Outlook uses categories rather than groups. Categories are per-mailbox strings applied to contacts (and calendar events, email messages). The outlookCategory resource supports up to 250 categories per mailbox. Assigning a category to a contact is a PATCH request updating the categories array. Unlike Gmail groups, Outlook categories are not membership collections — you can't list all contacts with a given category in a single API call without a $filter query.
Groups on CardDAV: RFC 6352 defines "addressbook collections" as the grouping mechanism — each collection is a separate address book URL. Some CardDAV servers also support vCard KIND:group vCards that reference member UIDs, but support varies. iCloud implements group vCards; Fastmail does not. The safest portable approach for CardDAV groups is to use separate address book collections.
How do sync and rate limits compare?
Sync strategy and rate limits determine whether your integration can stay current in production. The Google People API lacks a true delta endpoint: according to the connections.list reference, syncToken fetches contacts modified since a point in time, but tokens expire after 7 days and don't reliably surface deletes. A full re-sync every 24 hours is the recommended fallback strategy for contacts integrations on Google.
Microsoft Graph's delta endpoint (/me/contacts/delta) is the most developer-friendly sync mechanism of the three. Store the @odata.deltaLink returned by the last call. On the next poll, pass it as the URL and receive only records that changed, were added, or got a @removed annotation indicating deletion. For a mailbox with 5,000 contacts that changes 3 per day, this cuts sync payload from 5,000 records to 3.
CardDAV sync uses ETags and CTag headers. The collection-level CTag changes when any resource in the address book changes. On each sync, you fetch the collection's CTag; if it matches your stored value, nothing changed and you skip the sync. If it changed, you fetch all resource URLs and their ETags in a single PROPFIND, then download only the vCards whose ETags differ from your local store. This is 2 requests to determine what changed, plus N requests for the changed records. For typical personal address books under 2,000 contacts, this typically completes quickly.
Rate limits in practice: Google's People API free tier caps at 500 requests per day — enough for development but not production at scale. Paid quota via Google Cloud raises this, but the cost scales with usage. Microsoft Graph defaults to 10,000 requests per 10 minutes per mailbox (varies by tenant), which is sufficient for most sync workflows. CardDAV providers set their own limits: iCloud throttles aggressively with undocumented limits and returns HTTP 503 with a Retry-After header when you exceed them; Fastmail is more permissive.
How does a unified CLI handle all three?
Nylas CLI routes contacts operations through a unified API that speaks Google People API, Microsoft Graph, and CardDAV under the hood. A single nylas contacts list command returns normalized JSON regardless of whether the connected account is Gmail, Outlook, or an iCloud address book. You don't write provider-specific parsing code or manage 3 separate OAuth flows.
Install Nylas CLI with Homebrew and authenticate with your API key. For other install methods (shell script, PowerShell, Go), see the getting started guide. The auth step stores credentials locally in ~/.config/nylas/ — tokens refresh automatically.
# Install Nylas CLI
brew install nylas/nylas-cli/nylas
# Authenticate with your Nylas API key
nylas auth config --api-key YOUR_API_KEY
# List contacts — works with Gmail, Outlook, or CardDAV providers
nylas contacts list --limit 25 --json
# Search by name, email, or company
nylas contacts search --query "stripe" --json
# Sync latest contacts from provider
nylas contacts sync
# List contact groups / address books
nylas contacts groups list --jsonThe CLI handles the protocol translation automatically. When the connected account is Gmail, nylas contacts search routes through the People API's searchContacts endpoint. When it's Outlook, the search uses Graph's $filter. When it's iCloud or Fastmail, it issues a CardDAV REPORT. You write one command. The routing happens server-side.
For creating contacts in bulk, loop through a CSV file and call nylas contacts create for each row. The CLI returns the new contact's ID on stdout, so you can capture IDs for follow-up operations. Rate limits vary by provider — add a brief delay between iterations for large batches over 500 records to avoid 429 errors.
# Create a contact
nylas contacts create --name "Alice Chen" --email alice@example.com
# Show full details for a specific contact
nylas contacts show --id CONTACT_ID --json
# Bulk create from CSV (shell loop)
while IFS=, read -r name email; do
nylas contacts create --name "$name" --email "$email"
sleep 0.25
done < contacts.csvSee the manage contacts from terminal guide for a full walkthrough of list, search, export, and group management commands. For enriching contacts with data from email signatures, see the contact enrichment guide.
When should you use each API?
The right contacts API depends on which providers your users have, your sync frequency requirements, and how much auth infrastructure you want to own. Four scenarios cover most contacts integration use cases, from single-provider consumer apps to multi-provider enterprise tools.
| Scenario | Best choice | Why |
|---|---|---|
| Gmail-only app with read/write contacts | Google People API | Native Google groups, other-contacts endpoint, deep Gmail integration |
| Microsoft 365 enterprise with frequent delta sync | Microsoft Graph | Delta queries, OData filtering, JSON batching, Azure AD SSO |
| iCloud, Fastmail, or self-hosted server | CardDAV | No Google or Microsoft dependency, works with any RFC 6352 server |
| Multi-provider contacts sync or export | Nylas CLI / unified API | One command set, one auth flow, normalized JSON across all providers |
| High-frequency sync (>1,000 contacts/hour) | Microsoft Graph | Delta queries and JSON batching handle volume; 10K requests/10 min |
Choose Google People API when your product is Gmail-specific and you need features like other-contacts (people you've emailed but not added) or tight Google Workspace integration. The 500 requests/day free tier limits how far you can go without paid quota, but it's enough for small SaaS products under a few hundred active users.
Choose Microsoft Graph when your users are on Microsoft 365 or Outlook.com and you need efficient sync. The delta endpoint is the most developer-friendly change-detection mechanism of the three — it eliminates full re-syncs for mailboxes that change slowly. Enterprise deployments benefit from Azure AD SSO and existing admin-consent workflows.
Choose CardDAV when you need vendor independence or your users are on iCloud, Fastmail, or a self-hosted server. There's no app registration, no cloud console, and no quota to manage beyond what the provider enforces. The tradeoff is XML parsing overhead and inconsistent group support across implementations.
Choose a unified API when you support more than one provider. Maintaining 3 separate OAuth flows, 3 data models, and 3 sync strategies multiplies your integration surface by 3x. For email API comparisons in the same vein, see the IMAP vs Gmail API vs Graph API guide — the auth and sync tradeoffs are nearly identical for email.
Next steps
Pick the contacts API that matches your provider mix and sync requirements. To work with contacts from the terminal without building API clients yourself, install the CLI and connect your account:
# Install
brew install nylas/nylas-cli/nylas
# Authenticate
nylas auth config --api-key YOUR_API_KEY
# List contacts from any connected provider
nylas contacts list --limit 10 --json- Full command reference — every contacts, email, and calendar command with all flags documented
- Manage contacts from terminal — list, search, create, export to CSV, and manage groups step by step
- Enrich contacts from email — auto-populate contact fields by parsing email signatures and headers
- Map communication patterns between orgs — build relationship graphs from contacts and email data
- IMAP vs Gmail API vs Graph API — the same comparison applied to email protocols
- Nylas developer docs — full API reference including contacts endpoints