Guide to Nylas Webhooks: Features and Best Practices
Nylas webhooks notify your app when something changes across the email, calendar, and contacts accounts your users have connected: a message arrives, a calendar event is updated, a grant expires. Instead of polling each provider Nylas integrates with, you get a single normalized notification stream through Nylas.
This guide covers how Nylas webhooks work, the triggers you can subscribe to, the challenge handshake at setup, how to verify the x-nylas-signature (including the gzip trap that breaks most first attempts), the delivery behavior, and the best practices for running them in production.
What are Nylas webhooks?
Nylas webhooks are HTTP POSTs delivered to an endpoint you register, each describing a change on a connected account. Payloads follow the CloudEvents 1.0 format, so every notification carries specversion, type, source, id, time, a webhook_delivery_attempt counter, and a data object with the application_id, grant_id, and the affected object.
Nylas signs deliveries with an x-nylas-signature header and verifies your endpoint at creation with a challenge handshake.
Nylas webhook features
| Feature | Details |
|---|---|
| Configuration | Dashboard or POST /v3/webhooks |
| Endpoint verification | Challenge handshake: echo ?challenge=<value> within 10s |
| Signature header | x-nylas-signature (case-insensitive) |
| Signature scheme | hex HMAC-SHA256 of the raw body, keyed with the webhook_secret |
| Payload format | CloudEvents 1.0 |
| Compression | Optional gzip; the signature covers the compressed bytes |
| Retries | On temporary errors only; 2 retries (3 attempts total) |
| Webhook states | failing after 95% non-200s over 15 min; failed after 72h |
| SDKs | Webhook CRUD, rotateSecret, ipAddresses; no verify helper |
Common triggers
Nylas v3 exposes triggers across grants, messages, threads, calendars, events, folders, contacts, bookings, and notetaker activity. A few common ones:
| Trigger | Fires when |
|---|---|
message.created | A new message is received on a connected account |
message.opened | A tracked message is opened (if message tracking is enabled) |
thread.replied | A thread receives a reply |
calendar.updated | A calendar is updated |
event.created | A calendar event is created |
grant.expired | A connected account's grant expires and needs re-auth |
Subscribe only to the triggers you need, and consult the Nylas notification schema reference for the authoritative, current catalog.
Setting up Nylas webhooks
The challenge handshake
When you create a webhook (in the dashboard or via POST /v3/webhooks), Nylas sends a GET request with a challenge query parameter. Echo the exact value back as the response body with a 200, within 10 seconds, and without chunked encoding. Return anything other than the raw challenge value and creation fails.
app.get("/webhook", (req, res) => {
res.status(200).send(req.query.challenge); // echo the exact value
});
On successful creation Nylas returns a webhook_secret (unique per destination), which you use to verify deliveries. You can rotate it later via the API.
Securing Nylas webhooks
Every notification carries an x-nylas-signature header (casing varies, so treat it case-insensitively): a hex-encoded HMAC-SHA256 of the raw request body, keyed with the webhook_secret. Recompute it over the exact bytes and compare in constant time.
The gotcha that breaks most integrations: if you enable compressed delivery, Nylas sends Content-Encoding: gzip, and the signature covers the compressed bytes. Verify against the raw gzip body first, then decompress only after the check passes. Decompress first and the digest never matches.
const crypto = require("crypto");
const SECRET = process.env.NYLAS_WEBHOOK_SECRET;
function verify(rawBody, signatureHeader) {
const expected = crypto.createHmac("sha256", SECRET).update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from((signatureHeader || "").toLowerCase());
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Read the body as raw bytes; do NOT decompress before verifying
app.post("/webhook", express.raw({ type: () => true }), (req, res) => {
const signature = req.headers["x-nylas-signature"];
if (!verify(req.body, signature)) {
return res.sendStatus(401);
}
res.sendStatus(200); // acknowledge fast
// Decompress (if gzip) only after the signature check passes, then enqueue
processQueue.add(req.body);
});
The same check in Python:
import hashlib
import hmac
import os
SECRET = os.environ["NYLAS_WEBHOOK_SECRET"].encode()
def verify(raw_body: bytes, signature: str) -> bool:
expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, (signature or "").lower())
The official Nylas SDKs expose webhook CRUD, rotateSecret, ipAddresses, and a challenge-parameter helper, but no signature-verify helper, so you implement the HMAC check yourself with a constant-time comparison.
Nylas webhook limitations and pain points
The gzip signature trap
The Problem: With compressed delivery enabled, the signature is computed over the gzip bytes. Any framework or code path that decompresses (or re-serializes) the body before verification produces a different digest, and every notification fails the check.
Why It Happens: Signing the bytes on the wire is correct, but it collides with the common instinct to normalize the body before verifying.
Workarounds:
- Capture the raw body as bytes and verify before touching
Content-Encoding. - Decompress only after the signature passes.
How Hookdeck Can Help: Hookdeck verifies the x-nylas-signature at the edge against the bytes as received, so your app never has to reason about compressed-vs-decompressed body handling.
Retries only on temporary errors
The Problem: Nylas retries only on a specific set of temporary errors (408, 429, 502, 503, 504, 507), with just 2 retries for 3 attempts total. A 4xx you return for any other reason, or a bug that 500s, may not be retried.
Why It Happens: Nylas distinguishes transient infrastructure errors from application errors it assumes won't succeed on retry.
Workarounds:
- Return one of the retryable status codes when you want a delivery retried, and a 2xx once you've safely enqueued it.
- Persist notifications on receipt so a downstream failure doesn't depend on Nylas retrying.
How Hookdeck Can Help: Hookdeck retries to your downstream on a schedule you control regardless of the status code semantics, and preserves failed deliveries for replay, so a non-retryable error no longer means a lost notification.
Webhooks go failing, then failed
The Problem: If more than 95% of deliveries return non-200 over a 15-minute window, Nylas marks the webhook failing; 72 hours of that marks it failed, and a failed webhook needs manual reactivation. A broken endpoint can silently take your webhook offline.
Why It Happens: Nylas stops delivering to endpoints that look permanently broken.
Workarounds:
- Acknowledge fast with a 200 so transient downstream issues don't push you toward
failing. - Monitor webhook state via the API and alert on
failing.
How Hookdeck Can Help: Hookdeck always accepts deliveries from Nylas and absorbs downstream failures itself, keeping your Nylas webhook healthy while it retries to your service. Its observability surfaces failures long before they reach the 72-hour failed threshold.
Duplicate deliveries
The Problem: Retries and the webhook_delivery_attempt counter make clear the same notification can arrive more than once. Acting on duplicates double-processes.
Why It Happens: At-least-once delivery prioritizes not losing notifications over exactly-once semantics.
Workarounds:
- Dedupe on the CloudEvents
id. - Make side effects idempotent at the destination.
How Hookdeck Can Help: Hookdeck deduplicates deliveries at the edge, so retries don't reach your handler twice. See our guide to webhook idempotency.
Best practices
Verify against the raw (possibly gzip) body
Recompute the hex HMAC-SHA256 over the exact bytes with the webhook_secret, treat the header casing as case-insensitive, and never decompress before the check.
Handle the challenge handshake
Echo the ?challenge= value exactly, with a 200, within 10 seconds, and without chunked encoding.
Acknowledge fast, process asynchronously
Return 200 as soon as the signature verifies and defer real work to a queue, so transient slowness doesn't drive you toward the failing state. See why to process webhooks asynchronously.
Dedupe on the CloudEvents id
Use the event id as an idempotency key so retries are no-ops, and keep side effects idempotent.
Monitor webhook state and rotate secrets safely
Watch for the failing state, and use rotateSecret with an overlap window when rotating the webhook_secret.
Make Nylas webhooks production-ready
Hookdeck verifies x-nylas-signature (gzip and all), deduplicates, and durably queues every notification
Conclusion
Nylas webhooks give you one normalized, CloudEvents-formatted stream across every email, calendar, and contacts account your users connect. The mechanics are specific: a challenge handshake at setup, an x-nylas-signature you verify against the raw body, and a gzip mode where the signature covers the compressed bytes and trips up most first implementations. Delivery adds its own edges: retries only on certain errors, a limited retry count, and failing/failed states that can quietly disable a webhook.
That puts verification correctness, fast acknowledgement, deduplication, and webhook-state monitoring on you. Hookdeck verifies the signature (compressed or not), deduplicates, durably queues, and keeps your Nylas webhook healthy, so your app only ever processes verified, unique notifications.
Get started with Hookdeck for free and handle Nylas webhooks reliably in minutes.