<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Qasim</title>
    <description>The latest articles on DEV Community by Qasim (@mqasimca).</description>
    <link>https://dev.to/mqasimca</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3995627%2Feddc44d6-3e99-45b8-ae80-71279c900b01.jpg</url>
      <title>DEV Community: Qasim</title>
      <link>https://dev.to/mqasimca</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mqasimca"/>
    <language>en</language>
    <item>
      <title>Sync and manage contacts across providers: Nylas Contacts API</title>
      <dc:creator>Qasim</dc:creator>
      <pubDate>Sun, 21 Jun 2026 21:27:21 +0000</pubDate>
      <link>https://dev.to/mqasimca/sync-and-manage-contacts-across-providers-nylas-contacts-api-3nne</link>
      <guid>https://dev.to/mqasimca/sync-and-manage-contacts-across-providers-nylas-contacts-api-3nne</guid>
      <description>&lt;p&gt;Contacts are messier than they look. A user's real address book is spread across the people they've saved by hand, the people they've emailed often enough that the provider auto-collected them, and the colleagues in their company directory. Google exposes these through the People API; Microsoft through Graph; both model the data differently and split it across sources you have to query separately.&lt;/p&gt;

&lt;p&gt;The Nylas Contacts API unifies all of that behind one schema and one &lt;code&gt;grant_id&lt;/code&gt;. You read saved contacts, auto-collected contacts, and directory contacts through the same endpoint, create and update entries that sync back to the provider, and organize them into groups. This post walks the contact surface from the HTTP API and the &lt;a href="https://cli.nylas.com/docs/commands" rel="noopener noreferrer"&gt;Nylas CLI&lt;/a&gt;, which mirrors every operation for terminal use.&lt;/p&gt;

&lt;p&gt;I work on the CLI, so the terminal commands below are the ones I run when I'm exploring an address book.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contact model and its three sources
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;contact&lt;/strong&gt; in Nylas carries the fields you'd expect — &lt;code&gt;given_name&lt;/code&gt;, &lt;code&gt;surname&lt;/code&gt;, &lt;code&gt;emails&lt;/code&gt;, &lt;code&gt;phone_numbers&lt;/code&gt;, &lt;code&gt;company_name&lt;/code&gt;, &lt;code&gt;job_title&lt;/code&gt;, &lt;code&gt;notes&lt;/code&gt; — plus richer ones like &lt;code&gt;im_addresses&lt;/code&gt;, &lt;code&gt;physical_addresses&lt;/code&gt;, and &lt;code&gt;web_pages&lt;/code&gt;. The schema is the same across providers, so a Google contact and a Microsoft contact deserialize into one struct.&lt;/p&gt;

&lt;p&gt;The detail that trips people up is &lt;strong&gt;source&lt;/strong&gt;. Every contact has one of three sources, and they mean very different things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;address_book&lt;/code&gt; — contacts the user saved deliberately. This is the real address book.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;inbox&lt;/code&gt; — contacts the provider auto-collected because the user emailed them. These were never explicitly saved.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;domain&lt;/code&gt; — contacts from the organization's directory (coworkers).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Knowing the source matters because "all contacts" usually isn't what you want. If you're building a contact picker, the &lt;code&gt;inbox&lt;/code&gt; source can flood it with one-off recipients the user doesn't think of as contacts. Filter by source deliberately. See the &lt;a href="https://developer.nylas.com/docs/v3/email/contacts/" rel="noopener noreferrer"&gt;Contacts API overview&lt;/a&gt; for the full data model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you begin
&lt;/h2&gt;

&lt;p&gt;You need a Nylas API key and a connected account with contacts scopes. The CLI gets you set up in two commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas init        &lt;span class="c"&gt;# create an account, generate an API key&lt;/span&gt;
nylas auth login  &lt;span class="c"&gt;# connect an account over OAuth, store the grant&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After login the CLI runs against that grant by default, so the &lt;code&gt;nylas contacts&lt;/code&gt; commands below don't need an explicit ID. For the OAuth scopes Google and Microsoft require to read and write contacts, see the &lt;a href="https://developer.nylas.com/docs/v3/auth/" rel="noopener noreferrer"&gt;authentication docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  List contacts
&lt;/h2&gt;

&lt;p&gt;Listing is the first thing you'll do. The CLI returns 50 contacts by default and lets you filter by source so you're not drowning in auto-collected addresses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 50 contacts, default&lt;/span&gt;
nylas contacts list

&lt;span class="c"&gt;# Only the real, saved address book&lt;/span&gt;
nylas contacts list &lt;span class="nt"&gt;--source&lt;/span&gt; address_book &lt;span class="nt"&gt;--limit&lt;/span&gt; 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--limit&lt;/code&gt; flag auto-paginates once you raise it above 200. Over HTTP, the same operation is a &lt;code&gt;GET&lt;/code&gt; against the contacts collection, with &lt;code&gt;source&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;phone_number&lt;/code&gt;, and &lt;code&gt;group&lt;/code&gt; as query parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/grants/&amp;lt;GRANT_ID&amp;gt;/contacts?source=address_book&amp;amp;limit=100"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;NYLAS_API_KEY&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://developer.nylas.com/docs/reference/api/contacts/list-contact/" rel="noopener noreferrer"&gt;list contacts&lt;/a&gt; reference documents every filter, and the response paginates with the &lt;code&gt;page_token&lt;/code&gt; cursor just like messages and events. The CLI flags live at &lt;a href="https://cli.nylas.com/docs/commands/contacts-list" rel="noopener noreferrer"&gt;&lt;code&gt;contacts list&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a contact
&lt;/h2&gt;

&lt;p&gt;Creating a contact writes it back to the provider, so it shows up in the user's Google Contacts or Outlook address book too. The CLI uses friendly flag names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas contacts create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--first-name&lt;/span&gt; &lt;span class="s2"&gt;"Jane"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--last-name&lt;/span&gt; &lt;span class="s2"&gt;"Smith"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--email&lt;/span&gt; &lt;span class="s2"&gt;"jane@company.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--phone&lt;/span&gt; &lt;span class="s2"&gt;"+1-555-123-4567"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--company&lt;/span&gt; &lt;span class="s2"&gt;"Acme Corp"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--job-title&lt;/span&gt; &lt;span class="s2"&gt;"Engineer"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing worth knowing: the CLI flag names are not the API field names. The CLI maps &lt;code&gt;--first-name&lt;/code&gt; to &lt;code&gt;given_name&lt;/code&gt;, &lt;code&gt;--last-name&lt;/code&gt; to &lt;code&gt;surname&lt;/code&gt;, &lt;code&gt;--email&lt;/code&gt; to the &lt;code&gt;emails&lt;/code&gt; array, and &lt;code&gt;--phone&lt;/code&gt; to &lt;code&gt;phone_numbers&lt;/code&gt;. When you call the API directly, use the API names. Note that &lt;code&gt;emails&lt;/code&gt; and &lt;code&gt;phone_numbers&lt;/code&gt; are arrays of objects, because a contact can have several of each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/grants/&amp;lt;GRANT_ID&amp;gt;/contacts"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;NYLAS_API_KEY&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "given_name": "Jane",
    "surname": "Smith",
    "company_name": "Acme Corp",
    "job_title": "Engineer",
    "emails": [{ "email": "jane@company.com", "type": "work" }],
    "phone_numbers": [{ "number": "+1-555-123-4567", "type": "work" }]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full field list — including &lt;code&gt;birthday&lt;/code&gt;, &lt;code&gt;im_addresses&lt;/code&gt;, &lt;code&gt;physical_addresses&lt;/code&gt;, &lt;code&gt;manager_name&lt;/code&gt;, and &lt;code&gt;web_pages&lt;/code&gt; — is in the &lt;a href="https://developer.nylas.com/docs/reference/api/contacts/post-contact/" rel="noopener noreferrer"&gt;create contact&lt;/a&gt; reference, and the CLI flags are at &lt;a href="https://cli.nylas.com/docs/commands/contacts-create" rel="noopener noreferrer"&gt;&lt;code&gt;contacts create&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Those array fields come with provider-specific caps that bite if you assume everyone allows many values. IMAP, iCloud, and Yahoo accept at most one email address and one phone number per contact. Microsoft and EWS allow up to three email addresses. Microsoft caps phone numbers at two home, two work, and one mobile. So a contact with four email addresses that round-trips fine on Google will silently lose values on an iCloud account. Field types carry provider rules too: the &lt;code&gt;mobile&lt;/code&gt; phone type is Google and Microsoft Graph only, and the &lt;code&gt;other&lt;/code&gt; type is Google and EWS only. If you support multiple providers, store the canonical contact in your own system and treat the provider copy as a projection that may drop fields it can't represent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search and filter
&lt;/h2&gt;

&lt;p&gt;When you need to find a contact rather than list everything, the CLI's &lt;code&gt;search&lt;/code&gt; layers structured filters on top of the list endpoint. You can match by company, email, phone, group, or restrict to contacts that actually have an email address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Everyone at one company who has an email on file&lt;/span&gt;
nylas contacts search &lt;span class="nt"&gt;--company&lt;/span&gt; &lt;span class="s2"&gt;"Acme"&lt;/span&gt; &lt;span class="nt"&gt;--has-email&lt;/span&gt;

&lt;span class="c"&gt;# A specific person by email&lt;/span&gt;
nylas contacts search &lt;span class="nt"&gt;--email&lt;/span&gt; &lt;span class="s2"&gt;"jane@company.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--company&lt;/code&gt; filter does a partial match against the &lt;code&gt;company_name&lt;/code&gt; field, which is handy when you don't know the exact legal name. The &lt;code&gt;--has-email&lt;/code&gt; flag is the one I reach for most — it strips out directory or auto-collected entries that have a name but no usable email, which is exactly the noise you want gone in a contact picker. Full options are at &lt;a href="https://cli.nylas.com/docs/commands/contacts-search" rel="noopener noreferrer"&gt;&lt;code&gt;contacts search&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Organize with contact groups
&lt;/h2&gt;

&lt;p&gt;Providers expose contact groups (Google calls them labels; Outlook calls them categories) and Nylas reads them through a dedicated endpoint. Listing groups gives you the IDs you then use to filter contacts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas contacts &lt;span class="nb"&gt;groups &lt;/span&gt;list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Over HTTP that's a &lt;code&gt;GET&lt;/code&gt; to the contact-groups collection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/grants/&amp;lt;GRANT_ID&amp;gt;/contacts/groups"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;NYLAS_API_KEY&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have a group ID, pass it as the &lt;code&gt;group&lt;/code&gt; filter on a contacts list or search call to get just that group's members. The &lt;a href="https://developer.nylas.com/docs/reference/api/contacts/list-contact-groups/" rel="noopener noreferrer"&gt;list contact groups&lt;/a&gt; reference and the &lt;a href="https://cli.nylas.com/docs/commands/contacts-groups-list" rel="noopener noreferrer"&gt;&lt;code&gt;contacts groups list&lt;/code&gt;&lt;/a&gt; command cover the rest. Groups are read-mapped from the provider, so the IDs are stable references you can store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Profile photos and sync
&lt;/h2&gt;

