Google retired its standalone Contacts API in January 2022 and folded everything into the People API, so any code written against the old google.com/m8/feeds endpoints stopped working. The People API that replaced it splits a user’s address book across “connections” (saved contacts) and “other contacts” (auto-created from Gmail), each behind a different OAuth scope and verification tier. That split, plus Google’s app verification review, is where most teams get stuck.
This recipe shows how to pull Gmail and Google Workspace contacts through one call to the Nylas Contacts API, then filter and sync them. You get a single contact schema and one set of credentials instead of two People API resources and a GCP verification process.
Why use Nylas instead of the Google People API directly?
Section titled “Why use Nylas instead of the Google People API directly?”The People API asks for more setup than a contact picker usually warrants:
- Two resources, two scopes. Saved contacts come from
people.connections.listwith thecontactsscope; auto-created suggestions come fromotherContacts.listwith the separatecontacts.other.readonlyscope. The API merges them for you and exposes both through thesourcefield. - App verification. Both contact scopes are classified as sensitive, so a production Google app needs OAuth verification before users outside your test list can connect. The API handles token storage and refresh once a grant exists.
- Per-project quotas. The People API enforces a default 90 read requests per minute per user. Polling many users from your own GCP project can exhaust that fast, and the API smooths it with managed retries.
- One schema across providers. The same
GET /v3/grants/{grant_id}/contactsrequest reads a Gmail address book, an Outlook one, or Exchange, so your picker code doesn’t branch on provider.
Before you begin
Section titled “Before you begin”You’ll need a working project and an authorized account before any contacts come back. A grant represents one connected Google account, and the contacts you list always belong to that single grant. Set these up first:
- A Nylas application with a valid API key
- A grant for a Gmail or Google Workspace account, authorized with a contacts scope
- A GCP project with the Google OAuth scopes your integration needs
List Google contacts
Section titled “List Google contacts”Send a GET request to /v3/grants/{grant_id}/contacts with the grant ID for the connected Google account. The response is a data array of contact objects plus a request_id, and by default a single page returns up to 50 contacts. Every object carries the same fields (emails, phone_numbers, job_title, company_name, and groups) whether the account is Gmail or Google Workspace.
The samples below list contacts for one grant and print the unified response.
curl --compressed --request GET \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/contacts' \ --header 'Accept: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json'{ "request_id": "1", "data": [ { "birthday": "1960-12-31", "company_name": "Nylas", "emails": [ { "type": "work", }, { "type": "home", } ], "given_name": "Leyah", "grant_id": "<NYLAS_GRANT_ID>", "groups": [{ "id": "starred" }, { "id": "friends" }], "id": "<CONTACT_ID>", "im_addresses": [ { "type": "jabber", "im_address": "jabber_at_leyah" }, { "type": "msn", "im_address": "leyah.miller" } ], "job_title": "Software Engineer", "manager_name": "Nyla", "middle_name": "Allison", "nickname": "Allie", "notes": "Loves ramen", "object": "contact", "office_location": "123 Main Street", "phone_numbers": [ { "type": "work", "number": "+1-555-555-5555" }, { "type": "home", "number": "+1-555-555-5556" } ], "physical_addresses": [ { "type": "work", "street_address": "123 Main Street", "postal_code": "94107", "state": "CA", "country": "US", "city": "San Francisco" }, { "type": "home", "street_address": "123 Main Street", "postal_code": "94107", "state": "CA", "country": "US", "city": "San Francisco" } ], "picture_url": "https://example.com/picture.jpg", "source": "address_book", "surname": "Miller", "web_pages": [ { "type": "work", "url": "<WEBPAGE_URL>" }, { "type": "home", "url": "<WEBPAGE_URL>" } ] } ], "next_cursor": "2"}import Nylas from "nylas";
const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>", apiUri: "<NYLAS_API_URI>",});
async function fetchContacts() { try { const identifier = "<NYLAS_GRANT_ID>"; const contacts = await nylas.contacts.list({ identifier, queryParams: {}, });
console.log("Recent Contacts:", contacts); } catch (error) { console.error("Error fetching drafts:", error); }}
fetchContacts();from nylas import Client
nylas = Client( "<NYLAS_API_KEY>", "<NYLAS_API_URI>")
grant_id = "<NYLAS_GRANT_ID>"
contacts = nylas.contacts.list( grant_id,)
print(contacts)For create, update, and delete operations plus the complete field list, see Manage contacts with the Contacts API.
Filter Google contacts
Section titled “Filter Google contacts”Four query parameters narrow a Google contact list: email, phone_number, group, and source. The source parameter is the most useful one for Google, since it picks between saved contacts (address_book) and the auto-created suggestions Gmail builds from your sent mail (inbox). A fifth parameter, recurse, exists but is Microsoft-only, so it has no effect on Google grants. Combine these with limit to keep pages small.
The email filter matches any contact whose email address contains the value you pass, which makes it the right choice for an autocomplete box.
curl --request GET \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/contacts?source=inbox&email=jane&limit=10' \ --header 'Authorization: Bearer <NYLAS_API_KEY>'const contacts = await nylas.contacts.list({ identifier: grantId, queryParams: { source: "inbox", email: "jane", limit: 10, },});contacts = nylas.contacts.list( grant_id, query_params={ "source": "inbox", "email": "jane", "limit": 10, },)To list only the contacts inside one Google label, pass that label’s ID to the group parameter. Google contact groups map directly to the labels you create in Google Contacts, so group reads the same membership Gmail shows in its sidebar.
Things to know about Google contacts
Section titled “Things to know about Google contacts”Google’s contact model has a few quirks that shape how a list call behaves. The four below cover where contacts come from, how groups and photos work, which scope you need, and the quotas that throttle reads.
The source field maps to People API resources
Section titled “The source field maps to People API resources”Google exposes contacts through two People API resources, and the source field tells you which one a contact came from. address_book contacts are the saved entries from people.connections.list, the ones a user typed into Google Contacts. inbox contacts are the “other contacts” Gmail auto-creates from people you’ve emailed, returned by otherContacts.list. A heavy email user can have thousands of these auto-created entries against a few hundred saved ones, so filter on source=address_book when you only want real, saved contacts.
Contact groups are Google labels
Section titled “Contact groups are Google labels”The group query parameter filters by Google’s contact labels, which the People API calls contact groups. Each label has a stable resource ID, and the groups array on every contact object lists the labels that contact belongs to. System groups like myContacts and starred are always present, while user-created labels get their own IDs. Use the List contact groups endpoint to fetch the IDs before filtering, because passing a display name instead of an ID returns an empty list.
Profile photos need a separate request
Section titled “Profile photos need a separate request”The list response doesn’t embed photo bytes; instead each contact carries a picture_url you fetch separately when you need the image. Google stores high-resolution profile photos, often 512 by 512 pixels, so loading them inline for a long list is wasteful. Request the photo lazily as a contact scrolls into view, and cache it, rather than pulling every image up front.
Scopes and rate limits
Section titled “Scopes and rate limits”Reading saved contacts needs the contacts or contacts.readonly scope, and the inbox source additionally needs contacts.other.readonly. Both are sensitive scopes, so a production app must pass Google’s OAuth verification. On quotas, the People API defaults to 90 read requests per minute per user, and your GCP project carries an overall daily ceiling on top of that. The API retries throttled requests, but if you sync many accounts on a schedule, stagger the jobs rather than firing them at once. See Google’s People API documentation for the current quota figures and scope definitions.
What’s next
Section titled “What’s next”- List and sync contacts from Google and Outlook for autocomplete and webhook sync
- Filter contacts by group to read membership of a Google label
- Contacts API reference for every endpoint, parameter, and field
- Connect user accounts with OAuth to authorize contact access for a grant
- Google provider guide for OAuth scopes, verification, and Workspace setup