# How to search contacts

Source: https://developer.nylas.com/docs/cookbook/contacts/search-contacts/

There's no free-text search endpoint for contacts. You can't pass a `q` or `search` term and get fuzzy matches back. The Contacts API filters by exact field values: an email address, a phone number, a group ID, or a source. Knowing that upfront saves you from hunting for a parameter that doesn't exist, so this page shows what the 7 list filters do and how to match names in your own code.

## Search contacts by email or phone number

Pass the `email` or `phone_number` query parameter on `GET /v3/grants/{grant_id}/contacts` to find a contact by one of their addresses. The `email` filter matches across all 6 providers; `phone_number` works on Google, IMAP, iCloud, Yahoo, and EWS but not Microsoft. Both match the stored value, so they're best when you already know the exact address.

The request below filters the address book to contacts holding the given email address. The response is the same `data` array you get from an unfiltered list, narrowed to matches.

```bash
curl --compressed --request GET \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/contacts?email=leyah.miller@example.com' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

Both SDKs accept the filters through `query_params`. The Node.js and Python calls below send a `phone_number` lookup, which returns every contact storing that number across its `phone_numbers` array.

```js [searchByEmail-Node.js]
const contacts = await nylas.contacts.list({
  identifier: "<NYLAS_GRANT_ID>",
  queryParams: { phoneNumber: "+1-555-555-5555" },
});
console.log(contacts.data);
```

```python
contacts = nylas.contacts.list(
    "<NYLAS_GRANT_ID>",
    query_params={"phone_number": "+1-555-555-5555"},
)
print(contacts.data)
```

## Filter contacts by group or source

Narrow results by where a contact lives using `group` and `source`. The `group` parameter takes a Contact Group ID and works on Google and Microsoft, not EWS. The `source` parameter accepts `address_book` (saved contacts, the default), `domain` (the organization directory), or `inbox` (people Nylas generates from messages). Combining the two scopes a query to one segment of the address book.

The request below returns saved contacts from a single group. Use `source=address_book` to skip the generated `inbox` entries so you only see people the user saved.

```bash
curl --compressed --request GET \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/contacts?group=friends&source=address_book' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

The SDK call mirrors that filter. For IMAP and iCloud you can pass a comma-separated `source` such as `address_book,inbox` to merge both segments in one request; the other 4 providers accept a single source value only.

```python
contacts = nylas.contacts.list(
    "<NYLAS_GRANT_ID>",
    query_params={"group": "friends", "source": "address_book"},
)
print(contacts.data)
```

## Find a contact by name

The API has no name-search parameter, so to find someone by name you list contacts and filter the `given_name` and `surname` fields in your own code. This is the honest tradeoff: name matching happens client-side, on whatever page of results you fetched. The default page size is 30 contacts and the maximum is 200, so a name search only sees the page you loaded unless you paginate through the rest.

The snippet below fetches one page and checks whether each name field contains the search term, case-insensitive. It returns matches from those 30 contacts, which is fine for small address books but misses anyone on a later page.

```js [findByName-Node.js]
const page = await nylas.contacts.list({
  identifier: "<NYLAS_GRANT_ID>",
  queryParams: { limit: 50 },
});

const term = "miller";
const matches = page.data.filter((c) => {
  const name = `${c.givenName ?? ""} ${c.surname ?? ""}`.toLowerCase();
  return name.includes(term);
});
console.log(matches);
```

The Python version does the same local filter. For an address book larger than 200 contacts, loop through every page first (see the pagination pattern below), then run the match over the combined set so no one gets skipped.

```python
page = nylas.contacts.list(
    "<NYLAS_GRANT_ID>",
    query_params={"limit": 50},
)

term = "miller"
matches = [
    c for c in page.data
    if term in f"{c.given_name or ''} {c.surname or ''}".lower()
]
print(matches)
```

## Things to know about searching contacts

Filtering is exact, not fuzzy. The `email`, `phone_number`, and `group` parameters match stored values, so partial typing won't surface partial matches the way a search box would. Real fuzzy search means listing contacts and ranking them yourself, which is why an autocomplete picker needs a local cache rather than a live call per keystroke. The 7 available filters are `email`, `phone_number`, `source`, `group`, `recurse`, `limit`, and `page_token`.

A few provider behaviors shape what's searchable. On Microsoft, the `recurse=true` flag pulls contacts from a group's subgroups, but the recursion goes only 1 level deep, so deeper nesting still needs separate calls. Google polls contacts every 5 minutes, so a contact added seconds ago may not appear in a filter result yet. EWS doesn't support `group` filtering or the `inbox` source at all, which limits how you segment Exchange address books.

For large address books, pagination isn't optional: the maximum page is 200 contacts, and a 5,000-contact directory takes 25 requests to read in full. If you're building autocomplete, cache the full contact list locally and filter in memory rather than calling the API on every keystroke. The [list and sync contacts guide](/docs/cookbook/contacts/list-and-sync-contacts/) covers the webhook-driven cache that keeps that local copy current. For the exact filter semantics per provider, see Microsoft's [list contacts reference](https://learn.microsoft.com/en-us/graph/api/user-list-contacts) and Google's [People API documentation](https://developers.google.com/people).

## Paginate through results

When a filter or name search spans more than one page, follow the cursor. Each list response includes a `next_cursor` value; pass it back as the `page_token` query parameter to fetch the next 200 contacts. Repeat until `next_cursor` is absent. This is the only way to search a full address book, since every filter still respects the page limit.

The request below fetches the second page using a cursor from a prior response. Loop this until no cursor comes back, accumulating contacts so your client-side name or field match runs over the complete set.

```bash
curl --compressed --request GET \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/contacts?limit=200&page_token=<NEXT_CURSOR>' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

## What's next

- [List and sync contacts](/docs/cookbook/contacts/list-and-sync-contacts/) for recipient autocomplete and webhook-driven caching
- [How to list Google contacts](/docs/cookbook/contacts/list-contacts-google/) for Google-specific scopes and quotas
- [Filter contacts with contact groups](/docs/cookbook/contacts/contact-groups/) for group IDs and membership
- [Manage contacts with the Contacts API](/docs/v3/email/contacts/) for the full field schema
- [Contacts API reference](/docs/reference/api/) for every endpoint and parameter