A cart-recovery email asks a question whether you want it to or not. "We saved your cart" invites a reply: is the blue one still in stock? does the discount stack with the one in my account? when does this ship? Most recovery flows send that nudge from [email protected] and route every one of those answers into a black hole. The shopper who was a click away from buying hits a wall, and your "win-back" email becomes the reason they didn't come back.
The fix isn't a smarter subject line. It's sending the nudge from an address that can reply. An address a shopper can write back to, that reads the reply, answers the in-stock question with real data, and — the part everyone forgets — stops the rest of the sequence the instant the shopper engages. Nobody should get "still thinking it over?" an hour after they asked you a question and you answered it.
That replyable, automatable address is a Nylas Agent Account. I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for when wiring this up; the curl calls beside them are what the CLI does under the hood, so you can drop either into your stack.
What an Agent Account actually is
The thing worth internalizing before you write any code: an Agent Account is just a grant. Same grant_id, same /v3/grants/{grant_id}/* endpoints, same Authorization: Bearer <NYLAS_API_KEY> header as any connected Gmail or Microsoft mailbox. There's no separate "marketing email" SDK, no new auth dance, no second data model. If you've ever sent a message or listed a thread from a Nylas grant, you already know the data plane for this entire post.
What's different is the control plane. There's no human OAuth and no refresh token to babysit — you provision the mailbox yourself on a domain you own (or a *.nylas.email trial subdomain), and Nylas hosts it. For a recovery flow that's exactly right: the address [email protected] belongs to the system, not to a person who might leave and take their refresh token with them. And because it's a full mailbox, replies land back in it as ordinary inbound messages you can read and answer — which is the whole point.
One honest tradeoff to flag up front, because it shapes the design: Agent Accounts don't support custom metadata on messages. You can't stamp { "cart_id": "C-4821", "step": 2 } onto the outbound email and read it back later to figure out where a shopper is in the sequence. That's fine. The sequence state was always going to live in your database — keyed by cart, by shopper, by thread — and that's where it stays. The email is the message; your DB is the state machine.
Why a conversational agent beats a one-way ESP here
Let me be fair to the blast-email tools first, because this isn't a "throw out your ESP" pitch. If your recovery email is a pure one-way nudge — "you left something behind, here's 10% off, click to come back" — and you genuinely don't care about replies, a transactional ESP (SendGrid, Resend, Postmark) is a perfectly good fit. It's cheaper, it's built for fan-out, and you don't need any of this.
The agent earns its keep the moment a reply needs a real answer:
Replies go somewhere. "Is this still in stock?" is the single most common cart-recovery reply, and it's a buying signal you do not want to drop in a no-reply box. The agent reads it, checks your inventory, and answers — in-thread, from the same address.
The sequence stops on engagement. A one-way blaster keeps firing on a schedule. A conversational agent keys offmessage.created: the moment a reply arrives, you flip the sequence state topausedand the next nudge never sends. No "still thinking about it?" landing on someone you're mid-conversation with.
Context survives across steps. Because every send and reply rides the same thread, the shopper sees one coherent conversation, not three disconnected marketing blasts with the same footer.
It's the same grant you already send order confirmations from. One mailbox, one auth model, both transactional and conversational mail.
If you only ever need fire-and-forget nudges, stay on your ESP. The Agent Account wins when the recovery email is the start of a conversation, not the end of one.
Before you begin
You'll need:
- A Nylas API key from the dashboard, exported as
NYLAS_API_KEY. - A registered domain for the mailbox, or a Nylas
*.nylas.emailtrial subdomain. New domains warm over roughly four weeks, so ramp volume gradually — recovery mail is exactly the kind of low-volume, high-intent send that warms a domain well. - The for the product overview, then come back here.
:::
Provision the recovery mailbox
Create the grant first. The API call is
POST /v3/connect/customwithprovider: "nylas"and the email on your domain. The optional top-levelnamesets the display name shoppers see.
CODEcurl --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": "YourStore Cart",
"settings": { "email": "[email protected]" }
}'
The response carries the
grant_id— the only identifier you carry forward. Nylas also auto-creates a default workspace and policy for the account.
The CLI collapses the connector setup, grant creation, and default workspace into one command:
If the
nylasconnector doesn't exist in your app yet, this creates it first, then the grant. Add--jsonto capture thegrant_idfor a script. Export it asGRANT_IDfor the rest of this post.
Subscribe to inbound replies
The sequence needs to know when a shopper writes back. Inbound mail fires the standard
message.createdwebhook — the same trigger every other Nylas inbox uses. Register a webhook against it and you'll get an event the instant a reply lands.
CODEcurl --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"],
"webhook_url": "https://api.yourstore.com/hooks/cart",
"description": "Cart recovery replies"
}'
From the terminal, the same registration plus a local listener to watch real events during development:
CODEnylas webhook triggers
nylas webhook create --url https://api.yourstore.com/hooks/cart --triggers message.created
nylas webhook server --port 4000 --tunnel cloudflared --secret <webhook-secret>
nylas webhook serverstands up a local endpoint behind a cloudflared tunnel and verifies the HMAC signature on each event, so you can trigger a real reply and watch the payload land before you ship the production handler.
Send the recovery sequence
A recovery sequence is two or three sends spaced over a day or two: an immediate nudge, a reminder the next morning, a last-call with an incentive. Each one is a normal grant send —
POST /v3/grants/{grant_id}/messages/send.
Here's the first step as a
curlcall:
CODEcurl --request POST \
--url "https://api.us.nylas.com/v3/grants/$GRANT_ID/messages/send" \
--header 'Authorization: Bearer '"$NYLAS_API_KEY" \
--header 'Content-Type: application/json' \
--data '{
"to": [{ "email": "[email protected]", "name": "Sam" }],
"subject": "You left something in your cart",
"body": "Hi Sam — your cart is still saved. Reply to this email if you have any questions about your order and we'\''ll get right back to you."
}'
The same send from the CLI:
CODEnylas email send "$GRANT_ID" \
--to [email protected] \
--subject "You left something in your cart" \
--body "Hi Sam — your cart is still saved. Reply if you have any questions."
The response includes the message's
idandthread_id. Persist both, keyed to the cart, the moment the send returns:
CODEcart C-4821 → { thread_id, last_message_id, step: 1, state: "active" }
That row is your sequence state. There's no metadata on the email to lean on, so this DB record is the only place that knows shopper Sam is on step 1 of cart C-4821's recovery flow.
Schedule the later steps instead of running a cron
You don't have to hold step 2 in a job queue and fire it yourself. The send endpoint accepts a
send_at(Unix timestamp), and Nylas queues the message server-side:
CODEcurl --request POST \
--url "https://api.us.nylas.com/v3/grants/$GRANT_ID/messages/send" \
--header 'Authorization: Bearer '"$NYLAS_API_KEY" \
--header 'Content-Type: application/json' \
--data '{
"to": [{ "email": "[email protected]", "name": "Sam" }],
"subject": "Still thinking it over?",
"body": "Your cart is saved for a little longer. Reply with any questions.",
"send_at": 1718000000
}'
The CLI takes a human duration and computes the timestamp for you:
CODEnylas email send "$GRANT_ID" \
--to [email protected] \
--subject "Still thinking it over?" \
--body "Your cart is saved for a little longer. Reply with any questions." \
--schedule "tomorrow 9am"
A scheduled send returns a
schedule_idyou can store and cancel later — which matters a lot, because canceling step 2 is exactly how you stop the sequence when the shopper replies. More on that next.
Stop the sequence when the shopper replies
This is the behavior that separates a conversation from a pestering. The signal is the
message.createdwebhook: a reply on a thread you're tracking means the shopper engaged, and the rest of the nudges should not fire.
The flow in your handler:
- The webhook tells you a message arrived on
thread_id T. LookTup in your sequence state. - If you find an active sequence for that thread, flip its state to
pausedso no future step sends. - If you scheduled later steps with
send_at, cancel them byschedule_idso the queued message never goes out.
Two important caveats that the — the detection, dedup, and routing patterns in depth.
- The webhook tells you a message arrived on
— policies, rules, and deliverability webhooks (message.delivered,message.bounced,message.complaint).
- Industry playbooks hub: https://cli.nylas.com/ai-answers/agent-account-industry-playbooks.md
SOCIAL SHARE CARD GENERATOR