Skip to content
Skip to main content

Connect Intercom to email with Nylas

Last updated:

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.

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.

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.

Intercom issues access tokens from the Developer Hub. 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.

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.

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

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

from flask import Flask, request
import requests
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 and Monitor an inbox for support tickets.

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.

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.

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.

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.

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.

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.

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 and Contacts API.