# Extract contacts from email threads

Source: https://developer.nylas.com/docs/cookbook/contacts/extract-contacts-from-email/

Every reply in a thread carries contact data your CRM is missing: the sender's full name, their work address, sometimes a phone number and title buried in a signature block. Sales and support teams retype that information by hand, and half of it never makes it into the system of record. The data is already sitting in the mailbox you connected. You just need a reliable way to pull it out.

This guide shows how to read a thread, collect every participant, parse the signature on the latest message, and write a clean contact record back through one API that works the same on Gmail and Outlook.

## How do I use an API to extract contact information from an email thread?

To extract contact information from an email thread, call `GET /v3/grants/{grant_id}/threads/{thread_id}` to get the `participants` array (each entry has a `name` and `email`), then fetch the latest message body and parse the signature block for a title, company, and phone number. The thread endpoint returns this in one identical shape across providers.

A thread is the right starting point because it groups every message in the conversation and exposes a deduplicated `participants` list plus a `message_ids` array. The request below reads a single thread by ID. Use it when you want the full cast of a conversation without paging through messages one by one. A thread can hold dozens of messages, but `participants` collapses them into one list of distinct people.

```bash
curl --request GET \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/threads/<THREAD_ID>' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

```python [extractThread-Python SDK]
from nylas import Client

nylas = Client(api_key="<NYLAS_API_KEY>")

thread = nylas.threads.find(
    identifier="<NYLAS_GRANT_ID>",
    thread_id="<THREAD_ID>",
)

for person in thread.data.participants:
    print(person.name, person.email)
```

The response gives you a `subject`, a `participants` array, and `message_ids`. The participants list is your first pass at who belongs in the CRM. Names here come from the email envelope, so they're accurate but often incomplete: you'll get "J. Rivera" where the signature says "Jordan Rivera, VP of Sales." That's why the next step reads the message body.

## How do I parse contact details from an email signature?

Fetch the latest message with `GET /v3/grants/{grant_id}/messages/{message_id}`, then run the `body` through a signature parser to pull a job title, company, and phone number. Nylas returns the message body verbatim, so you control the parsing. A signature usually lives in the last 6 to 10 lines, set off by a delimiter like `--` or a name on its own line.

Signature parsing is heuristic, not exact. The most reliable signals are the email address (you already have it from the envelope), a phone pattern, and a title line that sits next to a known company name. Pull the message ID from the thread's `message_ids` array, request that one message, and feed `message.body` to your parser. The body is full HTML or plain text, so strip tags before matching.

```bash
curl --request GET \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/<MESSAGE_ID>' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

```python [extractMessage-Python SDK]

from nylas import Client

nylas = Client(api_key="<NYLAS_API_KEY>")

message = nylas.messages.find(
    identifier="<NYLAS_GRANT_ID>",
    message_id="<MESSAGE_ID>",
)

sender = message.data.from_[0]
sender_name = sender.get("name") or ""
phone = re.search(r"\+?\d[\d\s().-]{7,}\d", message.data.body or "")

contact = {
    "given_name": sender_name.split(" ")[0],
    "surname": " ".join(sender_name.split(" ")[1:]),
    "emails": [{"email": sender.get("email"), "type": "work"}],
    "phone_numbers": [{"number": phone.group(), "type": "work"}] if phone else [],
}
print(contact)
```

Don't trust a parsed title or company blindly. A 2 to 5 percent error rate on free-text signatures is normal, so flag low-confidence fields for human review instead of overwriting good CRM data. For an agent-driven approach that resolves company and role from the domain, see [enrich contacts from email signatures](/docs/cookbook/agents/signature-enrichment/).

## How do I write extracted contacts into a CRM?

Send a `POST /v3/grants/{grant_id}/contacts` with the fields you parsed: `given_name` is required, and `emails`, `company_name`, `job_title`, `phone_numbers`, and `notes` are optional. The contact lands in the connected address book and, on Google and Microsoft, syncs back to the provider's native contacts within a few minutes.

This is the write step that closes the loop. The request below creates one contact from parsed thread data. Use it after you've collected the name, address, and signature fields, and after you've deduplicated against existing records. Only `given_name` is required by the schema, so you can write a partial record and enrich it later. Adding a `notes` field with the thread subject gives your team context on where the contact came from.

