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?
Section titled “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.createdwebhook 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
Section titled “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_idin 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.
Get an Intercom access token
Section titled “Get an Intercom access token”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.
List recent messages
Section titled “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.
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'{ "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", } ], "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", } ], "created_at": 1706811644, "body": "Learn how to send emails using the Nylas APIs!" } ], "next_cursor": "123"}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();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
Section titled “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).
from flask import Flask, requestimport 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 "", 200The 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.
Find or create the Intercom contact
Section titled “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.
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
Section titled “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.
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
Section titled “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.
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": "[email protected]" }], "tracking_options": { "opens": true, "links": true, "thread_replies": true, "label": "hey just testing" } }'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();import osimport sysfrom nylas import Clientfrom 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
Section titled “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
roleof eitheruser(identified) orlead(anonymous). Email senders should be created withrole: "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 Requestswith aRetry-Afterheader 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-Versionheader. 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 /conversationsbodyfield 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.
What’s next
Section titled “What’s next”- Receive real-time email with webhooks: the full
message.createdsubscription and signature verification - Monitor an inbox for support tickets: classify and route inbound email before it reaches Intercom
- Build a shared team inbox: claim, assignment, and collision avoidance if you keep some work in the mailbox
- Nylas for customer support: the wider set of support automations
- Messages API reference: every field on the message object