Most "AI email" integrations assume everything on the other side speaks REST. You wire up a webhook, you call POST /messages/send
, and you move on. That works right up until you remember how much of your stack doesn't speak REST and never will: the ticketing system that ingests mail over IMAP, the backup script your predecessor wrote in 2014, the monitoring tool that only knows how to send SMTP, the compliance archiver that polls a mailbox every five minutes. None of those are getting rewritten to call an HTTP API for your demo.
So here's the trick that makes a Nylas Agent Account genuinely useful in a real environment: it's not API-only. You can expose the same mailbox over IMAP and SMTP submission, hand the host, port, and credentials to one of those legacy tools, and let it read and send like it's talking to any old mail server. Meanwhile your agent drives that identical mailbox over the v3 API. Both surfaces hit one storage layer. A flag, move, or delete on either side shows up on the other within seconds.
I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for. As usual I'll show both angles for every operation — the raw curl
against the API and the nylas
command — because half the point of an Agent Account is that you can mix them freely.
An Agent Account is just a grant. It has a grant_id
, and that grant_id
works with every grant-scoped endpoint you already know — Messages, Drafts, Threads, Folders, Attachments, Contacts, Calendars, Events. There's nothing new to learn on the data plane.
The IMAP/SMTP layer doesn't change that model. It adds a second door into the same room:
That last point is the one that surprises people, so I'll be blunt about it later: this is shared mutable state across two protocols. It's a feature, but you should design for it.
You could, in theory, stand up Dovecot, bolt on Postfix, wire in DKIM and SPF and DMARC, warm an IP, and expose that to your legacy tools yourself. People do. It's also a multi-week project with an on-call rotation attached.
With an Agent Account you skip all of it. Nylas hosts the mailbox, runs the IMAP and SMTP submission servers, handles TLS, manages deliverability, and gives you the API on top. The part I like as an SRE: there's no OAuth token to refresh. A connected Gmail or Microsoft grant lives and dies by its refresh token. An Agent Account has no upstream provider, so the grant doesn't silently expire on you at 3 a.m. — it just keeps being a mailbox.
The honest tradeoff: this is a Nylas-hosted mailbox, not your existing Google Workspace or Microsoft 365 account. If your goal is to put an agent on a human's real inbox, that's a connected-grant problem, not this. Agent Accounts are for mailboxes the agent (and your tooling) owns — support@
, notifications@
, intake@
, the address a workflow lives behind.
You need three things:
*.nylas.email
trial subdomain. New domains warm over roughly four weeks before they're sending at full reputation.grant_id
.If you don't have the account yet, create one. Over the API it's a POST /v3/connect/custom
with "provider": "nylas"
:
curl --request POST \
--url "https://api.us.nylas.com/v3/connect/custom" \
--header "Authorization: Bearer $NYLAS_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"provider": "nylas",
"name": "Support Bot",
"settings": {
"email": "support@yourcompany.com"
}
}'
The response carries the grant_id
. The optional top-level name
sets the display name on outbound mail. No refresh token, no redirect dance.
From the CLI it's one line:
nylas agent account create support@yourcompany.com --name "Support Bot"
The API auto-creates a default workspace and policy for the account, so you don't pass a workspace on create — there's no --workspace
flag. If you later want a custom policy governing limits and spam, you attach it to the workspace with nylas workspace update <workspace-id> --policy-id <policy-id>
.
New to Agent Accounts in general? The Agent Accounts docs walk through the model end to end. Come back here once you have a grant_id
in hand.
Here's the thing that trips people up first: API access and protocol access use different credentials. The API authenticates with your Nylas API key and a grant_id
. IMAP and SMTP can't use that — a mail client only knows how to send a username and a password. So protocol access has its own credential: an app password set directly on the grant.
Until you set one, Nylas rejects every IMAP LOGIN
and SMTP AUTH
attempt. The API still works fine without it; the app password gates only the protocol door. Set it before you hand connection details to any legacy tool.
The password has rules, and Nylas validates them on write:
Nylas stores it as a bcrypt hash, so you can't read it back later — only reset it. Treat it like any other secret: generate it, stash it in your secrets manager, hand it to the client.
You can set the app password at creation time. Over the CLI, pass --app-password
:
nylas agent account create support@yourcompany.com \
--name "Support Bot" \
--app-password "MySecureP4ssword2024"
Over the API, drop it into settings
on the same POST /v3/connect/custom
call:
curl --request POST \
--url "https://api.us.nylas.com/v3/connect/custom" \
--header "Authorization: Bearer $NYLAS_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"provider": "nylas",
"name": "Support Bot",
"settings": {
"email": "support@yourcompany.com",
"app_password": "MySecureP4ssword2024"
}
}'
When the app password leaks, or you're rotating on schedule, you set a new one on the grant you already have.
From the CLI, agent account update
takes the same flag and looks the account up by grant_id
or by email:
nylas agent account update support@yourcompany.com \
--app-password "MyRotatedP4ssword2025"
Over the API, PATCH /v3/grants/{grant_id}
does it — but note one sharp edge: a PATCH
replaces the entire settings
object, so you have to send email
alongside the new app_password
or you'll wipe it out.
curl --request PATCH \
--url "https://api.us.nylas.com/v3/grants/$NYLAS_GRANT_ID" \
--header "Authorization: Bearer $NYLAS_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"settings": {
"email": "support@yourcompany.com",
"app_password": "MyRotatedP4ssword2025"
}
}'
Rotating disconnects every active IMAP and SMTP client immediately — each one reconnects after the user (or the config) supplies the new value. Plan rotations for a quiet window if a legacy tool can't gracefully re-auth.
Once the app password is set, point any IMAP/SMTP tool at the Nylas servers for your region. The credentials are the account's own email address and the app password.
| Setting | Value |
|---|---|
| IMAP host | |
mail.us.nylas.email (US) · mail.eu.nylas.email (EU) |
|
| IMAP port | |
993 (implicit TLS) |
|
| SMTP host | |
mail.us.nylas.email (US) · mail.eu.nylas.email (EU) |
|
| SMTP port | |
465 (implicit TLS) or 587 (STARTTLS) |
|
| Username | The Agent Account email, e.g. support@yourcompany.com |
| Password | The app_password you set on the grant |
| Encryption | TLS required on every port |
In a desktop client like Thunderbird, Apple Mail, or Outlook, that's just the manual-setup screen: server mail.us.nylas.email
, IMAP 993 SSL/TLS, SMTP 465 SSL/TLS (or 587 STARTTLS), username and password as above.
For a script, it's whatever your language's IMAP library wants. The point is that it's ordinary — here's a throwaway Python check that the credentials work and counts what's in the inbox:
import imaplib
host = "mail.us.nylas.email"
user = "support@yourcompany.com"
app_password = "MySecureP4ssword2024" # pull from your secrets manager
with imaplib.IMAP4_SSL(host, 993) as imap:
imap.login(user, app_password)
imap.select("INBOX")
typ, data = imap.search(None, "ALL")
print("messages in inbox:", len(data[0].split()))
No SDK, no API key, no grant_id
in sight. That's the legacy tool's whole world, and it's enough.
This is the part worth internalizing, so let me make it concrete. The IMAP client above and the API are looking at one mailbox. Watch what happens when each side acts.
List the inbox over the API:
curl --request GET \
--url "https://api.us.nylas.com/v3/grants/$NYLAS_GRANT_ID/messages?in=inbox&limit=5" \
--header "Authorization: Bearer $NYLAS_API_KEY"
Or the CLI equivalent, which resolves the Agent Account grant for you:
nylas email list --folder INBOX
Now flag a message as read from the IMAP side with a STORE \Seen
, or move it to another folder. Re-run that same nylas email list
or GET /messages
and the unread
and folders
fields already reflect it. There's no propagation delay you need to code around — it's the same row.
It runs the other direction too. Send from the API:
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 '{
"to": [{ "email": "customer@example.com" }],
"subject": "Re: your ticket",
"body": "Thanks — we are on it."
}'
nylas email send --to customer@example.com \
--subject "Re: your ticket" \
--body "Thanks — we are on it."
That message lands in the Sent folder your IMAP client sees, exactly as if the client had submitted it over SMTP. And a message submitted over SMTP shows up in the API's message list the same way. Replying in-thread works from either side; from the CLI, nylas email reply <message-id> --body "..."
preserves the In-Reply-To
and References
headers so the conversation stays threaded.
The folder mapping is the obvious one: IMAP INBOX
/Sent
/Drafts
/Trash
/Junk
/Archive
map to the API folder IDs inbox
/sent
/drafts
/trash
/junk
/archive
. Custom folders you create over either surface appear on the other. IMAP APPEND
even deduplicates on the MIME Message-ID
, so a message that's already in the mailbox (because SMTP just put it there) won't get a phantom copy.
Because both surfaces share storage, both fire the same webhooks — and this is how your agent stays in the loop on what the legacy tool is doing. A human marks a message read in Thunderbird? message.updated
. The archiver moves something? message.updated
. New inbound mail, whether your agent or the IMAP client sees it first? message.created
.
Webhooks are application-scoped, not grant-scoped. You subscribe once at the app level and every grant's events arrive at that endpoint, each payload carrying a grant_id
you filter on:
curl --request POST \
--url "https://api.us.nylas.com/v3/webhooks" \
--header "Authorization: Bearer $NYLAS_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"trigger_types": ["message.created", "message.updated"],
"webhook_url": "https://your-app.example.com/nylas/webhooks",
"description": "Agent mailbox events"
}'
nylas webhook create \
--url https://your-app.example.com/nylas/webhooks \
--triggers message.created,message.updated
A few facts to build correctly against, all of which I've watched people get wrong:
GET /v3/grants/{grant_id}/messages/{message_id}
when you actually need the content. Also branch on message.created.truncated
— that's the variant you get when a body exceeds ~1 MB and gets omitted.id
.id
is constant across all retries of one event — that's your dedup key. The inner data.object.id
(the message id) identifies the message itself; you can additionally guard on it to avoid acting twice on the same message.X-Nylas-Signature
header: a hex HMAC-SHA256 of the crypto.timingSafeEqual
, check both buffers are the same length first, because it throws on a length mismatch. The CLI's nylas webhook verify
does this check locally while you're developing.Agent Accounts also emit deliverability webhooks for outbound mail — message.delivered
, message.bounced
, message.complaint
, and message.rejected
. Worth noting: message.rejected
fires specifically when an outbound message is rejected because an attachment contained a virus, not as a generic catch-all rejection signal.
A few honest gotchas before you ship this.
Shared mutable state is real. Two protocols writing one mailbox means you can race yourself. If your agent auto-archives messages over the API at the same moment a human is reading them in a client, the message moves out from under them. Decide who owns what. A common split: the agent handles triage and labeling, humans handle replies — or the agent only touches messages in a folder humans don't live in.
Attachments don't go through SMTP from the API side. When the agent sends with an attachment, that's the Messages or Drafts API, not SMTP. You attach either as Base64 strings in the JSON attachments
array, or via multipart where the file form field is named attachment
— not file
, which is the single most common mistake. Note that nylas email send
has no attachment flag; for attachments from the CLI you stage a draft with nylas email drafts create --attach
and send that.
Outbound size cap. SMTP submission enforces a 25 MB ceiling on outbound messages at the DATA
command. Over it and the send is rejected with an error naming the limit.
Connection limits exist. There's a default cap of 20 concurrent IMAP connections per grant, a 30-minute IDLE timeout (the client re-issues IDLE), and a 5-minute timeout on otherwise-idle TCP connections. If your legacy tool opens a connection per worker, count your workers.
Free-plan ceilings. The free plan allows 200 messages per account per day, 3 GB of storage per org, and retains the inbox 30 days and spam 7 days. Fine for building; size up before production volume.
Rotation disconnects clients. Said it above, repeating it because it's an operational footgun: rotating the app password drops every live session. Coordinate it with whoever owns the legacy tool's config.
The pattern here is small but powerful: an Agent Account gives you a mailbox your agent drives over a clean v3 API, and a standard IMAP/SMTP endpoint for every tool that will never call REST — both pointed at the same storage, both firing the same webhooks. You don't have to choose between "modern API" and "works with the stuff I already run." You get both doors into one room.
From here:
nylas agent
, nylas email
, and nylas webhook
flag.Set the app password, point one real client at the mailbox, and watch a flag you flip in the API show up in the client a second later. That moment is when the two-doors model clicks.
When this post is published, link AI agents and crawlers to the retrieval-ready version on cli.nylas.com
: