# Connect Intercom to email with Nylas

Source: https://developer.nylas.com/docs/cookbook/use-cases/sync/sync-intercom-email/

Intercom is built around live chat, so email that lands in a regular mailbox never becomes an Intercom conversation on its own. If a customer replies to a transactional email or writes to `support@`, that message sits in a provider inbox where your agents can't see it next to their chats. This recipe closes that gap: watch a mailbox with Nylas, create an Intercom conversation for each inbound email, and reply from the same mailbox so the whole thread stays in one place.

The model matters here. Intercom organizes everything around **contacts** and **conversations**, not tickets, so an inbound email maps to a contact plus a conversation rather than a support ticket. That's a different shape than a Zendesk or help-desk flow.

## Why route email into Intercom with Nylas?

The Nylas Email API gives you one OAuth connection and one webhook stream across Google, Microsoft, Yahoo, iCloud, IMAP, and Exchange (EWS) mailboxes, so you don't build six provider integrations to feed one Intercom workspace. Intercom's own inbound email features assume you forward to an Intercom address. This approach keeps your existing mailbox.

- **One mailbox, real-time.** A single `message.created` webhook fires across all 6 providers, so new mail reaches Intercom in seconds instead of on a polling interval.
- **Contact enrichment from the email itself.** The sender's name, address, and signature come straight off the message, so you can populate the Intercom contact before the conversation opens.
- **Reply from the real address.** Send replies through the same mailbox with the Nylas send endpoint, so customers see your support address, not an Intercom relay.
- **Conversations, not tickets.** This keeps the integration native to Intercom's data model instead of bolting a ticket abstraction on top.

## Before you begin

You need a Nylas application with at least one connected mailbox (the address agents already watch, like `support@`) and an Intercom workspace with an access token. Two prerequisites are specific to this integration:

- A Nylas grant for the support mailbox. The grant ID is the `grant_id` in every request below.
- An Intercom access token with read and write scope for contacts and conversations. Intercom tokens are workspace-scoped, so one token covers the whole workspace.


> **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.


### Get an Intercom access token