&lt;p&gt;Contacts often have a profile photo, which Nylas serves separately from the contact record so you only fetch image bytes when you need them. The CLI downloads a contact's photo to a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas contacts photo download &amp;lt;contact-id&amp;gt; &lt;span class="nt"&gt;--output&lt;/span&gt; ./jane.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://cli.nylas.com/docs/commands/contacts-photo-download" rel="noopener noreferrer"&gt;&lt;code&gt;contacts photo download&lt;/code&gt;&lt;/a&gt; command handles the fetch. There's also a &lt;code&gt;nylas contacts sync&lt;/code&gt; command that reports sync status for the account's contacts, which is useful when you've just connected a grant and want to know whether the initial contact sync has finished before you rely on the data being complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and provider notes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default page size&lt;/td&gt;
&lt;td&gt;50 contacts&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--limit&lt;/code&gt; auto-paginates above 200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sources&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;address_book&lt;/code&gt;, &lt;code&gt;inbox&lt;/code&gt;, &lt;code&gt;domain&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Filter deliberately — &lt;code&gt;inbox&lt;/code&gt; is auto-collected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Providers&lt;/td&gt;
&lt;td&gt;Google, Microsoft, and more&lt;/td&gt;
&lt;td&gt;One contact schema across all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-value fields&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;emails&lt;/code&gt;, &lt;code&gt;phone_numbers&lt;/code&gt; are arrays&lt;/td&gt;
&lt;td&gt;A contact can hold several of each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Groups&lt;/td&gt;
&lt;td&gt;Read-mapped from the provider&lt;/td&gt;
&lt;td&gt;Google labels / Outlook categories&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The biggest practical gotcha is the &lt;code&gt;inbox&lt;/code&gt; source. A user who emails a lot can accumulate thousands of auto-collected &lt;code&gt;inbox&lt;/code&gt; contacts they never deliberately saved, so a naive "show me all contacts" pulls in a long tail of one-off recipients. Decide up front which sources your feature should surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Contacts are deceptively simple — until you account for the three sources, the multi-value fields, and the fact that two providers model the same address book differently. Reading and writing them through one schema, with the source distinction made explicit, turns a fiddly multi-API integration into a handful of calls. The CLI gives you the same operations in the terminal, so you can inspect an address book and test a write before you build the feature around it.&lt;/p&gt;

&lt;p&gt;Where to go next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.nylas.com/docs/v3/email/contacts/" rel="noopener noreferrer"&gt;Contacts API overview&lt;/a&gt; — the full data model and the three sources&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.nylas.com/docs/reference/api/contacts/post-contact/" rel="noopener noreferrer"&gt;Create a contact&lt;/a&gt; and &lt;a href="https://developer.nylas.com/docs/reference/api/contacts/list-contact/" rel="noopener noreferrer"&gt;list contacts&lt;/a&gt; — endpoint references&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.nylas.com/docs/reference/api/contacts/list-contact-groups/" rel="noopener noreferrer"&gt;List contact groups&lt;/a&gt; — groups, labels, and categories&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cli.nylas.com/docs/commands" rel="noopener noreferrer"&gt;Nylas CLI command reference&lt;/a&gt; — every &lt;code&gt;nylas contacts&lt;/code&gt; subcommand&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://cli.nylas.com/authors/qasim-muhammad" rel="noopener noreferrer"&gt;Qasim Muhammad&lt;/a&gt; and &lt;a href="https://cli.nylas.com/authors/pouya-sanooei" rel="noopener noreferrer"&gt;Pouya Sanooei&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>devtools</category>
      <category>productivity</category>
      <category>contacts</category>
    </item>
    <item>
      <title>Record and transcribe meetings with the Nylas Notetaker API</title>
      <dc:creator>Qasim</dc:creator>
      <pubDate>Sun, 21 Jun 2026 21:27:02 +0000</pubDate>
      <link>https://dev.to/mqasimca/record-and-transcribe-meetings-with-the-nylas-notetaker-api-2emk</link>
      <guid>https://dev.to/mqasimca/record-and-transcribe-meetings-with-the-nylas-notetaker-api-2emk</guid>
      <description>&lt;p&gt;Meeting notes are the feature everyone wants and nobody wants to build. The hard part isn't the summary — an LLM handles that. The hard part is getting into the meeting: a bot that joins Zoom, Google Meet, and Microsoft Teams, survives each platform's waiting room and admission flow, records cleanly, and produces a transcript you can feed downstream. Each provider has its own join mechanics, and none of them ships a tidy "record this meeting" API.&lt;/p&gt;

&lt;p&gt;The Nylas Notetaker API is that bot as a service. You point it at a meeting link, it joins on schedule, records, and generates a transcript, and you fetch the recording and transcript through one endpoint. This post walks the Notetaker surface from the HTTP API and the &lt;a href="https://cli.nylas.com/docs/commands" rel="noopener noreferrer"&gt;Nylas CLI&lt;/a&gt;, which mirrors the whole lifecycle for terminal use and quick testing.&lt;/p&gt;

&lt;p&gt;I work on the CLI, so the terminal commands below are exactly what I run when I'm testing a notetaker against a live meeting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways to run a notetaker: grant-scoped or standalone
&lt;/h2&gt;

&lt;p&gt;Before any code, there's one architectural choice worth understanding, because it changes the endpoint you call.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;grant-scoped&lt;/strong&gt; notetaker is tied to a connected account and lives under &lt;code&gt;/v3/grants/{grant_id}/notetakers&lt;/code&gt;. Use it when the bot acts on behalf of a specific user — it can read that user's calendar and join their meetings as them.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;standalone&lt;/strong&gt; notetaker has no grant at all and lives under &lt;code&gt;/v3/notetakers&lt;/code&gt;. You hand it a raw meeting link and it joins, no connected account required. This is the one to reach for when you just have a URL and want a recording — a public webinar, a meeting on an account you haven't connected, or a system that deals in links rather than users.&lt;/p&gt;

&lt;p&gt;Same request body, same lifecycle, same media output; the only difference is whether there's a &lt;code&gt;grant_id&lt;/code&gt; in the path. See the &lt;a href="https://developer.nylas.com/docs/v3/notetaker/" rel="noopener noreferrer"&gt;Notetaker overview&lt;/a&gt; for how both models fit together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you begin
&lt;/h2&gt;

&lt;p&gt;You need a Nylas API key. If you're using a grant-scoped notetaker you also need a connected account; for standalone, the API key alone is enough. The CLI gets you started:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas init        &lt;span class="c"&gt;# create an account, generate an API key&lt;/span&gt;
nylas auth login  &lt;span class="c"&gt;# only needed for grant-scoped notetakers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The supported meeting providers are Zoom, Google Meet, and Microsoft Teams. Any meeting link from those three works as the target.&lt;/p&gt;

&lt;h2&gt;
  
  
  Send a notetaker into a meeting
&lt;/h2&gt;

&lt;p&gt;Creating a notetaker is one call. The only required field is the meeting link; everything else has a sensible default. The CLI joins immediately if you don't pass a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Join right now&lt;/span&gt;
nylas notetaker create &lt;span class="nt"&gt;--meeting-link&lt;/span&gt; &lt;span class="s2"&gt;"https://zoom.us/j/123456789"&lt;/span&gt;

