One slow database write inside your handler is all it takes to lose events. If your endpoint doesn’t answer within 10 seconds, the request fails and the notification gets queued for retry. A deploy that returns 502 for 30 seconds, a burst of event.updated notifications that backs up your worker pool, an exception that throws before you send 200 OK — each one drops events your app needed.
Reliable delivery comes down to three habits: acknowledge fast, treat every delivery as a possible duplicate, and know how to inspect a webhook that’s gone quiet. This recipe covers all three.
Respond fast and process asynchronously
Section titled “Respond fast and process asynchronously”Return 200 OK the instant a notification arrives, then do the real work in a background queue. The API enforces a 10-second timeout: if your endpoint spends those seconds parsing a 1 MB payload or writing to a database, the request fails and counts against your endpoint’s health. Acknowledge first, process second.
The pattern below reads the raw body, pushes it onto a queue, and answers within milliseconds. The handler never touches your database on the request path, so a slow query can’t blow the 10-second budget. A separate worker fetches objects and updates state.
import express from "express";const app = express();
// Capture the raw body so signature verification stays exact.app.use(express.raw({ type: "application/json", limit: "2mb" }));
app.post("/webhooks/nylas", (req, res) => { // Ack immediately, then hand the raw bytes to a queue. res.status(200).end(); queue.add("nylas-webhook", { body: req.body, headers: req.headers });});from flask import Flask, request
app = Flask(__name__)
@app.post("/webhooks/nylas")def handle(): # Enqueue the raw bytes, then return 200 OK in the same breath. raw = request.get_data() queue.enqueue("process_nylas_webhook", raw, dict(request.headers)) return "", 200For the architecture behind this, including load balancing and downtime handling, see Best practices for webhooks.
Handle retries and duplicate deliveries
Section titled “Handle retries and duplicate deliveries”The Nylas APIs guarantee at-least-once delivery, so your handler will sometimes see the same event twice. When the first delivery doesn’t return 200 OK, the API retries up to two more times for three attempts total, backing off exponentially, with the final attempt landing 10 to 20 minutes after the first.
Make handlers idempotent by checking the notification’s id against a set of IDs you’ve already processed before you act. Record each ID in a store with a short expiry, skip anything you’ve already seen, and your logic stays correct whether an event arrives once or three times. The snippet below shows the check.
async function process(notification) { const eventId = notification.id; // top-level notification ID, stable across retries // SET NX returns false if the key already exists -- a duplicate delivery. const isNew = await redis.set(`wh:${eventId}`, "1", "NX", "EX", 86400); if (!isNew) return; // already handled, skip await applyChange(notification); // your real work, runs exactly once}Retries fire only for status codes that signal a temporary problem: 408, 429, 502, 503, and 504, plus 507. The API treats every other code, including 4xx auth and validation errors, as permanent and never retries it, so a bug that returns 400 drops the event without warning.
Debug a webhook that isn’t firing
Section titled “Debug a webhook that isn’t firing”When notifications stop arriving, work the chain from your endpoint back to the subscription. Most silent failures trace to one of four causes: the endpoint isn’t reachable over public HTTPS, the challenge verification never completed, the subscription lacks the right trigger_types, or the destination is in a failing or failed state. Check each before suspecting the API.
Walk this checklist:
- Reach the endpoint — confirm your
webhook_urlis public HTTPS and returns200to aGET. The API blocks Ngrok URLs, so use VS Code port forwarding or Hookdeck for local testing. - Verification — the first activation sends a
GETwith achallengequery parameter you must echo verbatim in a200 OKwithin 10 seconds. Miss it and the webhook never activates. - Triggers — list your webhook and confirm
trigger_typesincludes the event you expect. A handler waiting onmessage.createdsees nothing if you only subscribed toevent.created. - Health — a destination marked
failing(95% non-200over 15 minutes) orfailed(95% over 72 hours) stops normal delivery. Reactivate it from the Dashboard or the Webhooks API.
Once the subscription looks right, send a known-good payload instead of waiting for a real event. The Send Test Event endpoint posts a test notification and reports whether your endpoint answered 200 OK, and the Get Mock Notification Payload endpoint returns an example body for any trigger_type so you can unit-test your parser.
# Fire a test event at your live endpoint and watch for a 200.nylas webhook test send https://yourapp.com/webhooks/nylas
# Print a mock payload for one trigger to test your parser offline.nylas webhook test payload message.createdThings to know about webhook delivery
Section titled “Things to know about webhook delivery”Delivery is best-effort with bounded retries, so design for gaps and repeats rather than assuming one in-order arrival per event. The API attempts each notification up to three times over a 10-to-20-minute window. If all three fail, it skips that notification type and keeps sending others. After 72 hours of 95% non-200 responses, it marks the endpoint failed.
A few specifics worth building around:
- Timeouts count as failures. Your endpoint has 10 seconds per request. The verification
GETshares the same 10-second budget, and the API verifies an endpoint only once — fail the first check and you must recreate or reactivate the webhook. - Ordering isn’t guaranteed. Per the Standard Webhooks specification, notifications can arrive out of order. Handle an
.updatedor.deletednotification that lands before the matching.created. - Payloads cap at 1 MB. Larger
message.*notifications arrive truncated with a.truncatedsuffix and no body, so re-query the object with a Get Message request when you see one. - Verify before you trust. Every notification carries an
X-Nylas-Signatureheader, a hex HMAC-SHA256 of the raw body. See Verify webhook signatures for the check, and secure a webhook for the secret it uses. - Allowlist the sender. Failure-state notifications come by email from
[email protected]. Allowlist that address so the warning doesn’t land in Spam.
For the exact retry codes and failure thresholds, see Retry a webhook.
What’s next
Section titled “What’s next”- Verify webhook signatures — check the HMAC-SHA256 signature on every payload
- Get real-time updates with webhooks — subscribe one endpoint to messages, opens, replies, and calendar events
- Get notified of new email — wire the
message.createdtrigger end to end - Using webhooks with Nylas — the full setup, verification, retry, and failure reference