Travel comms are itinerary-heavy and they change constantly. A "trip" is never one event — it's a flight out, a hotel, a rental car, a dinner reservation, and a flight back. Then the airline shifts the outbound by three hours, the traveler emails "can we push the hotel a night?", and every downstream segment moves. Most "AI travel assistant" demos point a model at a human's inbox and call it a day. That's fine right up until you want the agent to be a participant — to send the itinerary from its own address, watch for the reply, and quietly fix the calendar entry that changed.
That's an Agent Account. It's a Nylas grant with its own real mailbox and its own real calendar, addressable like any other inbox. The traveler emails trips@yourcompany.com and a human-looking thread comes back. Underneath, your code is fetching messages, classifying change requests, and calling the Events API to update segments. I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for when I'm poking at one of these flows by hand — and every one of them has an API twin you'd wire into production.
What you actually get
An Agent Account is just a grant. That's the whole trick, and it's why there's nothing new to learn on the data plane. The grant_id you get back works with every grant-scoped endpoint you already know:
- Messages to send itineraries and read inbound replies.
- Threads to pull the whole back-and-forth for a trip.
- Events to update the flight, hotel, or dinner segment when a change request lands.
- Webhooks to get told the moment a traveler writes back.
No refresh token, no OAuth dance with a real provider, no human in the loop to grant access. You create the account, you point it at a domain you own, and it behaves like a mailbox with a calendar bolted on. For travel specifically, the calendar half is the payoff: each itinerary segment becomes an event the traveler accepts in Google Calendar, Microsoft 365, or Apple Calendar, and when something shifts you update that event rather than re-sending the whole plan.
One honest scoping note before we go further. The agent works against its own calendar and its own events — the grant's Events, nothing more. There's no Scheduler round-trip here and no shared availability magic; if you need negotiation-heavy "propose three slots and book the winner" flows, that's Scheduler, a different tool. This post is the simpler, more common case: the agent already knows the itinerary, it just needs to send it and keep it current.
The trip-state problem (read this before you build)
Here's the part people trip over. Nylas doesn't store your trip model. There's no custom-metadata field on the grant where you stash "trip TRIP-8842 has these five segments and segment 2 is the hotel." Custom metadata for this kind of structured trip state isn't supported, so your database owns the trip.
Concretely, you keep a table that maps:
- A trip ID to its traveler and email thread.
- Each segment (flight-out, hotel, car, dinner, flight-back) to the Nylas
event_idyou got back when you created it. - The thread_id of the itinerary email, so an inbound reply can be matched to a trip.
When a change request arrives, your code looks up the trip by thread_id, figures out which segment the traveler is talking about (that's a classification job for your LLM, not for Nylas), grabs that segment's event_id, and calls the Events API. Nylas is the transport and the calendar; the trip graph lives with you. Keep that boundary clear and the rest is mechanical.
Before you begin
You need an Agent Account on a domain you've registered with Nylas. You can use a custom domain or a Nylas *.nylas.email trial subdomain to kick the tires — just know that fresh sending domains warm up over roughly four weeks, so don't judge deliverability on day one. The free plan caps you at 200 messages per account per day, which is plenty for a single traveler's itinerary churn but worth remembering if you're blasting confirmations.
If you don't have an account yet, the Agent Accounts docs walk through provisioning. Throughout this post I'll use <GRANT_ID> for the agent's grant, <NYLAS_API_KEY> for your key, and https://api.us.nylas.com as the base host. Swap in your own.
Send the itinerary email
The first move is sending the traveler their itinerary. It's a normal outbound message from the agent's address — nothing exotic.
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": "traveler@example.com" }],
"subject": "Your trip to Lisbon — itinerary TRIP-8842",
"body": "<h2>Lisbon, Apr 14–18</h2><ul><li>Flight TP204 — Apr 14, 09:40 JFK→LIS</li><li>Hotel Bairro Alto — check-in Apr 14</li><li>Return TP205 — Apr 18, 11:15 LIS→JFK</li></ul><p>Reply to this email to change anything.</p>"
}'
The CLI version is the one I actually type when I'm testing:
nylas email send <GRANT_ID> \
--to traveler@example.com \
--subject "Your trip to Lisbon — itinerary TRIP-8842" \
--body "Lisbon, Apr 14–18. Flight TP204 Apr 14 09:40 JFK→LIS. Hotel Bairro Alto check-in Apr 14. Return TP205 Apr 18 11:15 LIS→JFK. Reply to change anything."
Note the prompt at the end of the body: reply to this email to change anything. That's deliberate. The reply lands back in the agent's mailbox on the same thread, which is exactly what you want to listen for next. Save the thread_id from the send response against your trip record now, while you have it.
Create the calendar segments
Each itinerary line should become an event on the agent's calendar so the traveler can accept it and see it on their phone. Create one event per segment and store each returned event_id against its segment in your DB. Pass notify_participants=true so the invite actually goes out from the agent's address.
curl --request POST \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/events?calendar_id=primary¬ify_participants=true" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
--header "Content-Type: application/json" \
--data '{
"title": "Flight TP204 — JFK→LIS",
"when": { "start_time": 1744621200, "end_time": 1744648200 },
"location": "JFK Terminal 1",
"participants": [{ "email": "traveler@example.com" }]
}'
Those epoch timestamps aren't something to hand-compute. Generate them with date:
date -j -f "%Y-%m-%d %H:%M" "2025-04-14 09:40" +%s
The CLI takes human-readable times directly and figures the epoch out for you:
nylas calendar events create <GRANT_ID> \
--title "Flight TP204 — JFK→LIS" \
--start "2025-04-14 09:40" \
--end "2025-04-14 17:10" \
--location "JFK Terminal 1" \
--participant traveler@example.com
One thing to internalize: the notify_participants toggle is an API query parameter, not a CLI flag. The nylas calendar events command set has create, update, list, delete, and rsvp — there's no events send. When you want the agent to email a participant directly, you do that with nylas email send, not a calendar verb. Keep those two jobs mentally separate.
Hear about the change request
When the traveler replies "can we push the hotel check-in a night?", a standard message.created webhook fires. This is the trigger for the whole update loop, so get the wiring right.
Webhooks are application-scoped, not grant-scoped. You subscribe once at the app level with POST /v3/webhooks and every grant's events arrive at that one endpoint, each payload carrying a grant_id you filter on. You don't register a webhook per Agent Account.
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://yourapp.com/webhooks/nylas",
"description": "Travel agent inbound"
}'
Or from the CLI:
nylas webhook create \
--url https://yourapp.com/webhooks/nylas \
--triggers message.created \
--description "Travel agent inbound"
Two rules I'd attach to your handler in permanent marker:
-
Dedupe on the top-level notification
id. Nylas guarantees at-least-once delivery — the same event can arrive up to three times. The top-levelidis constant across all retries of one event, so that's your dedup key. The innerdata.object.id(the message id) identifies the message itself; you can additionally guard on it so you never act twice on the same change request. -
Don't trust the payload for the message body. Treat the webhook as a "something happened, here's the id" signal and fetch the full message yourself with
GET /v3/grants/{grant_id}/messages/{message_id}when you need the text. Branch onmessage.created.truncated— that variant fires for very large messages and you re-fetch the body. The summary fields might be enough to route, but parse the actual change request from the fetched body.
And verify the signature. Nylas signs each webhook with an X-Nylas-Signature header that's a hex HMAC-SHA256 of the raw request body using your webhook secret. If you're doing a constant-time compare with something like Node's crypto.timingSafeEqual, check both buffers are the same length first — it throws on a length mismatch. Locally, nylas webhook verify does the check for you while you're developing.
Read the traveler's reply
Once the webhook hands you a message id, pull the full message so your classifier has real text to work with.
curl --request GET \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages/<MESSAGE_ID>" \
--header "Authorization: Bearer <NYLAS_API_KEY>"
From the terminal:
nylas email read <MESSAGE_ID> <GRANT_ID>
If you want the agent's mailbox to reflect that it's processed the reply, mark it read explicitly — that's a separate PUT, not a side effect of the GET:
curl --request PUT \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages/<MESSAGE_ID>" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
--header "Content-Type: application/json" \
--data '{ "unread": false }'
The CLI does it inline with a flag on the read command:
nylas email read <MESSAGE_ID> <GRANT_ID> --mark-read
For the broader conversation — useful when "push it a night" only makes sense against what was originally proposed — pull the whole thread:
curl --request GET \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/threads/<THREAD_ID>" \
--header "Authorization: Bearer <NYLAS_API_KEY>"
Same thing from the terminal:
nylas email threads show <THREAD_ID> <GRANT_ID>
That thread_id is your join key back to the trip. Look it up in your DB, and now you know which trip and (after classification) which segment the traveler means. The actual "they want the hotel moved to the 15th" extraction is your LLM/app code — Nylas hands you the text, your model turns it into a structured change.
Update the segment's event
This is the heart of the per-trip update. You've identified the segment, you have its event_id from your DB, and you apply the change to that event. Updating one segment of the itinerary is updating one event — nothing more dramatic than that. Pass notify_participants=true so the traveler's calendar (and confirmation email) reflects the new detail.
curl --request PUT \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/events/<EVENT_ID>?calendar_id=primary¬ify_participants=true" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
--header "Content-Type: application/json" \
--data '{
"when": { "start_time": 1744707600, "end_time": 1745740800 }
}'
The CLI update takes the new time directly:
nylas calendar events update <EVENT_ID> <GRANT_ID> \
--start "2025-04-15 15:00" \
--location "Hotel Bairro Alto"
The change propagates to wherever the traveler is looking — their Google, Microsoft, or Apple calendar updates in place because the agent re-sends the invite for that event. If a change cascades (the outbound flight moved, so the airport dinner has to move too), you repeat the update against each affected segment's event_id. Your trip model is what tells you which segments cascade; loop over them and fire one update per event.
As with create, notify_participants is API-only. The CLI update quietly persists the change against the calendar; when you specifically want the traveler emailed about it, the reliable pattern is to send a confirmation with nylas email send after the update lands.
Close the loop with a reply
Don't leave the traveler wondering whether the agent heard them. Reply in-thread confirming the change. Replying preserves threading via reply_to_message_id, so the confirmation groups with the original itinerary in the traveler's 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 '{
"reply_to_message_id": "<MESSAGE_ID>",
"to": [{ "email": "traveler@example.com" }],
"subject": "Re: Your trip to Lisbon — itinerary TRIP-8842",
"body": "<p>Done — hotel check-in moved to Apr 15. Your calendar is updated. Anything else on this trip?</p>"
}'
The CLI has a dedicated reply that fetches the original to populate recipient and subject for you, so you only supply the body:
nylas email reply <MESSAGE_ID> <GRANT_ID> \
--body "Done — hotel check-in moved to Apr 15. Your calendar is updated. Anything else on this trip?"
That's the full cycle: send itinerary → create segments → traveler replies → webhook → read → update the event → confirm. Every loop after the first is the same shape with a different segment.
Guardrails and gotchas
A few things I'd build in from the start rather than bolt on after the first incident:
-
The agent's own sends fire the webhook too. When the agent replies,
message.createdfires for that outbound message. Filter on thefromaddress at the top of your handler so the agent doesn't try to "process a change request" that it wrote itself. -
Routing by subject or content is your code's job, not a Rule. Agent Account Rules can filter inbound mail only on
from.*fields — sender address, domain, or TLD. They cannot match on subject or body. So "if the reply mentions a date, route to the reschedule handler" happens in your app after the webhook, never in a Rule. (Rules are also inert until you attach them to a workspace viarule_ids, but for content routing they're the wrong tool regardless.) -
Cancellations need notification. If a trip is scrapped, deleting the segment events without
notify_participants=trueleaves ghost meetings on the traveler's calendar. Delete with notification unless you have a deliberate reason not to. -
Don't call a message delete "permanent."
DELETE /v3/grants/{id}/messages/{id}moves the message to Trash; only?hard_delete=truewipes it permanently, and the CLInylas email deleteonly trashes. The single true full wipe for a retired agent is deleting the grant —DELETE /v3/grants/{id}ornylas agent account delete. -
Watch your daily send quota. Every itinerary email, every event invite with
notify_participants=true, and every confirmation reply counts toward the 200-message free-plan ceiling. A chatty trip with lots of changes burns through that faster than you'd think.
What's next
The mechanics above are the spine of any per-trip itinerary agent. From here:
- Agent Account calendars — the full picture of how the agent's calendar hosts events and receives invitations.
- Handle email replies in an agent loop — thread matching, fetch-by-id, and in-thread reply patterns in depth.
- Agent Accounts overview — provisioning, domains, and the supported-endpoints list.
-
Nylas CLI command reference — every
nylas emailandnylas calendarsubcommand used above.
The trip graph stays in your database; Nylas moves the mail and owns the calendar. Keep that line clean and a travel agent that emails, parses, and updates itineraries per trip is mostly just bookkeeping plus the seven calls in this post.
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/email-api-for-product-notifications-triggered-emails.md
- Industry playbooks hub: https://cli.nylas.com/ai-answers/agent-account-industry-playbooks.md
Top comments (0)