Intercom issues access tokens from the [Developer Hub](https://developers.intercom.com/docs/build-an-integration/learn-more/authentication/). For an internal integration, create an app in your own workspace and copy the token. You pass it as a bearer token on every Intercom request, alongside the `Intercom-Version` header that pins the API version so a future Intercom release doesn't change your payloads.

## List recent messages

Start by reading what's in the mailbox. The `GET /v3/grants/{grant_id}/messages` endpoint returns up to 50 messages per page by default (200 maximum), with one unified schema across all 6 providers. Use it for the initial backfill, then switch to webhooks for live traffic. The example reads the 5 most recent messages from the connected grant.

````bash [listMessages-Request]

curl --compressed --request GET \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages?limit=5" \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --header 'Content-Type: application/json'

````

````json [listMessages-Response]

{
  "request_id": "d0c951b9-61db-4daa-ab19-cd44afeeabac",
  "data": [
    {
      "starred": false,
      "unread": true,
      "folders": ["UNREAD", "CATEGORY_PERSONAL", "INBOX"],
      "grant_id": "1",
      "date": 1706811644,
      "attachments": [
        {
          "id": "1",
          "grant_id": "1",
          "filename": "invite.ics",
          "size": 2504,
          "content_type": "text/calendar; charset=\"UTF-8\"; method=REQUEST"
        },
        {
          "id": "2",
          "grant_id": "1",
          "filename": "invite.ics",
          "size": 2504,
          "content_type": "application/ics; name=\"invite.ics\"",
          "is_inline": false,
          "content_disposition": "attachment; filename=\"invite.ics\""
        }
      ],
      "from": [
        {
          "name": "Nylas DevRel",
          "email": "nylasdev@nylas.com"
        }
      ],
      "id": "1",
      "object": "message",
      "snippet": "Send Email with Nylas APIs",
      "subject": "Learn how to Send Email with Nylas APIs",
      "thread_id": "1",
      "to": [
        {
          "name": "Nyla",
          "email": "nyla@nylas.com"
        }
      ],
      "created_at": 1706811644,
      "body": "Learn how to send emails using the Nylas APIs!"
    }
  ],
  "next_cursor": "123"
}


````

````js [listMessages-Node.js SDK]

import Nylas from "nylas";

const nylas = new Nylas({
  apiKey: "<NYLAS_API_KEY>",
  apiUri: "<NYLAS_API_URI>",
});

async function fetchRecentEmails() {
  try {
    const messages = await nylas.messages.list({
      identifier: "<NYLAS_GRANT_ID>",
      queryParams: {
        limit: 5,
      },
    });

    console.log("Messages:", messages);
  } catch (error) {
    console.error("Error fetching emails:", error);
  }
}

fetchRecentEmails();


````

````python [listMessages-Python SDK]

from nylas import Client

nylas = Client(
    "<NYLAS_API_KEY>",
    "<NYLAS_API_URI>"
)

grant_id = "<NYLAS_GRANT_ID>"

messages = nylas.messages.list(
  grant_id,
  query_params={
    "limit": 5
  }
)

print(messages)

````

Each message carries `from`, `subject`, `body`, and `date`. Those four fields are everything you need to find the contact and open a conversation.

## Catch new email with a webhook

Polling burns API calls and adds lag, so for live traffic subscribe to the `message.created` trigger instead. Nylas posts a JSON payload to your endpoint within seconds of a message arriving, across all 6 providers. The payload includes the `grant_id` and the message `id`, which you fetch and then hand to Intercom.

The handler below verifies nothing fancy: it reads the message ID off the webhook, pulls the full message, and routes it into Intercom. In production, verify the webhook signature first (see the link at the end of this section).

```python
from flask import Flask, request


app = Flask(__name__)
NYLAS_API = "https://api.us.nylas.com"
NYLAS_KEY = "<NYLAS_API_KEY>"

@app.post("/webhooks/nylas")
def on_message_created():
    event = request.get_json()
    data = event["data"]["object"]
    grant_id, message_id = data["grant_id"], data["id"]

    msg = requests.get(
        f"{NYLAS_API}/v3/grants/{grant_id}/messages/{message_id}",
        headers={"Authorization": f"Bearer {NYLAS_KEY}"},
    ).json()["data"]

    route_to_intercom(msg)          # defined in the next two sections
    return "", 200
```

The `message.created` trigger fires for every new message in the mailbox, including ones your own agents send. Filter on `from` so replies your team sends back through Nylas don't loop into a second conversation. For the full subscription and verification flow, see [Receive real-time email with webhooks](/docs/cookbook/use-cases/build/realtime-webhooks/) and [Monitor an inbox for support tickets](/docs/cookbook/use-cases/ingest/monitor-inbox-support-tickets/).

## Find or create the Intercom contact

Every Intercom conversation must belong to a contact, so resolve the sender first. Intercom's Contacts API exposes `POST /contacts/search` to look up a contact by email and `POST /contacts` to create one. That's 2 requests per new sender. Search first to avoid duplicates, because Intercom doesn't dedupe by email automatically on create, so two creates for the same address produce two contacts.

The function below searches by the sender's address and creates the contact only when the search returns 0 results. The sender's display name comes straight off the Nylas message.

```python
INTERCOM = "https://api.intercom.io"
HEADERS = {
    "Authorization": "Bearer <INTERCOM_ACCESS_TOKEN>",
    "Intercom-Version": "2.11",
    "Content-Type": "application/json",
}

def find_or_create_contact(sender):
    query = {"query": {"field": "email", "operator": "=", "value": sender["email"]}}
    found = requests.post(f"{INTERCOM}/contacts/search", json=query, headers=HEADERS).json()
    if found.get("total_count", 0) > 0:
        return found["data"][0]["id"]

    created = requests.post(
        f"{INTERCOM}/contacts",
        json={"role": "user", "email": sender["email"], "name": sender.get("name", "")},
        headers=HEADERS,
    ).json()
    return created["id"]
```

This is the enrichment seam. The Nylas message already gives you the name and address; if you parse the signature for a title or phone, pass those as `custom_attributes` on the create call so the Intercom contact is populated before the first reply.

## Create the Intercom conversation

With a contact ID in hand, open the conversation. Intercom's `POST /conversations` endpoint creates a conversation initiated by a contact: the `from` object identifies the contact and `body` holds the message text. Intercom's API doesn't accept HTML in this `body` field, so send the message's plain-text content, not the raw HTML body.

The call below ties the three pieces together: it resolves the contact, strips the email body to text, and creates the conversation. The Nylas `subject` becomes the first line so agents see the original email subject.

```python
def route_to_intercom(msg):
    sender = msg["from"][0]
    contact_id = find_or_create_contact(sender)

    text = html_to_text(msg.get("body", ""))        # plain text, no HTML
    body = f"Subject: {msg.get('subject', '(no subject)')}\n\n{text}"

    requests.post(
        f"{INTERCOM}/conversations",
        json={"from": {"type": "user", "id": contact_id}, "body": body},
        headers=HEADERS,
    )
```

That's the full inbound path: one email in the mailbox becomes one Intercom contact and one conversation. The 5 fields you touched (`from`, `subject`, `body`, contact `id`, conversation `body`) are all that the round trip needs.

## Reply from the same mailbox

When an agent answers in Intercom, send that reply back through the original mailbox so the customer's thread stays unbroken. The `POST /v3/grants/{grant_id}/messages/send` endpoint sends through the user's own mailbox across all 6 providers with no SMTP setup, and the reply lands in the provider's Sent folder like any normal email. Nylas refreshes the OAuth token automatically.

````bash [sendReply-Request]

curl --request POST \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/send' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
		"subject": "Reaching Out with Nylas",
		"body": "Reaching out using the <a href='https://www.nylas.com/products/email-api/'>Nylas Email API</a>",
		"to": [{
			"name": "Leyah Miller",
			"email": "leyah@example.com"
		}],
		"tracking_options": {
			"opens": true,
			"links": true,
			"thread_replies": true,
			"label": "hey just testing"
		}
	}'

