{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreiatn2l6wfgargcquxwavpyo7qomygmx75mnodhihredlnulpueuqu",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3motdnivxkux2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreiap3oazxdgzaprp5joa44twnaywrkt3lyo5eezqfiq767gd452ux4"
},
"mimeType": "image/webp",
"size": 62822
},
"path": "/mqasimca/stop-polling-real-time-email-and-calendar-webhooks-with-nylas-534i",
"publishedAt": "2026-06-21T21:22:34.000Z",
"site": "https://dev.to",
"tags": [
"webhooks",
"api",
"devtools",
"security",
"Nylas CLI",
"notifications overview",
"webhook create",
"webhook verify",
"webhook server",
"receive webhooks with the CLI",
"webhook rotate-secret",
"Notifications overview",
"Receive webhooks with the CLI",
"Notifications reference",
"Nylas CLI command reference",
"Qasim Muhammad",
"Pouya Sanooei"
],
"textContent": "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.\n\nThis post walks the webhook surface from both sides: the HTTP API that registers and manages webhooks, and the Nylas CLI, 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.\n\n## Triggers and destinations\n\nA webhook has two halves: the **trigger types** it listens for and the **destination URL** it pushes to. Trigger types are dotted event names like `message.created`, `event.updated`, and `contact.created`, grouped into categories — grant, message, thread, event, contact, calendar, folder, and notetaker. You subscribe one destination to as many triggers as you want.\n\nThe CLI lists every available trigger so you don't have to guess the names:\n\n\n\n # All trigger types\n nylas webhook triggers\n\n # Only message-related triggers\n nylas webhook triggers --category message\n\n\nWebhooks are application-scoped, not grant-scoped: one webhook registered on your application receives notifications for every connected account, identified by the `grant_id` in each payload. See the notifications overview for the full event model.\n\n## Before you begin\n\nYou 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:\n\n\n\n nylas init # create an account, generate an API key\n\n\nFor 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.\n\n## Create a webhook\n\nRegistering a webhook takes a destination URL and at least one trigger. The CLI maps these to `--url` and `--triggers`:\n\n\n\n nylas webhook create \\\n --url https://yourapp.example.com/webhooks/nylas \\\n --triggers message.created,event.created,contact.created \\\n --description \"Production receiver\" \\\n --notify admin@example.com\n\n\nThe `--notify` 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 `webhook_url`, `trigger_types`, `description`, and `notification_email_addresses`:\n\n\n\n curl --request POST \\\n --url \"https://api.us.nylas.com/v3/webhooks\" \\\n --header \"Authorization: Bearer <NYLAS_API_KEY>\" \\\n --header \"Content-Type: application/json\" \\\n --data '{\n \"webhook_url\": \"https://yourapp.example.com/webhooks/nylas\",\n \"trigger_types\": [\"message.created\", \"event.created\"],\n \"description\": \"Production receiver\",\n \"notification_email_addresses\": [\"admin@example.com\"]\n }'\n\n\nNote the field name is `webhook_url`, not `callback_url`. The CLI flags are documented at webhook create.\n\n## The challenge handshake\n\nHere's the first thing that trips people up. When you register a webhook (or reactivate one), Nylas immediately sends a `GET` request to your URL with a `challenge` query parameter, and your endpoint must echo the exact value back in a `200 OK` within 10 seconds. If it doesn't, the webhook never activates.\n\n\n\n # What Nylas sends to verify your endpoint\n curl -X GET 'https://yourapp.example.com/webhooks/nylas?challenge=bc609b38-c81f-47fb-a275-1d9bd61a968b'\n\n\nYour handler has to return that `challenge` 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 `POST` requests, so your endpoint needs to handle both methods. The notifications overview documents the full handshake.\n\n## Verify every webhook signature\n\nThis 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 `x-nylas-signature` header containing a hex-encoded HMAC-SHA256 signature of the raw request body, signed with your endpoint's auto-generated `webhook_secret`.\n\nThe critical detail is _raw_ : 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 `verify` command implements this correctly, which makes it a good oracle when you're debugging your own implementation:\n\n\n\n nylas webhook verify \\\n --payload-file ./raw-body.json \\\n --secret \"<WEBHOOK_SECRET>\" \\\n --signature \"<x-nylas-signature header value>\"\n\n\nIf your own code and the CLI disagree on a payload, your code is mangling the body before hashing — almost always the bug. The webhook verify command documents the flags. One more wrinkle: if you accept compressed payloads, compute the signature over the compressed bytes _before_ decompressing, or it won't match.\n\n## Develop locally with a tunnel\n\nThe chicken-and-egg problem with webhooks is that you can't receive them on `localhost`, but you don't want to deploy to production just to test a handler. The CLI's `server` command solves this: it runs a local receiver and can expose it through a Cloudflare tunnel so real Nylas events reach your machine.\n\n\n\n # Local receiver behind a cloudflared tunnel, verifying signatures\n nylas webhook server --tunnel cloudflared --secret \"<WEBHOOK_SECRET>\"\n\n\nWith `--tunnel` set, `--secret` 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, `--no-tunnel` skips the tunnel and listens on localhost. The webhook server command covers the options, and there's a full walkthrough in receive webhooks with the CLI.\n\nYou can also test without a live event at all. `nylas webhook test payload` prints a mock payload for any trigger type so you can see the exact shape your handler will receive, and `nylas webhook test send` posts a test event to a URL.\n\n## Rotate the secret when it leaks\n\nIf your `webhook_secret` 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:\n\n\n\n nylas webhook rotate-secret <webhook-id>\n\n\nThe webhook rotate-secret command prints the new secret. Treat the secret like any other credential: out of source control, in your secrets manager, never logged.\n\n## Acknowledge fast, or get retried\n\nYour endpoint must respond with `200 OK` to each notification. If it doesn't, Nylas marks the webhook `failing` 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.\n\nRetries depend on the status code you return. Temporary signals like `429 Too Many Requests` (respect the `Retry-After` 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 `200 OK` 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.\n\n## What to know\n\nDimension | Value | Notes\n---|---|---\nScope | Application-level | One webhook covers every connected grant\nEndpoint | HTTPS, public | `localhost` works only via a tunnel\nChallenge | Echo within 10 seconds | Return the raw `challenge` value, nothing else\nSignature | HMAC-SHA256, hex-encoded | `x-nylas-signature` header, signed over the raw body\nTrigger categories | grant, message, thread, event, contact, calendar, folder, notetaker | Subscribe one destination to many\n\nThe 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 `POST` requests.\n\n## Wrapping up\n\nWebhooks 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.\n\nWhere to go next:\n\n * Notifications overview — triggers, the challenge handshake, and signatures\n * Receive webhooks with the CLI — the local-tunnel development workflow\n * Notifications reference — every webhook payload schema\n * Nylas CLI command reference — every `nylas webhook` subcommand\n\n\n\n_Written by Qasim Muhammad and Pouya Sanooei._",
"title": "Stop polling: real-time email and calendar webhooks with Nylas"
}