```bash
curl --request POST \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/contacts' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "given_name": "Jordan",
    "surname": "Rivera",
    "company_name": "Acme Inc",
    "job_title": "VP of Sales",
    "emails": [{ "email": "jordan@acme.com", "type": "work" }],
    "phone_numbers": [{ "number": "+1 555 010 7788", "type": "work" }],
    "notes": "Extracted from thread: Q3 renewal discussion"
  }'
```

```python [createContact-Python SDK]
from nylas import Client

nylas = Client(api_key="<NYLAS_API_KEY>")

contact = nylas.contacts.create(
    identifier="<NYLAS_GRANT_ID>",
    request_body={
        "given_name": "Jordan",
        "surname": "Rivera",
        "company_name": "Acme Inc",
        "job_title": "VP of Sales",
        "emails": [{"email": "jordan@acme.com", "type": "work"}],
        "phone_numbers": [{"number": "+1 555 010 7788", "type": "work"}],
        "notes": "Extracted from thread: Q3 renewal discussion",
    },
)
print(contact.data.id)
```

If you mirror to an external CRM like Salesforce or HubSpot rather than the mailbox address book, treat the API as the read-and-parse layer and push the parsed object to your CRM's own create endpoint. The [sync Gmail contacts to a CRM](/docs/cookbook/contacts/sync-google-contacts-crm/) guide covers the full two-way flow.


> **Info:** 
> **New to Nylas?** Start with the [quickstart guide](/docs/v3/getting-started/) to set up your app and connect a test account before continuing here.


## How do I build a contact sync feature across Gmail and Outlook?

Build it once against one schema. The same `threads`, `messages`, and `contacts` endpoints return identical JSON whether the grant is Google, Microsoft, or IMAP, so a single code path covers both Gmail and Outlook. You connect each account through one OAuth flow, store the returned `grant_id`, and the provider differences disappear behind it.

The alternative is two integrations that never quite match. The table below shows what cross-provider contact extraction costs each way. The unified column collapses the Gmail API and Microsoft Graph into one set of calls, so adding Yahoo or iCloud later means zero new parsing code.

| Task | Provider APIs directly | Nylas unified API |
| --- | --- | --- |
| **Read a thread** | `users.threads.get` (Gmail) and `GET /me/messages` (Graph) | `GET /v3/grants/{grant_id}/threads/{thread_id}` |
| **Get participants** | Parse `From`/`To`/`Cc` headers per provider | `participants` array, same shape everywhere |
| **Write a contact** | `people.createContact` and `POST /me/contacts` | `POST /v3/grants/{grant_id}/contacts` |
| **Auth** | 2 OAuth projects, 2 consent flows | 1 connector per provider, 1 grant per account |
| **Add a 3rd provider** | A new integration | Reuse the same code path |

One honest tradeoff: if you only ever support Gmail and want raw control over Google's People API fields, calling [Google's People API](https://developers.google.com/people) directly skips the layer. The moment a second provider appears, the unified path wins on maintenance.

## How do I avoid duplicate contacts when extracting at scale?

Deduplicate on the email address before every write, because the address is the one field signature parsing can't get wrong. List existing contacts with `GET /v3/grants/{grant_id}/contacts` and filter by the `email` query parameter, or pull contacts the API already harvested from messages by passing `source=inbox`. Match on a normalized, lowercased address.

The contacts list endpoint accepts a `source` filter that distinguishes `address_book` entries from `inbox` ones the system extracted automatically, so you can audit what already exists before adding more. At thread-extraction scale, a single busy mailbox can surface hundreds of distinct addresses, and writing each one blindly creates a duplicate storm. Check first, then create only the addresses you don't already hold. For the read-side filtering options, see [list and sync contacts](/docs/cookbook/contacts/list-and-sync-contacts/).

## What's next

- [Enrich contacts from email signatures](/docs/cookbook/agents/signature-enrichment/) for an agent that resolves company and role from the signature
- [Sync Gmail contacts to a CRM](/docs/cookbook/contacts/sync-google-contacts-crm/) for the full two-way CRM flow
- [List and sync contacts](/docs/cookbook/contacts/list-and-sync-contacts/) for source filtering and deduplication queries
- [Getting started with Nylas](/docs/v3/getting-started/) to create a project, connector, and your first grant