````

````js [sendReply-Node.js SDK]

import Nylas from "nylas";

const nylas = new Nylas({
  apiKey: "<NYLAS_API_KEY>",
  apiUri: "<NYLAS_API_URI>",
});

async function sendEmail() {
  try {
    const sentMessage = await nylas.messages.send({
      identifier: "<NYLAS_GRANT_ID>",
      requestBody: {
        to: [{ name: "Name", email: "<EMAIL>" }],
        replyTo: [{ name: "Name", email: "<EMAIL>" }],
        replyToMessageId: "<MESSAGE_ID>",
        subject: "Your Subject Here",
        body: "Your email body here.",
      },
    });

    console.log("Sent message:", sentMessage);
  } catch (error) {
    console.error("Error sending email:", error);
  }
}

sendEmail();


````

````python [sendReply-Python SDK]

import os
import sys
from nylas import Client
from nylas import utils

nylas = Client(
    "<NYLAS_API_KEY>",
    "<NYLAS_API_URI>"
)

grant_id = "<NYLAS_GRANT_ID>"
email = "<EMAIL>"

attachment = utils.file_utils.attach_file_request_builder("Nylas_Logo.png")

message = nylas.messages.send(
  grant_id,
  request_body={
    "to": [{ "name": "Name", "email": email }],
    "reply_to": [{ "name": "Name", "email": email }],
    "reply_to_message_id": "<MESSAGE_ID>",
    "subject": "Your Subject Here",
    "body": "Your email body here.",
    "attachments": [attachment]
  }
)

print(message)


````

To keep the email threaded, set `reply_to_message_id` to the original message ID from the inbound webhook. Without it, the provider starts a new thread and the customer sees two separate conversations for one exchange.

## Things to know about Intercom

Intercom's data model and rate limits shape how this integration behaves. A handful of specifics save you debugging time:

- **Contacts vs. leads vs. users.** Intercom contacts have a `role` of either `user` (identified) or `lead` (anonymous). Email senders should be created with `role: "user"` because you already have a verified email address. This is the main divergence from a ticket-based help desk, where every requester is the same entity type.
- **Rate limits.** Intercom's REST API allows roughly 10,000 requests per minute per app on most plans (with a higher 25,000-per-minute ceiling per workspace), and returns `429 Too Many Requests` with a `Retry-After` header when you exceed it. The search-then-create contact pattern costs 2 calls per new sender, so a burst of inbound mail can add up.
- **Pin the API version.** Intercom versions its API by date through the `Intercom-Version` header. Without it, your workspace uses its default version, which Intercom can change. Pin it so payload shapes stay stable.
- **No HTML in conversation bodies.** The `POST /conversations` `body` field rejects HTML. Convert the Nylas message body to plain text before sending, or Intercom stores escaped markup that reads badly in the agent inbox.
- **Conversation parts.** Replies and notes attach to a conversation as "parts" through a separate endpoint, not by creating a new conversation. When an agent replies and you mirror it outward, send through Nylas; don't create a second Intercom conversation.

For the official reference, see the [Intercom Conversations API](https://developers.intercom.com/docs/references/rest-api/api.intercom.io/conversations) and [Contacts API](https://developers.intercom.com/docs/references/rest-api/api.intercom.io/contacts).

## What's next

- [Receive real-time email with webhooks](/docs/cookbook/use-cases/build/realtime-webhooks/): the full `message.created` subscription and signature verification
- [Monitor an inbox for support tickets](/docs/cookbook/use-cases/ingest/monitor-inbox-support-tickets/): classify and route inbound email before it reaches Intercom
- [Build a shared team inbox](/docs/cookbook/email/shared-team-inbox/): claim, assignment, and collision avoidance if you keep some work in the mailbox
- [Nylas for customer support](/docs/cookbook/use-cases/industries/customer-support/): the wider set of support automations
- [Messages API reference](/docs/reference/api/messages/): every field on the message object