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.
This 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.
Triggers and destinations
A 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.
The CLI lists every available trigger so you don't have to guess the names:
# All trigger types
nylas webhook triggers
# Only message-related triggers
nylas webhook triggers --category message
Webhooks 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.
Before you begin
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:
nylas init # create an account, generate an API key
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.
Create a webhook
Registering a webhook takes a destination URL and at least one trigger. The CLI maps these to --url and --triggers:
nylas webhook create \
--url https://yourapp.example.com/webhooks/nylas \
--triggers message.created,event.created,contact.created \
--description "Production receiver" \
--notify admin@example.com
The --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:
curl --request POST \
--url "https://api.us.nylas.com/v3/webhooks" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
--header "Content-Type: application/json" \
--data '{
"webhook_url": "https://yourapp.example.com/webhooks/nylas",
"trigger_types": ["message.created", "event.created"],
"description": "Production receiver",
"notification_email_addresses": ["admin@example.com"]
}'
Note the field name is webhook_url, not callback_url. The CLI flags are documented at webhook create.
The challenge handshake
Here'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.
# What Nylas sends to verify your endpoint
curl -X GET 'https://yourapp.example.com/webhooks/nylas?challenge=bc609b38-c81f-47fb-a275-1d9bd61a968b'
Your 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.
Verify every webhook signature
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 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.
The 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:
nylas webhook verify \
--payload-file ./raw-body.json \
--secret "<WEBHOOK_SECRET>" \
--signature "<x-nylas-signature header value>"
If 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.
Develop locally with a tunnel
The 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.
# Local receiver behind a cloudflared tunnel, verifying signatures
nylas webhook server --tunnel cloudflared --secret "<WEBHOOK_SECRET>"
With --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.
You 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.
Rotate the secret when it leaks
If 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:
nylas webhook rotate-secret <webhook-id>
The 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.
Acknowledge fast, or get retried
Your 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.
Retries 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.
What to know
| Dimension | Value | Notes |
|---|---|---|
| Scope | Application-level | One webhook covers every connected grant |
| Endpoint | HTTPS, public |
localhost works only via a tunnel |
| Challenge | Echo within 10 seconds | Return the raw challenge value, nothing else |
| Signature | HMAC-SHA256, hex-encoded |
x-nylas-signature header, signed over the raw body |
| Trigger categories | grant, message, thread, event, contact, calendar, folder, notetaker | Subscribe one destination to many |
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 POST requests.
Wrapping up
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.
Where to go next:
- Notifications overview — triggers, the challenge handshake, and signatures
- Receive webhooks with the CLI — the local-tunnel development workflow
- Notifications reference — every webhook payload schema
-
Nylas CLI command reference — every
nylas webhooksubcommand
Written by Qasim Muhammad and Pouya Sanooei.
Top comments (0)