&lt;span class="c"&gt;# Join at a specific time, with a custom display name&lt;/span&gt;
nylas notetaker create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--meeting-link&lt;/span&gt; &lt;span class="s2"&gt;"https://meet.google.com/abc-defg-hij"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--join-time&lt;/span&gt; &lt;span class="s2"&gt;"2026-06-23 14:00"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bot-name&lt;/span&gt; &lt;span class="s2"&gt;"Acme Notetaker"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--bot-name&lt;/code&gt; flag sets what the other participants see in the attendee list, which matters more than it sounds — a bot labeled "Acme Notetaker" reads as intentional, while a generic name makes people wonder who joined. The &lt;code&gt;--join-time&lt;/code&gt; flag accepts an absolute time, a relative offset like &lt;code&gt;30m&lt;/code&gt;, or natural phrasing like &lt;code&gt;tomorrow 9am&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Over HTTP, the request body fields are &lt;code&gt;meeting_link&lt;/code&gt; (required), &lt;code&gt;join_time&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, and &lt;code&gt;meeting_settings&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/grants/&amp;lt;GRANT_ID&amp;gt;/notetakers"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;NYLAS_API_KEY&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "meeting_link": "https://meet.google.com/abc-defg-hij",
    "name": "Acme Notetaker",
    "meeting_settings": {
      "video_recording": true,
      "audio_recording": true,
      "transcription": true
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a standalone notetaker, drop the grant from the path and POST the same body to &lt;code&gt;/v3/notetakers&lt;/code&gt;. The &lt;code&gt;meeting_settings&lt;/code&gt; object controls what gets captured — &lt;code&gt;video_recording&lt;/code&gt;, &lt;code&gt;audio_recording&lt;/code&gt;, and &lt;code&gt;transcription&lt;/code&gt; toggles. They aren't fully independent, though: if &lt;code&gt;transcription&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;, both &lt;code&gt;video_recording&lt;/code&gt; and &lt;code&gt;audio_recording&lt;/code&gt; must also be &lt;code&gt;true&lt;/code&gt;, because the transcript is generated from the recording. So "transcript only, no recording" isn't a valid combination — you can record audio-only, but a transcript always implies a recording exists.&lt;/p&gt;

&lt;p&gt;There's also a &lt;code&gt;transcription_settings&lt;/code&gt; object with an &lt;code&gt;expected_languages&lt;/code&gt; array. It's a language &lt;em&gt;declaration&lt;/em&gt;, not translation: the hints help the speech recognizer pick between the languages you expect in the meeting, and Notetaker never translates the transcript or forces it into one language. The full request body is in the &lt;a href="https://developer.nylas.com/docs/reference/api/notetaker/invite-notetaker/" rel="noopener noreferrer"&gt;invite notetaker&lt;/a&gt; reference, and the CLI flags are at &lt;a href="https://cli.nylas.com/docs/commands/notetaker-create" rel="noopener noreferrer"&gt;&lt;code&gt;notetaker create&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Follow the lifecycle
&lt;/h2&gt;

&lt;p&gt;A notetaker moves through states, and your app usually waits for it to finish before pulling media. The CLI's &lt;code&gt;--state&lt;/code&gt; filter accepts friendly names — &lt;code&gt;scheduled&lt;/code&gt; (created, waiting to join), &lt;code&gt;connecting&lt;/code&gt;, &lt;code&gt;attending&lt;/code&gt; (in the meeting, recording), &lt;code&gt;complete&lt;/code&gt; (done, media ready), &lt;code&gt;cancelled&lt;/code&gt;, and &lt;code&gt;failed&lt;/code&gt;. The raw API &lt;code&gt;state&lt;/code&gt; field is more granular: a notetaker object surfaces values like &lt;code&gt;waiting_for_entry&lt;/code&gt;, &lt;code&gt;processing&lt;/code&gt;, and &lt;code&gt;available&lt;/code&gt; (the API's equivalent of the CLI's "complete"). The list endpoint's &lt;code&gt;state&lt;/code&gt; query filter uses its own enum (for example &lt;code&gt;media_available&lt;/code&gt;), so don't assume the filter value and the object's &lt;code&gt;state&lt;/code&gt; string are spelled identically. List notetakers and filter by state to find the ones worth acting on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# All notetakers (20 by default)&lt;/span&gt;
nylas notetaker list

&lt;span class="c"&gt;# Only the finished ones, ready for media&lt;/span&gt;
nylas notetaker list &lt;span class="nt"&gt;--state&lt;/span&gt; &lt;span class="nb"&gt;complete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--state&lt;/code&gt; filter is how you build a polling loop: ask for &lt;code&gt;complete&lt;/code&gt; notetakers, process their media, move on. The CLI returns 20 by default and the &lt;a href="https://cli.nylas.com/docs/commands/notetaker-list" rel="noopener noreferrer"&gt;&lt;code&gt;notetaker list&lt;/code&gt;&lt;/a&gt; command documents the rest. To inspect a single notetaker's current state and metadata, use &lt;code&gt;nylas notetaker show &amp;lt;notetaker-id&amp;gt;&lt;/code&gt;. In production, rather than polling, you'd subscribe to notetaker webhooks so Nylas tells you when a session reaches &lt;code&gt;complete&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull the recording and transcript
&lt;/h2&gt;

&lt;p&gt;Once a notetaker is &lt;code&gt;complete&lt;/code&gt;, its media is ready. The media endpoint returns URLs for the recording (the video or audio file) and the transcript (the text). One thing to watch: these URLs expire, so download promptly rather than storing the URL and fetching it days later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas notetaker media &amp;lt;notetaker-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Over HTTP, it's a &lt;code&gt;GET&lt;/code&gt; against the notetaker's media path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/grants/&amp;lt;GRANT_ID&amp;gt;/notetakers/&amp;lt;NOTETAKER_ID&amp;gt;/media"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;NYLAS_API_KEY&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response gives you the download links; pull the bytes to your own storage immediately if you need them long-term. From there the transcript is plain text you can hand to an LLM for a summary, action items, or a searchable index. The &lt;a href="https://developer.nylas.com/docs/reference/api/notetaker/" rel="noopener noreferrer"&gt;get notetaker media&lt;/a&gt; reference and the &lt;a href="https://cli.nylas.com/docs/commands/notetaker-media" rel="noopener noreferrer"&gt;&lt;code&gt;notetaker media&lt;/code&gt;&lt;/a&gt; command cover the details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leave, cancel, or delete — they're different
&lt;/h2&gt;

&lt;p&gt;This is the distinction that catches people, because the three sound interchangeable and aren't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Leave&lt;/strong&gt; tells an &lt;em&gt;active&lt;/em&gt; notetaker to exit the meeting now. It stops the recording, triggers media generation, and keeps the notetaker record and its media. Use it to cleanly end a live recording early.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cancel&lt;/strong&gt; stops a &lt;em&gt;scheduled&lt;/em&gt; notetaker before it ever joins. There's nothing to record, so there's no media. The API exposes this as a dedicated cancel endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete&lt;/strong&gt; permanently removes the notetaker and all its media. If the notetaker is still scheduled or active, delete cancels it first, and any recording or transcript you haven't already downloaded is lost. In the CLI, &lt;code&gt;cancel&lt;/code&gt; and &lt;code&gt;rm&lt;/code&gt; are aliases of &lt;code&gt;delete&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# End a live recording but keep the media&lt;/span&gt;
nylas notetaker leave &amp;lt;notetaker-id&amp;gt;

&lt;span class="c"&gt;# Remove a notetaker and its media&lt;/span&gt;
nylas notetaker delete &amp;lt;notetaker-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reaching for &lt;code&gt;delete&lt;/code&gt; when you meant &lt;code&gt;leave&lt;/code&gt; permanently throws away the recording you just made, so it's worth getting right. The &lt;a href="https://cli.nylas.com/docs/commands/notetaker-delete" rel="noopener noreferrer"&gt;&lt;code&gt;notetaker delete&lt;/code&gt;&lt;/a&gt; command and the API's separate leave and cancel endpoints keep these operations distinct on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capabilities and notes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Providers&lt;/td&gt;
&lt;td&gt;Zoom, Google Meet, Microsoft Teams&lt;/td&gt;
&lt;td&gt;Any meeting link from these three&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Models&lt;/td&gt;
&lt;td&gt;Grant-scoped and standalone&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/v3/grants/{id}/notetakers&lt;/code&gt; vs &lt;code&gt;/v3/notetakers&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capture toggles&lt;/td&gt;
&lt;td&gt;video, audio, transcription&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;meeting_settings&lt;/code&gt; flags; transcription requires both recordings on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Media URLs&lt;/td&gt;
&lt;td&gt;Expiring&lt;/td&gt;
&lt;td&gt;Download promptly; store your own copy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifecycle&lt;/td&gt;
&lt;td&gt;scheduled → connecting → attending → complete&lt;/td&gt;
&lt;td&gt;Simplified CLI states; the API exposes more granular values&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The standalone model is the feature I'd point a new integration at first. Not needing a connected account to record a meeting removes the entire OAuth step for the common case where you already have a link, and you can always move to grant-scoped notetakers later when the bot needs to act as a specific user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The value here is that the genuinely hard part — a bot that reliably joins three different meeting platforms and comes back with clean media — is the part you don't write. You make one call with a meeting link, wait for &lt;code&gt;complete&lt;/code&gt;, and pull a recording and transcript. The summary layer on top is a solved problem; getting a trustworthy transcript out of a live meeting was the bottleneck, and that's what the API removes. The CLI mirrors the full lifecycle, so you can record a real meeting and inspect the transcript from your terminal before committing any of it to code.&lt;/p&gt;

&lt;p&gt;Where to go next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.nylas.com/docs/v3/notetaker/" rel="noopener noreferrer"&gt;Notetaker overview&lt;/a&gt; — grant-scoped and standalone models, lifecycle, and webhooks&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.nylas.com/docs/reference/api/notetaker/invite-notetaker/" rel="noopener noreferrer"&gt;Invite a notetaker&lt;/a&gt; — the full create request body&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.nylas.com/docs/reference/api/notetaker/" rel="noopener noreferrer"&gt;Notetaker API reference&lt;/a&gt; — media, leave, cancel, and list endpoints&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cli.nylas.com/docs/commands" rel="noopener noreferrer"&gt;Nylas CLI command reference&lt;/a&gt; — every &lt;code&gt;nylas notetaker&lt;/code&gt; subcommand&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://cli.nylas.com/authors/qasim-muhammad" rel="noopener noreferrer"&gt;Qasim Muhammad&lt;/a&gt; and &lt;a href="https://cli.nylas.com/authors/pouya-sanooei" rel="noopener noreferrer"&gt;Pouya Sanooei&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>ai</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Stop polling: real-time email and calendar webhooks with Nylas</title>
      <dc:creator>Qasim</dc:creator>
      <pubDate>Sun, 21 Jun 2026 21:22:34 +0000</pubDate>
      <link>https://dev.to/mqasimca/stop-polling-real-time-email-and-calendar-webhooks-with-nylas-534i</link>
      <guid>https://dev.to/mqasimca/stop-polling-real-time-email-and-calendar-webhooks-with-nylas-534i</guid>
      <description>&lt;p&gt;If your integration polls Nylas every minute to check for new email, you're doing too much work and still getting stale data. Polling is a tax: you burn rate limit on requests that mostly return nothing, and a message that arrives at 12:00:05 doesn't reach your app until the next poll. Webhooks flip that around. Nylas pushes a notification to your endpoint the moment something happens — a message arrives, an event changes, a contact is created — and your app reacts in real time.&lt;/p&gt;

&lt;p&gt;This post walks the webhook surface from both sides: the HTTP API that registers and manages webhooks, and the &lt;a href="https://cli.nylas.com/docs/commands" rel="noopener noreferrer"&gt;Nylas CLI&lt;/a&gt;, which has genuinely useful tooling for the part everyone gets stuck on — verifying signatures and testing webhooks against local code. I work on the CLI, so the terminal commands below are the ones I run when I'm wiring up a webhook receiver.&lt;/p&gt;

&lt;h2&gt;
  
  
  Triggers and destinations
&lt;/h2&gt;

&lt;p&gt;A webhook has two halves: the &lt;strong&gt;trigger types&lt;/strong&gt; it listens for and the &lt;strong&gt;destination URL&lt;/strong&gt; it pushes to. Trigger types are dotted event names like &lt;code&gt;message.created&lt;/code&gt;, &lt;code&gt;event.updated&lt;/code&gt;, and &lt;code&gt;contact.created&lt;/code&gt;, grouped into categories — grant, message, thread, event, contact, calendar, folder, and notetaker. You subscribe one destination to as many triggers as you want.&lt;/p&gt;

&lt;p&gt;The CLI lists every available trigger so you don't have to guess the names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# All trigger types&lt;/span&gt;
nylas webhook triggers

&lt;span class="c"&gt;# Only message-related triggers&lt;/span&gt;
nylas webhook triggers &lt;span class="nt"&gt;--category&lt;/span&gt; message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Webhooks are application-scoped, not grant-scoped: one webhook registered on your application receives notifications for every connected account, identified by the &lt;code&gt;grant_id&lt;/code&gt; in each payload. See the &lt;a href="https://developer.nylas.com/docs/v3/notifications/" rel="noopener noreferrer"&gt;notifications overview&lt;/a&gt; for the full event model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you begin
&lt;/h2&gt;

&lt;p&gt;You need a Nylas API key — webhook management is admin-level, so it uses the application's API key rather than a grant. You also need an HTTPS endpoint reachable from the public internet to receive the notifications. The CLI gets the key set up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas init  &lt;span class="c"&gt;# create an account, generate an API key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For local development you won't have a public HTTPS URL yet, which is exactly the problem the CLI's local server solves — more on that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a webhook
&lt;/h2&gt;

&lt;p&gt;Registering a webhook takes a destination URL and at least one trigger. The CLI maps these to &lt;code&gt;--url&lt;/code&gt; and &lt;code&gt;--triggers&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas webhook create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; https://yourapp.example.com/webhooks/nylas &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--triggers&lt;/span&gt; message.created,event.created,contact.created &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt; &lt;span class="s2"&gt;"Production receiver"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--notify&lt;/span&gt; admin@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--notify&lt;/code&gt; addresses get an email if the webhook starts failing, which is the kind of thing you want to know before your users do. Over HTTP, the request body fields are &lt;code&gt;webhook_url&lt;/code&gt;, &lt;code&gt;trigger_types&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, and &lt;code&gt;notification_email_addresses&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/webhooks"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;NYLAS_API_KEY&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "webhook_url": "https://yourapp.example.com/webhooks/nylas",
    "trigger_types": ["message.created", "event.created"],
    "description": "Production receiver",
    "notification_email_addresses": ["admin@example.com"]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the field name is &lt;code&gt;webhook_url&lt;/code&gt;, not &lt;code&gt;callback_url&lt;/code&gt;. The CLI flags are documented at &lt;a href="https://cli.nylas.com/docs/commands/webhook-create" rel="noopener noreferrer"&gt;&lt;code&gt;webhook create&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The challenge handshake
&lt;/h2&gt;

&lt;p&gt;Here's the first thing that trips people up. When you register a webhook (or reactivate one), Nylas immediately sends a &lt;code&gt;GET&lt;/code&gt; request to your URL with a &lt;code&gt;challenge&lt;/code&gt; query parameter, and your endpoint must echo the exact value back in a &lt;code&gt;200 OK&lt;/code&gt; within 10 seconds. If it doesn't, the webhook never activates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# What Nylas sends to verify your endpoint&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; GET &lt;span class="s1"&gt;'https://yourapp.example.com/webhooks/nylas?challenge=bc609b38-c81f-47fb-a275-1d9bd61a968b'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your handler has to return that &lt;code&gt;challenge&lt;/code&gt; string and nothing else — no quotation marks, no JSON wrapper, just the raw value. This GET-with-challenge step is separate from the actual event notifications, which arrive as &lt;code&gt;POST&lt;/code&gt; requests, so your endpoint needs to handle both methods. The &lt;a href="https://developer.nylas.com/docs/v3/notifications/" rel="noopener noreferrer"&gt;notifications overview&lt;/a&gt; documents the full handshake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify every webhook signature
&lt;/h2&gt;

&lt;p&gt;This is the part you cannot skip. Your webhook URL is public, so anyone who finds it can POST fake events at it. Nylas signs every notification so you can prove it's genuine: each request carries an &lt;code&gt;x-nylas-signature&lt;/code&gt; header containing a hex-encoded HMAC-SHA256 signature of the raw request body, signed with your endpoint's auto-generated &lt;code&gt;webhook_secret&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The critical detail is &lt;em&gt;raw&lt;/em&gt;: you compute the HMAC over the exact bytes Nylas sent, before any JSON re-parsing or reformatting. Pretty-print the body first and verification fails. The CLI's &lt;code&gt;verify&lt;/code&gt; command implements this correctly, which makes it a good oracle when you're debugging your own implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas webhook verify &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--payload-file&lt;/span&gt; ./raw-body.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--secret&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;WEBHOOK_SECRET&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--signature&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;x-nylas-signature header value&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your own code and the CLI disagree on a payload, your code is mangling the body before hashing — almost always the bug. The &lt;a href="https://cli.nylas.com/docs/commands/webhook-verify" rel="noopener noreferrer"&gt;&lt;code&gt;webhook verify&lt;/code&gt;&lt;/a&gt; command documents the flags. One more wrinkle: if you accept compressed payloads, compute the signature over the compressed bytes &lt;em&gt;before&lt;/em&gt; decompressing, or it won't match.&lt;/p&gt;

&lt;h2&gt;
  
  
  Develop locally with a tunnel
&lt;/h2&gt;

&lt;p&gt;The chicken-and-egg problem with webhooks is that you can't receive them on &lt;code&gt;localhost&lt;/code&gt;, but you don't want to deploy to production just to test a handler. The CLI's &lt;code&gt;server&lt;/code&gt; command solves this: it runs a local receiver and can expose it through a Cloudflare tunnel so real Nylas events reach your machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Local receiver behind a cloudflared tunnel, verifying signatures&lt;/span&gt;
nylas webhook server &lt;span class="nt"&gt;--tunnel&lt;/span&gt; cloudflared &lt;span class="nt"&gt;--secret&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;WEBHOOK_SECRET&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;--tunnel&lt;/code&gt; set, &lt;code&gt;--secret&lt;/code&gt; is required so the server verifies the HMAC signature on every incoming event — you don't want to process traffic from anyone who happens to hit the public tunnel URL. If you'd rather run loopback-only and drive it with local tooling, &lt;code&gt;--no-tunnel&lt;/code&gt; skips the tunnel and listens on localhost. The &lt;a href="https://cli.nylas.com/docs/commands/webhook-server" rel="noopener noreferrer"&gt;&lt;code&gt;webhook server&lt;/code&gt;&lt;/a&gt; command covers the options, and there's a full walkthrough in &lt;a href="https://developer.nylas.com/docs/v3/notifications/receive-webhooks-cli/" rel="noopener noreferrer"&gt;receive webhooks with the CLI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can also test without a live event at all. &lt;code&gt;nylas webhook test payload&lt;/code&gt; prints a mock payload for any trigger type so you can see the exact shape your handler will receive, and &lt;code&gt;nylas webhook test send&lt;/code&gt; posts a test event to a URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rotate the secret when it leaks
&lt;/h2&gt;

&lt;p&gt;If your &lt;code&gt;webhook_secret&lt;/code&gt; is ever exposed — committed to a repo, logged, leaked — rotate it. Rotating changes the signing key for future deliveries, so update your receiver with the new value before you resume trusting traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas webhook rotate-secret &amp;lt;webhook-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://cli.nylas.com/docs/commands/webhook-rotate-secret" rel="noopener noreferrer"&gt;&lt;code&gt;webhook rotate-secret&lt;/code&gt;&lt;/a&gt; command prints the new secret. Treat the secret like any other credential: out of source control, in your secrets manager, never logged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Acknowledge fast, or get retried
&lt;/h2&gt;

&lt;p&gt;Your endpoint must respond with &lt;code&gt;200 OK&lt;/code&gt; to each notification. If it doesn't, Nylas marks the webhook &lt;code&gt;failing&lt;/code&gt; and retries: it attempts delivery two more times for three total, backing off exponentially, with the final attempt landing 10–20 minutes after the first. After three failures it skips that notification type and keeps sending the others.&lt;/p&gt;

&lt;p&gt;Retries depend on the status code you return. Temporary signals like &lt;code&gt;429 Too Many Requests&lt;/code&gt; (respect the &lt;code&gt;Retry-After&lt;/code&gt; header) get retried; permanent ones like authentication or invalid-request errors don't, because retrying won't fix them. The practical rule: acknowledge the event with &lt;code&gt;200 OK&lt;/code&gt; immediately, then do the real processing asynchronously. If you run your handler's slow work before responding, you risk blowing past the timeout and triggering retries for events you actually received — which shows up as duplicate processing. Queue the payload, return 200, and work it off the queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to know
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;Application-level&lt;/td&gt;
&lt;td&gt;One webhook covers every connected grant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint&lt;/td&gt;
&lt;td&gt;HTTPS, public&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;localhost&lt;/code&gt; works only via a tunnel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Challenge&lt;/td&gt;
&lt;td&gt;Echo within 10 seconds&lt;/td&gt;
&lt;td&gt;Return the raw &lt;code&gt;challenge&lt;/code&gt; value, nothing else&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signature&lt;/td&gt;
&lt;td&gt;HMAC-SHA256, hex-encoded&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;x-nylas-signature&lt;/code&gt; header, signed over the raw body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trigger categories&lt;/td&gt;
&lt;td&gt;grant, message, thread, event, contact, calendar, folder, notetaker&lt;/td&gt;
&lt;td&gt;Subscribe one destination to many&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two things people get wrong are both above: failing the challenge handshake (so the webhook never activates) and verifying the signature against a reformatted body (so every event looks forged). Get those two right and the rest is just handling &lt;code&gt;POST&lt;/code&gt; requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Webhooks turn a polling loop that's always slightly behind into an event stream that's immediate, and they cost you far less rate limit while being more current. The setup has two real gotchas — the challenge echo and raw-body signature verification — but the CLI gives you a correct reference for both, plus a tunnel that lets you test against real events from your laptop. Build the receiver once, verify signatures on every request, and you've replaced a brittle poller with a push pipeline.&lt;/p&gt;

&lt;p&gt;Where to go next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.nylas.com/docs/v3/notifications/" rel="noopener noreferrer"&gt;Notifications overview&lt;/a&gt; — triggers, the challenge handshake, and signatures&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.nylas.com/docs/v3/notifications/receive-webhooks-cli/" rel="noopener noreferrer"&gt;Receive webhooks with the CLI&lt;/a&gt; — the local-tunnel development workflow&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.nylas.com/docs/reference/notifications/" rel="noopener noreferrer"&gt;Notifications reference&lt;/a&gt; — every webhook payload schema&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cli.nylas.com/docs/commands" rel="noopener noreferrer"&gt;Nylas CLI command reference&lt;/a&gt; — every &lt;code&gt;nylas webhook&lt;/code&gt; subcommand&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://cli.nylas.com/authors/qasim-muhammad" rel="noopener noreferrer"&gt;Qasim Muhammad&lt;/a&gt; and &lt;a href="https://cli.nylas.com/authors/pouya-sanooei" rel="noopener noreferrer"&gt;Pouya Sanooei&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webhooks</category>
      <category>api</category>
      <category>devtools</category>
      <category>security</category>
    </item>
    <item>
      <title>One calendar API for Google, Microsoft, and beyond: Nylas Calendar</title>
      <dc:creator>Qasim</dc:creator>
      <pubDate>Sun, 21 Jun 2026 21:21:49 +0000</pubDate>
      <link>https://dev.to/mqasimca/one-calendar-api-for-google-microsoft-and-beyond-nylas-calendar-46kk</link>
      <guid>https://dev.to/mqasimca/one-calendar-api-for-google-microsoft-and-beyond-nylas-calendar-46kk</guid>
      <description>&lt;p&gt;Scheduling features look simple until you build them. Google Calendar speaks its own REST API with &lt;code&gt;events.insert&lt;/code&gt;; Microsoft 365 wants Graph and &lt;code&gt;POST /me/calendar/events&lt;/code&gt;; Apple and a long tail of providers expect CalDAV. The moment your app needs to read a user's events, drop a meeting on their calendar, or check whether three people are free at 2pm, you're staring down three integrations that disagree on field names, time formats, and recurrence rules.&lt;/p&gt;

&lt;p&gt;The Nylas Calendar API gives you one interface over all of them. Connect a user's account once, get a &lt;code&gt;grant_id&lt;/code&gt;, and read calendars, manage events, send RSVPs, and compute free/busy with the same request shape whether the backing provider is Google or Microsoft. This post walks the calendar surface from both sides: the HTTP API your backend calls, and the &lt;a href="https://cli.nylas.com/docs/commands" rel="noopener noreferrer"&gt;Nylas CLI&lt;/a&gt; for testing the same operations in a terminal.&lt;/p&gt;

&lt;p&gt;I work on the CLI, so the terminal snippets below are the commands I actually run when I'm poking at a calendar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calendars, events, and the calendar_id
&lt;/h2&gt;

&lt;p&gt;A connected account has one or more &lt;strong&gt;calendars&lt;/strong&gt;, and every event belongs to exactly one of them. Most operations take a &lt;code&gt;calendar_id&lt;/code&gt;, and the special value &lt;code&gt;primary&lt;/code&gt; resolves to the account's default calendar — so you don't need to look up an ID to act on the main calendar. One exception: iCloud doesn't support &lt;code&gt;primary&lt;/code&gt;, so for iCloud accounts you pass a real calendar ID from &lt;code&gt;nylas calendar list&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;event&lt;/strong&gt; carries a &lt;code&gt;title&lt;/code&gt;, a &lt;code&gt;when&lt;/code&gt; object holding its start and end times, a list of &lt;code&gt;participants&lt;/code&gt;, an optional &lt;code&gt;location&lt;/code&gt;, and flags like &lt;code&gt;busy&lt;/code&gt;. That schema is identical across providers, which is the whole point: you read a Google event and a Microsoft event into the same struct. See the &lt;a href="https://developer.nylas.com/docs/v3/calendar/" rel="noopener noreferrer"&gt;Calendar API overview&lt;/a&gt; for how calendars, events, and availability fit together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you begin
&lt;/h2&gt;

&lt;p&gt;You need a Nylas API key and a connected account with calendar scopes. The CLI gets you there in two commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas init        &lt;span class="c"&gt;# create an account, generate an API key&lt;/span&gt;
nylas auth login  &lt;span class="c"&gt;# connect an account over OAuth, store the grant&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After login the CLI uses that grant as your default, so the &lt;code&gt;nylas calendar&lt;/code&gt; commands below run without an explicit ID. For the production OAuth flow and the calendar scopes Google and Microsoft require, see the &lt;a href="https://developer.nylas.com/docs/v3/auth/" rel="noopener noreferrer"&gt;authentication docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  List calendars and events
&lt;/h2&gt;

&lt;p&gt;Start by seeing what calendars the account has. The CLI lists them in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas calendar list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each row includes the calendar ID you'll pass to event operations. To list events, point at a calendar and a time window. The CLI defaults to the primary calendar and a short look-ahead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Upcoming events on the primary calendar&lt;/span&gt;
nylas calendar events list

&lt;span class="c"&gt;# Next 14 days, capped at 50 results&lt;/span&gt;
nylas calendar events list &lt;span class="nt"&gt;--days&lt;/span&gt; 14 &lt;span class="nt"&gt;--limit&lt;/span&gt; 50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same read over HTTP is a &lt;code&gt;GET&lt;/code&gt; against the events collection, and &lt;code&gt;calendar_id&lt;/code&gt; is a required query parameter. Pass &lt;code&gt;start&lt;/code&gt; and &lt;code&gt;end&lt;/code&gt; as Unix timestamps to bound the window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/grants/&amp;lt;GRANT_ID&amp;gt;/events?calendar_id=primary&amp;amp;start=1718841600&amp;amp;end=1719446400"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;NYLAS_API_KEY&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://developer.nylas.com/docs/reference/api/events/" rel="noopener noreferrer"&gt;&lt;code&gt;GET /events&lt;/code&gt;&lt;/a&gt; reference documents the full filter set — by calendar, time range, attendee, and more. The CLI equivalents are at &lt;a href="https://cli.nylas.com/docs/commands/calendar-events-list" rel="noopener noreferrer"&gt;&lt;code&gt;calendar events list&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create an event with participants
&lt;/h2&gt;

&lt;p&gt;Creating an event is a single &lt;code&gt;POST&lt;/code&gt;. The only strictly required pieces are the &lt;code&gt;calendar_id&lt;/code&gt; query parameter and a &lt;code&gt;when&lt;/code&gt; object in the body — &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;participants&lt;/code&gt; are optional, though you'll almost always set them. The CLI's &lt;code&gt;events create&lt;/code&gt; takes start and end times in &lt;code&gt;YYYY-MM-DD HH:MM&lt;/code&gt; form (or a bare &lt;code&gt;YYYY-MM-DD&lt;/code&gt; for an all-day event):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas calendar events create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"Design review"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--start&lt;/span&gt; &lt;span class="s2"&gt;"2026-06-23 14:00"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--end&lt;/span&gt; &lt;span class="s2"&gt;"2026-06-23 15:00"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--participant&lt;/span&gt; alice@example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--participant&lt;/span&gt; bob@example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s2"&gt;"Zoom"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Over HTTP, the &lt;code&gt;when&lt;/code&gt; object has a few shapes; the one used here is a timespan that takes &lt;code&gt;start_time&lt;/code&gt; and &lt;code&gt;end_time&lt;/code&gt; as Unix timestamps (the others are a single &lt;code&gt;time&lt;/code&gt;, a &lt;code&gt;date&lt;/code&gt;, and a &lt;code&gt;datespan&lt;/code&gt;). Each participant is an object with an email:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/grants/&amp;lt;GRANT_ID&amp;gt;/events?calendar_id=primary"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;NYLAS_API_KEY&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "title": "Design review",
    "when": { "start_time": 1718901000, "end_time": 1718904600 },
    "participants": [
      { "email": "alice@example.com" },
      { "email": "bob@example.com" }
    ],
    "location": "Zoom"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you add participants, the provider sends them invitations and they show up as normal calendar invites in Gmail, Outlook, or Apple Calendar. One field worth knowing is &lt;code&gt;conferencing&lt;/code&gt;: set it and Nylas can auto-create a video link (Google Meet, Teams, or Zoom depending on the provider) so you don't have to mint one yourself. The full request body — &lt;code&gt;busy&lt;/code&gt;, &lt;code&gt;recurrence&lt;/code&gt;, &lt;code&gt;conferencing&lt;/code&gt;, &lt;code&gt;metadata&lt;/code&gt; — is in the &lt;a href="https://developer.nylas.com/docs/reference/api/events/create-event/" rel="noopener noreferrer"&gt;create event&lt;/a&gt; reference, and the CLI flags are at &lt;a href="https://cli.nylas.com/docs/commands/calendar-events-create" rel="noopener noreferrer"&gt;&lt;code&gt;calendar events create&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  RSVP to invitations
&lt;/h2&gt;

&lt;p&gt;When the connected account is invited to an event, you can respond programmatically. The RSVP status is one of exactly three values — &lt;code&gt;yes&lt;/code&gt;, &lt;code&gt;no&lt;/code&gt;, or &lt;code&gt;maybe&lt;/code&gt; — and Nylas sends it to the event organizer as an email update, the same notification you'd trigger by clicking the button in a mail client.&lt;/p&gt;

&lt;p&gt;From the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas calendar events rsvp &amp;lt;event-id&amp;gt; &lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="nt"&gt;--comment&lt;/span&gt; &lt;span class="s2"&gt;"See you there"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Over HTTP, it's a &lt;code&gt;POST&lt;/code&gt; to the event's send-rsvp endpoint with a &lt;code&gt;status&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/grants/&amp;lt;GRANT_ID&amp;gt;/events/&amp;lt;EVENT_ID&amp;gt;/send-rsvp?calendar_id=primary"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;NYLAS_API_KEY&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{ "status": "maybe" }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a provider quirk worth flagging here, straight from the API docs: due to a Microsoft Graph limitation, declining ("no") might not update the event status properly on Microsoft accounts — the event stays on the calendar with the no-RSVP status rather than being removed. If your app relies on RSVP state for Microsoft users, test that path explicitly. The &lt;a href="https://developer.nylas.com/docs/reference/api/events/send-rsvp/" rel="noopener noreferrer"&gt;send RSVP&lt;/a&gt; reference and the &lt;a href="https://cli.nylas.com/docs/commands/calendar-events-rsvp" rel="noopener noreferrer"&gt;&lt;code&gt;calendar events rsvp&lt;/code&gt;&lt;/a&gt; command document the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compute availability across calendars
&lt;/h2&gt;

&lt;p&gt;This is the operation that's genuinely painful to build yourself, because it means reading several people's free/busy data from potentially different providers and intersecting it. The Calendar API does the intersection for you. &lt;code&gt;POST /availability&lt;/code&gt; takes a list of participants, a time range, and a meeting duration, and returns only the slots when everyone is free.&lt;/p&gt;

&lt;p&gt;The endpoint is &lt;code&gt;POST /v3/calendars/availability&lt;/code&gt;, and the four required fields are &lt;code&gt;participants&lt;/code&gt;, &lt;code&gt;start_time&lt;/code&gt;, &lt;code&gt;end_time&lt;/code&gt;, and &lt;code&gt;duration_minutes&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/calendars/availability"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;NYLAS_API_KEY&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "start_time": 1718841600,
    "end_time": 1719446400,
    "duration_minutes": 30,
    "participants": [
      { "email": "alice@example.com" },
      { "email": "bob@example.com" }
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response is the set of 30-minute windows where all participants are free across the whole range. The CLI exposes the same computation through &lt;a href="https://cli.nylas.com/docs/commands/calendar-availability-check" rel="noopener noreferrer"&gt;&lt;code&gt;calendar availability check&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas calendar availability check &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--emails&lt;/span&gt; alice@example.com,bob@example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--start&lt;/span&gt; &lt;span class="s2"&gt;"tomorrow 9am"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--end&lt;/span&gt; &lt;span class="s2"&gt;"tomorrow 5pm"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single endpoint replaces the read-everyone, normalize-timezones, intersect-the-gaps logic you'd otherwise write by hand. The &lt;a href="https://developer.nylas.com/docs/reference/api/availability/" rel="noopener noreferrer"&gt;availability reference&lt;/a&gt; covers group availability, round-robin, and open-hours constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find the best time, not just any time
&lt;/h2&gt;

&lt;p&gt;Free/busy tells you when people &lt;em&gt;can&lt;/em&gt; meet. It doesn't tell you when they &lt;em&gt;should&lt;/em&gt;. A slot at 7am for someone in Berlin and 10pm for someone in Tokyo is technically "free" but a terrible choice. The CLI's &lt;code&gt;find-time&lt;/code&gt; command layers a scoring model on top of availability to rank candidate slots.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas calendar find-time &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--participants&lt;/span&gt; alice@example.com,bob@example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--duration&lt;/span&gt; 1h &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--days&lt;/span&gt; 7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It scores each candidate out of 100 points across five factors: working hours (40 points — is everyone inside 9-to-5?), time quality (25 points — morning versus late afternoon), cultural norms (15 points — avoid Friday afternoons and the lunch hour), weekday preference (10 points — mid-week beats Monday), and holiday avoidance (10 points). You can override the working window with &lt;code&gt;--working-start&lt;/code&gt; and &lt;code&gt;--working-end&lt;/code&gt; and pass per-participant timezones with &lt;code&gt;--timezones&lt;/code&gt;. Full flags are at &lt;a href="https://cli.nylas.com/docs/commands/calendar-find-time" rel="noopener noreferrer"&gt;&lt;code&gt;calendar find-time&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recurring events and limits
&lt;/h2&gt;

&lt;p&gt;Recurring events use the standard iCalendar &lt;code&gt;RRULE&lt;/code&gt; format in the event's &lt;code&gt;recurrence&lt;/code&gt; field, so a weekly standup is one event with a recurrence rule rather than 52 separate events. The CLI groups recurring-event operations under &lt;code&gt;nylas calendar recurring&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Providers&lt;/td&gt;
&lt;td&gt;Google Calendar, Microsoft 365, and more&lt;/td&gt;
&lt;td&gt;One event schema across all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RSVP statuses&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;yes&lt;/code&gt;, &lt;code&gt;no&lt;/code&gt;, &lt;code&gt;maybe&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Microsoft Graph may not apply a "no" cleanly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default calendar&lt;/td&gt;
&lt;td&gt;&lt;code&gt;calendar_id=primary&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Resolves to the account's main calendar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time format&lt;/td&gt;
&lt;td&gt;Unix timestamps&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;when&lt;/code&gt; object holds &lt;code&gt;start_time&lt;/code&gt; / &lt;code&gt;end_time&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recurrence&lt;/td&gt;
&lt;td&gt;iCalendar &lt;code&gt;RRULE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;One event carries the whole series&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The provider differences that survive the abstraction are narrow: the Microsoft RSVP quirk above, and the fact that auto-conferencing creates a Meet link on Google and a Teams link on Microsoft. Everything else — reading events, creating them, computing availability — is one code path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Calendar work is where provider fragmentation hurts most, because availability and invitations genuinely require reading and writing across accounts. Doing it once through one schema, instead of once per provider, is the difference between a feature you ship this week and one you maintain forever. The CLI mirrors every operation, so you can prove out a scheduling flow in the terminal before writing a line of backend code.&lt;/p&gt;

&lt;p&gt;Where to go next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.nylas.com/docs/v3/calendar/" rel="noopener noreferrer"&gt;Calendar API overview&lt;/a&gt; — calendars, events, and availability concepts&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.nylas.com/docs/reference/api/events/create-event/" rel="noopener noreferrer"&gt;Create an event&lt;/a&gt; and &lt;a href="https://developer.nylas.com/docs/reference/api/events/send-rsvp/" rel="noopener noreferrer"&gt;send RSVP&lt;/a&gt; — endpoint references&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.nylas.com/docs/reference/api/availability/" rel="noopener noreferrer"&gt;Availability API&lt;/a&gt; — free/busy and group scheduling&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cli.nylas.com/docs/commands" rel="noopener noreferrer"&gt;Nylas CLI command reference&lt;/a&gt; — every &lt;code&gt;nylas calendar&lt;/code&gt; subcommand&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://cli.nylas.com/authors/qasim-muhammad" rel="noopener noreferrer"&gt;Qasim Muhammad&lt;/a&gt; and &lt;a href="https://cli.nylas.com/authors/pouya-sanooei" rel="noopener noreferrer"&gt;Pouya Sanooei&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>calendar</category>
      <category>api</category>
      <category>devtools</category>
      <category>scheduling</category>
    </item>
    <item>
      <title>Read and send email from one API across every provider: Nylas Email</title>
      <dc:creator>Qasim</dc:creator>
      <pubDate>Sun, 21 Jun 2026 21:21:19 +0000</pubDate>
      <link>https://dev.to/mqasimca/read-and-send-email-from-one-api-across-every-provider-nylas-email-2kga</link>
      <guid>https://dev.to/mqasimca/read-and-send-email-from-one-api-across-every-provider-nylas-email-2kga</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyblfqndmbjpbsl3k4zpv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyblfqndmbjpbsl3k4zpv.png" alt="Read and send email from one API across every provider: Nylas Email" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every email provider speaks a different dialect. Gmail has labels and the Gmail API; Microsoft 365 has folders and Graph; Yahoo and iCloud want IMAP with app passwords; Exchange wants EWS. Build email into your product the obvious way and you end up maintaining four or five integrations that drift apart over time, each with its own auth, its own pagination, and its own quirks.&lt;/p&gt;

&lt;p&gt;The Nylas Email API collapses that into one interface. You connect a user's mailbox once, get a &lt;code&gt;grant_id&lt;/code&gt;, and from then on every message, thread, folder, and attachment comes back in the same shape no matter who hosts the mailbox. This post is a working tour of that surface from two angles: the HTTP API for your backend, and the &lt;a href="https://cli.nylas.com/docs/commands" rel="noopener noreferrer"&gt;Nylas CLI&lt;/a&gt; for the terminal, scripts, and quick experiments.&lt;/p&gt;

&lt;p&gt;I work on the CLI, so the terminal examples below are the exact commands I reach for.&lt;/p&gt;

&lt;p&gt;I'll cover the operations you actually reach for day to day — list, read, search, send, reply, and draft — and call out the provider differences worth knowing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The grant: one connection, every provider
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;grant&lt;/strong&gt; is an authenticated connection to a single user's mailbox. You create it once through an OAuth flow (or Bring Your Own Auth), and Nylas hands you a &lt;code&gt;grant_id&lt;/code&gt; that you pass on every request. That ID is the only provider-specific thing you deal with — the rest of the API is identical across Gmail, Microsoft 365, Exchange, Yahoo, iCloud, and IMAP.&lt;/p&gt;

&lt;p&gt;Because the grant abstracts the provider, code you write against one mailbox works unchanged against all of them. A &lt;code&gt;GET /messages&lt;/code&gt; against a Gmail grant and the same call against an iCloud grant return the same JSON fields. See the &lt;a href="https://developer.nylas.com/docs/v3/email/" rel="noopener noreferrer"&gt;Email API overview&lt;/a&gt; for the full concept map of messages, threads, folders, and attachments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you begin
&lt;/h2&gt;

&lt;p&gt;You need a Nylas API key and at least one connected account. The fastest setup is the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas init        &lt;span class="c"&gt;# creates an account and generates an API key&lt;/span&gt;
nylas auth login  &lt;span class="c"&gt;# connect a mailbox over OAuth and store its grant&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After &lt;code&gt;nylas auth login&lt;/code&gt;, the CLI stores the grant as your default account, so every &lt;code&gt;nylas email&lt;/code&gt; command below runs against it without you passing an ID. List what's connected anytime with &lt;code&gt;nylas auth list&lt;/code&gt;. For the full authentication flow your app uses in production — OAuth scopes, redirect URIs, hosted auth — see the &lt;a href="https://developer.nylas.com/docs/v3/auth/" rel="noopener noreferrer"&gt;authentication docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  List and read messages
&lt;/h2&gt;

&lt;p&gt;Reading mail is the first thing most integrations do. The CLI lists your inbox in one command and defaults to the 10 most recent messages from &lt;code&gt;INBOX&lt;/code&gt; only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 10 most recent inbox messages&lt;/span&gt;
nylas email list

&lt;span class="c"&gt;# Only unread, from a specific sender&lt;/span&gt;
nylas email list &lt;span class="nt"&gt;--unread&lt;/span&gt; &lt;span class="nt"&gt;--from&lt;/span&gt; boss@company.com

&lt;span class="c"&gt;# Everything across all folders, paginated automatically&lt;/span&gt;
nylas email list &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--max&lt;/span&gt; 500 &lt;span class="nt"&gt;--all-folders&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--all&lt;/code&gt; flag walks pagination for you and &lt;code&gt;--max&lt;/code&gt; caps the total, which matters because a busy mailbox can hold hundreds of thousands of messages. Full flag reference lives at &lt;a href="https://cli.nylas.com/docs/commands/email-list" rel="noopener noreferrer"&gt;&lt;code&gt;email list&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The same operation over HTTP is a &lt;code&gt;GET&lt;/code&gt; against the grant's messages collection. The default page size is 50 and the maximum is 200; pass the &lt;code&gt;next_cursor&lt;/code&gt; value from each response back as the &lt;code&gt;page_token&lt;/code&gt; query parameter to fetch the next page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/grants/&amp;lt;GRANT_ID&amp;gt;/messages?limit=5&amp;amp;unread=true"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;NYLAS_API_KEY&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To open a single message and mark it read in one step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas email &lt;span class="nb"&gt;read&lt;/span&gt; &amp;lt;message-id&amp;gt; &lt;span class="nt"&gt;--mark-read&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://developer.nylas.com/docs/reference/api/messages/get-messages/" rel="noopener noreferrer"&gt;&lt;code&gt;GET /messages&lt;/code&gt;&lt;/a&gt; reference lists every query parameter — sender, recipient, subject, unread state, starred, thread, and received-date ranges — so you can filter server-side instead of pulling everything and filtering in your app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search your mailbox
&lt;/h2&gt;

&lt;p&gt;Filtering by exact fields covers a lot, but sometimes you need a free-text query. The CLI's &lt;code&gt;search&lt;/code&gt; runs a query string against the provider's own search engine and layers structured filters on top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Free-text subject and body search&lt;/span&gt;
nylas email search &lt;span class="s2"&gt;"project update"&lt;/span&gt;

&lt;span class="c"&gt;# Any subject from one sender, with attachments, in a date window&lt;/span&gt;
nylas email search &lt;span class="s2"&gt;"*"&lt;/span&gt; &lt;span class="nt"&gt;--from&lt;/span&gt; &lt;span class="s2"&gt;"hr@company.com"&lt;/span&gt; &lt;span class="nt"&gt;--has-attachment&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--after&lt;/span&gt; 2024-01-01 &lt;span class="nt"&gt;--before&lt;/span&gt; 2024-12-31
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;*&lt;/code&gt; as the query with a &lt;code&gt;--from&lt;/code&gt; filter is the idiom for "every message from this sender regardless of content." The &lt;a href="https://cli.nylas.com/docs/commands/email-search" rel="noopener noreferrer"&gt;&lt;code&gt;email search&lt;/code&gt;&lt;/a&gt; command documents the &lt;code&gt;--in&lt;/code&gt;, &lt;code&gt;--to&lt;/code&gt;, &lt;code&gt;--subject&lt;/code&gt;, and &lt;code&gt;--starred&lt;/code&gt; filters. Search returns 20 results by default, and only auto-paginates across multiple pages once you raise &lt;code&gt;--limit&lt;/code&gt; above 200.&lt;/p&gt;

&lt;p&gt;One thing worth knowing: search semantics are the provider's, not Nylas's. Gmail's search is more forgiving and operator-rich than a bare IMAP search, so the same query can return slightly different relevance across providers. For deterministic results, prefer the structured filters on &lt;code&gt;GET /messages&lt;/code&gt; over free-text search.&lt;/p&gt;

&lt;h2&gt;
  
  
  Send email
&lt;/h2&gt;

&lt;p&gt;Sending is one request, and the recipient sees a normal message from the connected mailbox — sent through the user's own provider, landing in their real Sent folder. From the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas email send &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--to&lt;/span&gt; user@example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subject&lt;/span&gt; &lt;span class="s2"&gt;"Hello from Nylas"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s2"&gt;"This went out through the connected mailbox."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same call over HTTP hits &lt;a href="https://developer.nylas.com/docs/reference/api/messages/send-message/" rel="noopener noreferrer"&gt;&lt;code&gt;POST /messages/send&lt;/code&gt;&lt;/a&gt;. The request body accepts &lt;code&gt;to&lt;/code&gt;, &lt;code&gt;cc&lt;/code&gt;, &lt;code&gt;bcc&lt;/code&gt;, &lt;code&gt;subject&lt;/code&gt;, &lt;code&gt;body&lt;/code&gt;, &lt;code&gt;from&lt;/code&gt;, &lt;code&gt;reply_to&lt;/code&gt;, &lt;code&gt;attachments&lt;/code&gt;, and more:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/grants/&amp;lt;GRANT_ID&amp;gt;/messages/send"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;NYLAS_API_KEY&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "subject": "Q3 report",
    "body": "&amp;lt;p&amp;gt;Attached is the report.&amp;lt;/p&amp;gt;",
    "to": [{ "email": "finance@example.com", "name": "Finance" }],
    "cc": [{ "email": "lead@example.com" }]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few capabilities that turn a basic send into a production one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled send.&lt;/strong&gt; Pass a &lt;code&gt;send_at&lt;/code&gt; Unix timestamp in the API, or use the CLI's human-friendly &lt;code&gt;--schedule&lt;/code&gt; flag: &lt;code&gt;nylas email send --to a@b.com --subject Hi --body "later" --schedule "tomorrow 9am"&lt;/code&gt;. Manage queued sends with &lt;code&gt;nylas email scheduled list&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open and link tracking.&lt;/strong&gt; Set &lt;code&gt;tracking_options&lt;/code&gt; in the request, or &lt;code&gt;--track-opens&lt;/code&gt; and &lt;code&gt;--track-links&lt;/code&gt; on the CLI, to record when recipients open mail or click links. See &lt;a href="https://developer.nylas.com/docs/v3/email/message-tracking/" rel="noopener noreferrer"&gt;message tracking&lt;/a&gt; for how the pixel and link-rewrite work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotent send.&lt;/strong&gt; Retrying a failed send risks sending twice. Nylas supports an idempotency key so a retried request is de-duplicated server-side — see &lt;a href="https://developer.nylas.com/docs/v3/email/idempotent-send/" rel="noopener noreferrer"&gt;idempotent send&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The CLI's &lt;a href="https://cli.nylas.com/docs/commands/email-send" rel="noopener noreferrer"&gt;&lt;code&gt;email send&lt;/code&gt;&lt;/a&gt; command also supports GPG signing and encryption and hosted templates, which are handy for transactional mail that needs consistent branding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reply and keep the thread intact
&lt;/h2&gt;

&lt;p&gt;A reply is not just a send with &lt;code&gt;Re:&lt;/code&gt; in the subject. To make mail clients group the reply with the original conversation, you have to set the in-reply-to relationship. The API field is &lt;code&gt;reply_to_message_id&lt;/code&gt;; set it to the message you're answering and Nylas wires up the threading headers.&lt;/p&gt;

&lt;p&gt;The CLI's &lt;code&gt;reply&lt;/code&gt; command does this for you. It fetches the original to populate the recipient and subject, preserves threading through &lt;code&gt;reply_to_message_id&lt;/code&gt;, and replies only to the original sender unless you add &lt;code&gt;--all&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Reply to the sender, in-thread&lt;/span&gt;
nylas email reply &amp;lt;message-id&amp;gt; &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s2"&gt;"Sounds good, thanks!"&lt;/span&gt;

&lt;span class="c"&gt;# Reply to everyone on the thread&lt;/span&gt;
nylas email reply &amp;lt;message-id&amp;gt; &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s2"&gt;"Looping everyone in."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the biggest correctness trap in email integrations: send a "reply" without the threading header and it shows up as a brand-new conversation, which looks broken to the recipient. Always thread your replies. For how threads behave across providers — Gmail groups by conversation, others by header chain — see &lt;a href="https://developer.nylas.com/docs/cookbook/email/email-threading-explained/" rel="noopener noreferrer"&gt;email threading explained&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compose drafts before sending
&lt;/h2&gt;

&lt;p&gt;When a human (or an approval step) reviews mail before it goes out, draft first and send later. Drafts persist server-side in the user's mailbox, so they sync to the provider's Drafts folder and show up in the user's normal mail client too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas email drafts create &lt;span class="nt"&gt;--to&lt;/span&gt; user@example.com &lt;span class="nt"&gt;--subject&lt;/span&gt; &lt;span class="s2"&gt;"WIP"&lt;/span&gt; &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s2"&gt;"Draft body"&lt;/span&gt;
nylas email drafts list
nylas email drafts send &amp;lt;draft-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://developer.nylas.com/docs/reference/api/drafts/" rel="noopener noreferrer"&gt;Drafts API&lt;/a&gt; mirrors this — create, update, list, and send — and the CLI commands are documented at &lt;a href="https://cli.nylas.com/docs/commands/email-drafts-create" rel="noopener noreferrer"&gt;&lt;code&gt;email drafts create&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://cli.nylas.com/docs/commands/email-drafts-send" rel="noopener noreferrer"&gt;&lt;code&gt;email drafts send&lt;/code&gt;&lt;/a&gt;. Drafting is also the right pattern for an AI agent that proposes replies: generate the draft, route it through human approval, then send the approved draft by ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle attachments
&lt;/h2&gt;

&lt;p&gt;Attachments ride on the message object as metadata; you download the bytes separately by attachment ID. Nylas accepts up to 25 MB inline through multipart on send, or up to 150 MB for Microsoft grants through the attachment-uploads flow. From the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas email attachments list &amp;lt;message-id&amp;gt;
nylas email attachments download &amp;lt;attachment-id&amp;gt; &amp;lt;message-id&amp;gt; &lt;span class="nt"&gt;--output&lt;/span&gt; ./file.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The size ceiling is the one provider difference you can't ignore here: a 40 MB attachment that sails through a Microsoft grant will be rejected on a provider capped at 25 MB. Check the limit before you build a feature around large files. See &lt;a href="https://developer.nylas.com/docs/v3/email/attachments/" rel="noopener noreferrer"&gt;attachments&lt;/a&gt; for the upload-session flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and provider notes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default page size&lt;/td&gt;
&lt;td&gt;50 messages&lt;/td&gt;
&lt;td&gt;Maximum 200 per page; use &lt;code&gt;page_token&lt;/code&gt; to paginate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inline attachment size&lt;/td&gt;
&lt;td&gt;25 MB&lt;/td&gt;
&lt;td&gt;Up to 150 MB for Microsoft grants via upload sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Providers&lt;/td&gt;
&lt;td&gt;Gmail, Microsoft 365, Exchange, Yahoo, iCloud, IMAP&lt;/td&gt;
&lt;td&gt;One schema across all of them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Folders vs labels&lt;/td&gt;
&lt;td&gt;Provider-specific&lt;/td&gt;
&lt;td&gt;Gmail models folders as labels, so a message can be in several at once&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are the defaults you'll hit first; rate limits and quotas vary by provider and plan. Gmail in particular enforces its own per-user sending and API quotas underneath Nylas — see &lt;a href="https://developer.nylas.com/docs/cookbook/email/gmail-api-quotas/" rel="noopener noreferrer"&gt;Gmail API quotas&lt;/a&gt; before you scale outbound volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The win with the Email API is that you write the integration once. List, search, send, reply, and draft are the same five calls whether the mailbox is Gmail or IMAP, and the CLI gives you the same operations in the terminal for testing and automation before you commit them to code. The provider differences that remain — search relevance, attachment ceilings, folders-as-labels — are few enough to enumerate, which is the whole point.&lt;/p&gt;

&lt;p&gt;Where to go next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.nylas.com/docs/v3/email/" rel="noopener noreferrer"&gt;Email API overview&lt;/a&gt; — the full concept map and capabilities&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.nylas.com/docs/reference/api/messages/send-message/" rel="noopener noreferrer"&gt;Send a message&lt;/a&gt; and &lt;a href="https://developer.nylas.com/docs/reference/api/messages/get-messages/" rel="noopener noreferrer"&gt;list messages&lt;/a&gt; — endpoint references&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cli.nylas.com/docs/commands" rel="noopener noreferrer"&gt;Nylas CLI command reference&lt;/a&gt; — every &lt;code&gt;nylas email&lt;/code&gt; subcommand&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.nylas.com/docs/cookbook/email/email-threading-explained/" rel="noopener noreferrer"&gt;Email threading explained&lt;/a&gt; — keep replies in the right conversation&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://cli.nylas.com/authors/qasim-muhammad" rel="noopener noreferrer"&gt;Qasim Muhammad&lt;/a&gt; and &lt;a href="https://cli.nylas.com/authors/pouya-sanooei" rel="noopener noreferrer"&gt;Pouya Sanooei&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>email</category>
      <category>api</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Give your AI agent its own inbox</title>
      <dc:creator>Qasim</dc:creator>
      <pubDate>Sun, 21 Jun 2026 19:10:48 +0000</pubDate>
      <link>https://dev.to/mqasimca/give-your-ai-agent-its-own-inbox-4bo1</link>
      <guid>https://dev.to/mqasimca/give-your-ai-agent-its-own-inbox-4bo1</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/mqasimca/give-your-ai-agent-its-own-inbox-nylas-agent-accounts-via-api-and-cli-1c5o" class="crayons-story__hidden-navigation-link"&gt;Give your AI agent its own inbox: Nylas Agent Accounts via API and CLI&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/mqasimca" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3995627%2Feddc44d6-3e99-45b8-ae80-71279c900b01.jpg" alt="mqasimca profile" class="crayons-avatar__image" width="96" height="96"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/mqasimca" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Qasim
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Qasim
                
              
              &lt;div id="story-author-preview-content-3957216" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/mqasimca" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3995627%2Feddc44d6-3e99-45b8-ae80-71279c900b01.jpg" class="crayons-avatar__image" alt="" width="96" height="96"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Qasim&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/mqasimca/give-your-ai-agent-its-own-inbox-nylas-agent-accounts-via-api-and-cli-1c5o" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 21&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/mqasimca/give-your-ai-agent-its-own-inbox-nylas-agent-accounts-via-api-and-cli-1c5o" id="article-link-3957216"&gt;
          Give your AI agent its own inbox: Nylas Agent Accounts via API and CLI
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/email"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;email&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/api"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;api&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devtools"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devtools&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/mqasimca/give-your-ai-agent-its-own-inbox-nylas-agent-accounts-via-api-and-cli-1c5o#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            6 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Give your AI agent its own inbox: Nylas Agent Accounts via API and CLI</title>
      <dc:creator>Qasim</dc:creator>
      <pubDate>Sun, 21 Jun 2026 18:41:28 +0000</pubDate>
      <link>https://dev.to/mqasimca/give-your-ai-agent-its-own-inbox-nylas-agent-accounts-via-api-and-cli-1c5o</link>
      <guid>https://dev.to/mqasimca/give-your-ai-agent-its-own-inbox-nylas-agent-accounts-via-api-and-cli-1c5o</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5kzx38scflepr3wqgm9z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5kzx38scflepr3wqgm9z.png" alt="Nylas Agent Accounts via API and CLI" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most "AI email" demos point a model at a human's inbox over OAuth. That's fine until you want the agent to &lt;em&gt;be&lt;/em&gt; a participant — to have its own address that people reply to, that calendars invite, and that builds its own sender reputation. Pointing at someone's personal inbox doesn't give you that.&lt;/p&gt;

&lt;p&gt;Nylas &lt;strong&gt;Agent Accounts&lt;/strong&gt; take the other approach: a real &lt;code&gt;name@yourdomain.com&lt;/code&gt; mailbox and calendar that the agent owns end to end. It sends, receives, hosts events, and RSVPs — and to anyone on the other side it's indistinguishable from a human-operated account. It went GA in June 2026.&lt;/p&gt;

&lt;p&gt;The part I like as an SRE: it's not a new API surface. An Agent Account is just another Nylas &lt;a href="https://developer.nylas.com/docs/v3/auth/" rel="noopener noreferrer"&gt;grant&lt;/a&gt;. It gets a &lt;code&gt;grant_id&lt;/code&gt; that works with every endpoint you already use — Messages, Drafts, Threads, Folders, Attachments, Calendars, Events, Webhooks. Nothing new to learn on the data plane.&lt;/p&gt;

&lt;p&gt;This walkthrough provisions one two ways (CLI and raw API), then sends, receives, RSVPs, and adds guardrails.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get
&lt;/h2&gt;

&lt;p&gt;When you create an Agent Account, Nylas provisions a real mailbox on a domain you've registered (or a Nylas &lt;code&gt;*.nylas.email&lt;/code&gt; trial domain). Each account comes with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An email address that sends and receives like any other mailbox&lt;/li&gt;
&lt;li&gt;Six system folders (&lt;code&gt;inbox&lt;/code&gt;, &lt;code&gt;sent&lt;/code&gt;, &lt;code&gt;drafts&lt;/code&gt;, &lt;code&gt;trash&lt;/code&gt;, &lt;code&gt;junk&lt;/code&gt;, &lt;code&gt;archive&lt;/code&gt;), plus any custom ones you create&lt;/li&gt;
&lt;li&gt;A primary calendar that hosts events and RSVPs over standard iCalendar/ICS&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;grant_id&lt;/code&gt; for all the existing Nylas endpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Before you begin
&lt;/h2&gt;

&lt;p&gt;You need two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A Nylas API key.&lt;/strong&gt; The fastest path is the CLI — &lt;code&gt;nylas init&lt;/code&gt; creates an account and generates a key in one command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A domain.&lt;/strong&gt; Either a Nylas-provided &lt;code&gt;*.nylas.email&lt;/code&gt; trial subdomain (good for testing in minutes) or your own custom domain with MX + TXT records. Register it under Organization Settings → Domains.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why your own domain? It's what makes the agent a real first-class sender instead of a shared relay address — people reply to it, calendars invite it, and its mail authenticates as coming from you. A new domain builds sender reputation over roughly four weeks of gradual sending, so run one domain per customer or use case to keep reputations isolated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create an Agent Account
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option A — the CLI (fastest)
&lt;/h3&gt;

&lt;p&gt;After &lt;code&gt;nylas init&lt;/code&gt;, provisioning is one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas agent account create &lt;span class="nb"&gt;test&lt;/span&gt;@your-application.nylas.email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI prints the new grant ID alongside connector and status details. A few companion commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List every agent account&lt;/span&gt;
nylas agent account list

&lt;span class="c"&gt;# Confirm the connector is ready&lt;/span&gt;
nylas agent status

&lt;span class="c"&gt;# Show one account&lt;/span&gt;
nylas agent account get &lt;span class="nb"&gt;test&lt;/span&gt;@your-application.nylas.email

&lt;span class="c"&gt;# See how accounts, workspaces, policies, rules, and lists fit together&lt;/span&gt;
nylas agent overview
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last one — &lt;code&gt;nylas agent overview&lt;/code&gt; — renders a tree per account and flags dangling references to deleted policies/rules. It's the command I'd reach for first when something looks misconfigured.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B — the API
&lt;/h3&gt;

&lt;p&gt;Under the hood the CLI calls &lt;a href="https://developer.nylas.com/docs/reference/api/manage-grants/byo_auth/" rel="noopener noreferrer"&gt;&lt;code&gt;POST /v3/connect/custom&lt;/code&gt;&lt;/a&gt; with &lt;code&gt;"provider": "nylas"&lt;/code&gt; — the same Bring Your Own Auth endpoint used for other providers, minus the refresh token. Just the email on a registered domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/connect/custom"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;NYLAS_API_KEY&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "provider": "nylas",
    "name": "Test Agent",
    "settings": {
      "email": "test@your-application.nylas.email"
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response contains a &lt;code&gt;grant_id&lt;/code&gt; — &lt;strong&gt;save it&lt;/strong&gt;, you'll use it everywhere. The top-level &lt;code&gt;name&lt;/code&gt; sets the display name recipients see on outbound mail (&lt;code&gt;Test Agent &amp;lt;test@your-application.nylas.email&amp;gt;&lt;/code&gt;). Omit it and the account sends with no display name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Receive mail
&lt;/h2&gt;

&lt;p&gt;Two ways, same as any grant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Poll the messages endpoint:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/grants/&amp;lt;GRANT_ID&amp;gt;/messages?limit=5"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;NYLAS_API_KEY&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Or subscribe to a webhook&lt;/strong&gt; so Nylas calls you the moment mail arrives. From the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas webhook create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; https://yourapp.example.com/webhooks/nylas &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--triggers&lt;/span&gt; message.created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inbound mail fires the standard &lt;code&gt;message.created&lt;/code&gt; webhook — identical in shape to &lt;code&gt;message.created&lt;/code&gt; for any other grant, so a handler you already have just works. On top of that, Agent Accounts emit deliverability webhooks like &lt;code&gt;message.delivered&lt;/code&gt;, &lt;code&gt;message.bounced&lt;/code&gt;, and &lt;code&gt;message.complaint&lt;/code&gt; for tracking outbound mail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Send mail
&lt;/h2&gt;

&lt;p&gt;Same &lt;code&gt;/messages/send&lt;/code&gt; endpoint you'd use for a connected grant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/grants/&amp;lt;GRANT_ID&amp;gt;/messages/send"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;NYLAS_API_KEY&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "subject": "Hello from my Agent Account",
    "body": "This message was sent by a Nylas Agent Account.",
    "to": [{ "email": "you@yourdomain.com", "name": "You" }]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recipient sees a normal message from the agent's address — no "sent via" branding, no relay footer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the calendar
&lt;/h2&gt;

&lt;p&gt;Every Agent Account ships with a primary calendar, reachable through the same Events endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /v3/grants/{grant_id}/events&lt;/code&gt; — list events&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /v3/grants/{grant_id}/events&lt;/code&gt; — create an event and invite others&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /v3/grants/{grant_id}/events/{id}/send-rsvp&lt;/code&gt; — accept or decline an invitation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Invitations travel over standard iCalendar/ICS, so Google Calendar, Microsoft 365, and Apple Calendar all treat the agent as a normal participant. This is what lets a scheduling agent propose slots and have participants accept them as ordinary invites.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardrail it: policies, rules, and lists
&lt;/h2&gt;

&lt;p&gt;Provisioning gives you a working mailbox. The interesting operational part is constraining what it can do. Three application-scoped resources handle that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Policies&lt;/strong&gt; bundle limits (send quota, storage, retention, attachment caps) and spam settings. One policy governs many accounts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rules&lt;/strong&gt; match inbound or outbound mail on sender/recipient fields and run actions like &lt;code&gt;block&lt;/code&gt;, &lt;code&gt;mark_as_spam&lt;/code&gt;, &lt;code&gt;assign_to_folder&lt;/code&gt;, &lt;code&gt;archive&lt;/code&gt;, or &lt;code&gt;trash&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lists&lt;/strong&gt; are typed collections of domains, TLDs, or addresses that rules reference via the &lt;code&gt;in_list&lt;/code&gt; operator.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They attach through &lt;strong&gt;workspaces&lt;/strong&gt;, not individual grants: a workspace carries one &lt;code&gt;policy_id&lt;/code&gt; and an array of &lt;code&gt;rule_ids&lt;/code&gt;, and every account in it inherits both. Each application has a default workspace, so configuring it governs all your unassigned accounts at once.&lt;/p&gt;

&lt;p&gt;Inspect them from the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas agent policy list
nylas agent rule list
nylas agent list list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And create each piece from the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# A typed list of domains you can reference from a rule's in_list condition&lt;/span&gt;
nylas agent list create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"Blocked domains"&lt;/span&gt; &lt;span class="nt"&gt;--type&lt;/span&gt; domain &lt;span class="nt"&gt;--item&lt;/span&gt; spam.com

&lt;span class="c"&gt;# An outbound rule that archives the sent copy of mail to a recipient domain&lt;/span&gt;
nylas agent rule create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"Archive outbound mail"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--trigger&lt;/span&gt; outbound &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--condition&lt;/span&gt; recipient.domain,is,example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--action&lt;/span&gt; archive

&lt;span class="c"&gt;# A policy you can attach to a workspace&lt;/span&gt;
nylas agent policy create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"Strict Policy"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap &lt;code&gt;--action archive&lt;/code&gt; for &lt;code&gt;--action block&lt;/code&gt; and the rule rejects the message instead — before it ever reaches the mailbox (inbound) or the provider (outbound). That's handy for data-loss prevention, keeping test domains out of production, or stopping an agent from emailing the wrong people. Inbound and outbound rules are isolated: inbound rules never run on sends, and outbound rules never re-evaluate the stored sent copy.&lt;/p&gt;

&lt;p&gt;Policies are optional. Skip them and the account runs at your billing plan's maximum limits and delivers every inbound message to the inbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Known limits (free plan)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Send rate&lt;/td&gt;
&lt;td&gt;200 messages/account/day&lt;/td&gt;
&lt;td&gt;Paid plans have no daily cap by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;3 GB/org&lt;/td&gt;
&lt;td&gt;Higher on paid plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retention&lt;/td&gt;
&lt;td&gt;30 days inbox, 7 days spam&lt;/td&gt;
&lt;td&gt;Configurable via policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domains&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;One application can manage accounts across any number of domains&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The thing that makes Agent Accounts click is that there's no second system. You provision an identity with one CLI command or one API call, and from that point it's the same Messages, Events, and Webhooks surface you'd use for a connected Gmail or Microsoft grant — now owned by your agent instead of borrowed from a user.&lt;/p&gt;

&lt;p&gt;If you want to go deeper:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.nylas.com/docs/v3/getting-started/agent-accounts/" rel="noopener noreferrer"&gt;Agent Accounts quickstart&lt;/a&gt; — end to end in under 5 minutes&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.nylas.com/docs/v3/agent-accounts/" rel="noopener noreferrer"&gt;What are Agent Accounts&lt;/a&gt; — the full concept guide&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.nylas.com/docs/v3/agent-accounts/policies-rules-lists/" rel="noopener noreferrer"&gt;Policies, Rules, and Lists&lt;/a&gt; — the guardrail model in detail&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.nylas.com/docs/v3/agent-accounts/supported-endpoints/" rel="noopener noreferrer"&gt;Supported endpoints&lt;/a&gt; — everything that works against an Agent Account grant&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://cli.nylas.com/authors/qasim-muhammad" rel="noopener noreferrer"&gt;Qasim Muhammad&lt;/a&gt; and &lt;a href="https://cli.nylas.com/authors/pouya-sanooei" rel="noopener noreferrer"&gt;Pouya Sanooei&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>email</category>
      <category>api</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
