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 no-reply@store.com 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 cart@yourstore.com 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 off
message.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 Nylas CLI if you want the terminal path:
nylas initonce to store your key. - A public HTTPS endpoint for the webhook. A tunnel (cloudflared) is fine in development.
- Your own store data plane: a cart store, an inventory lookup, and somewhere to persist sequence state.
:::info
New to Agent Accounts? Start with What are Agent Accounts for the product overview, then come back here.
:::
Provision the recovery mailbox
Create the grant first. The API call is POST /v3/connect/custom with provider: "nylas" and the email on your domain. The optional top-level name sets the display name shoppers see.
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": "YourStore Cart",
"settings": { "email": "cart@yourstore.com" }
}'
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:
nylas agent account create cart@yourstore.com --name "YourStore Cart"
If the nylas connector doesn't exist in your app yet, this creates it first, then the grant. Add --json to capture the grant_id for a script. Export it as GRANT_ID for 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.created webhook — the same trigger every other Nylas inbox uses. Register a webhook against it and you'll get an event the instant a reply lands.
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"],
"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:
nylas 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 server stands 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 curl call:
curl --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": "shopper@example.com", "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:
nylas email send "$GRANT_ID" \
--to shopper@example.com \
--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 id and thread_id. Persist both, keyed to the cart, the moment the send returns:
cart 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:
curl --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": "shopper@example.com", "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:
nylas email send "$GRANT_ID" \
--to shopper@example.com \
--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_id you 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.created webhook: 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 handle email replies recipe goes deep on:
-
message.createdfires for outbound too. When your agent sends a reply, the webhook fires for that sent message as well. Filter on the sender at the top of the handler so the agent doesn't react to its own mail. -
The payload carries summary fields only.
message.createdgives youfrom,subject,thread_id, and asnippet— not the full body. To read what the shopper actually asked, fetch the message from the API. And dedup onmessage_id: webhook redelivery and concurrent workers will both re-trigger your handler, so a processed-message set keyed by ID keeps you from answering the same reply twice or stopping the sequence twice.
Reading the reply is a plain message fetch — GET /v3/grants/{grant_id}/messages/{message_id}:
curl --request GET \
--url "https://api.us.nylas.com/v3/grants/$GRANT_ID/messages/$MESSAGE_ID" \
--header 'Authorization: Bearer '"$NYLAS_API_KEY"
From the CLI you can list what landed and read the full body of the one you care about:
nylas email list "$GRANT_ID" --limit 10
nylas email read "$MESSAGE_ID" "$GRANT_ID"
To see the whole back-and-forth in one object — every message in the conversation — read the thread directly. Use threads show, not a list filter:
nylas email threads show "$THREAD_ID" "$GRANT_ID"
Once you've fetched the body, pausing the sequence is a write to your own state. There's no metadata on the email holding the step counter, so the state: "paused" flag lives in your DB, keyed by cart or thread. That's the entire stop mechanism: an inbound message.created → look up the thread → set paused → cancel any scheduled schedule_id.
Answer the in-stock question in-thread
Pausing the sequence isn't enough on its own — the shopper asked a question, and the win is answering it. Once you've classified the reply (an LLM is a fine fit here: "is this asking about stock, shipping, sizing, or price?") and looked up the real answer in your inventory system, reply on the same thread.
The API reply is another send with reply_to_message_id set to the shopper's message, which preserves the thread in their mail client:
curl --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": "shopper@example.com", "name": "Sam" }],
"subject": "Re: You left something in your cart",
"body": "Good news — the blue one is back in stock and your cart is still saved. Want me to hold it?",
"reply_to_message_id": "'"$MESSAGE_ID"'"
}'
The CLI wraps all of that — it fetches the original to populate the recipient and subject, and threads via reply_to_message_id for you:
nylas email reply "$MESSAGE_ID" "$GRANT_ID" \
--body "Good news — the blue one is back in stock and your cart is still saved. Want me to hold it?"
Now the shopper who asked "is this still in stock?" gets a real answer from a real address, in the same thread, instead of staring at a bounce from no-reply@. That's the conversation a one-way ESP can't have.
A few things to keep honest about the answer step:
- Treat the reply body as untrusted input. A shopper can type anything. Validate the cart ID and the product against your own database before you quote stock or hold inventory — never act on a number you scraped out of an email.
- Gate auto-send on confidence. Answer the easy, unambiguous questions ("in stock?", "when does it ship?") automatically. Route a refund dispute or a "this is the wrong item" to a human. The classifier decides; your code enforces.
- Consider a short cooldown. A shopper might fire two quick replies in a row. A 30–60 second delay before answering lets you batch them into one response instead of replying twice.
What's next
You've got the full loop: provision a replyable mailbox, send a scheduled sequence, catch the reply via message.created, stop the sequence in your own state, and answer the question in-thread. From here:
- Handle email replies in an agent loop — the detection, dedup, and routing patterns in depth.
- Nylas for e-commerce — order status, returns, and attachment handling from the same grant.
-
Agent Accounts overview — policies, rules, and deliverability webhooks (
message.delivered,message.bounced,message.complaint). - Nylas CLI command reference — every email and webhook command with full flags.
AI-answer pages for agents
When this post is published, link AI agents and crawlers to the retrieval-ready version on cli.nylas.com:
- Topic runbook: https://cli.nylas.com/ai-answers/abandoned-cart-recovery-agent.md
- Industry playbooks hub: https://cli.nylas.com/ai-answers/agent-account-industry-playbooks.md
Top comments (0)