A cron job that lists messages every minute wastes most of its calls finding nothing new, and it still leaves you up to 60 seconds behind. Gmail makes that worse: the Gmail API caps per-project quota, so polling many mailboxes drains it fast. A message.created webhook flips the model. Nylas pushes you the new message within seconds of it landing, and the same subscription covers Gmail and Outlook without separate Pub/Sub topics or Graph subscriptions.
This recipe shows the three pieces you need: subscribe to the trigger, pass the challenge handshake, and handle the notification when mail arrives. For the wider catalog of message, calendar, and engagement triggers, see real-time webhooks.
Subscribe to new-message events
Section titled “Subscribe to new-message events”Create a webhook destination with POST /v3/webhooks/ and set trigger_types to ["message.created"]. That one trigger fires for every new message on any connected account, so a single subscription spans all 6 providers including Gmail and Outlook. The request takes your HTTPS webhook_url, a description, and optional notification_email_addresses for failure alerts.
The samples below register one endpoint for new-email notifications. The webhook_url must be a public HTTPS URL, since Nylas calls it from the internet within 10 seconds to verify it. Swap message.created for the full trigger array if you need more events on the same endpoint.
curl --request POST \ --url 'https://api.us.nylas.com/v3/webhooks/' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --data-raw '{ "trigger_types": ["message.created"], "webhook_url": "https://yourapp.com/webhooks/nylas", "description": "New email notifications", "notification_email_addresses": ["[email protected]"] }'import Nylas, { WebhookTriggers } from "nylas";
const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>" });
const webhook = await nylas.webhooks.create({ requestBody: { triggerTypes: [WebhookTriggers.MessageCreated], webhookUrl: "https://yourapp.com/webhooks/nylas", description: "New email notifications", },});
console.log("Webhook created:", webhook.data.id);from nylas import Clientfrom nylas.models.webhooks import WebhookTriggers
nylas = Client("<NYLAS_API_KEY>")
webhook = nylas.webhooks.create( request_body={ "trigger_types": [WebhookTriggers.MESSAGE_CREATED], "webhook_url": "https://yourapp.com/webhooks/nylas", "description": "New email notifications", })
print(webhook.data.id)Verify the webhook challenge
Section titled “Verify the webhook challenge”Before any notification arrives, the API confirms you own the endpoint with a handshake. The first time you create the webhook or set it back to active, Nylas sends a GET request carrying a challenge query parameter, and your handler has 10 seconds to echo that exact value back in a 200 OK body. Return only the raw value: no quotes, no JSON wrapper, no chunked encoding. Get any of those wrong and the endpoint is marked failed on the first try, with no retry.
The handler below answers the GET challenge and accepts POST notifications on the same route. A passing handshake is what generates your webhook_secret, the key you’ll need for signature checks later.
import express from "express";
const app = express();app.use(express.json());
// Challenge handshake: echo the raw value within 10 secondsapp.get("/webhooks/nylas", (req, res) => { res.status(200).send(req.query.challenge);});
// Notifications land here once verification passesapp.post("/webhooks/nylas", (req, res) => { res.status(200).end(); // process req.body asynchronously});
app.listen(3000);from flask import Flask, request
app = Flask(__name__)
@app.route("/webhooks/nylas", methods=["GET"])def challenge(): # Echo the raw value within 10 seconds, no quotes return request.args.get("challenge"), 200
@app.route("/webhooks/nylas", methods=["POST"])def notification(): # Acknowledge first, process req body afterward return "", 200Handle a new-email notification
Section titled “Handle a new-email notification”Once verified, Nylas POSTs a JSON notification each time mail arrives. The message.created payload nests the message under data.object, which carries the id and grant_id you need (application_id sits on data). Acknowledge with a 200 OK inside 10 seconds first, then fetch the full message with GET /v3/grants/{grant_id}/messages/{message_id} to read the subject, sender, and body. Acknowledging before you fetch keeps a slow downstream call from timing out the 10-second webhook window.
The handler below pulls the IDs from the payload and requests the full message. Returning the 200 OK before the fetch keeps you under the timeout even when the message read is slow.
app.post("/webhooks/nylas", async (req, res) => { res.status(200).end(); // acknowledge first
const { object } = req.body.data; const grant_id = object.grant_id; const messageId = object.id;
const message = await nylas.messages.find({ identifier: grant_id, messageId, });
console.log("New email:", message.data.subject, "from", message.data.from);});@app.route("/webhooks/nylas", methods=["POST"])def notification(): payload = request.get_json() obj = payload["data"]["object"] grant_id = obj["grant_id"] message_id = obj["id"]
message = nylas.messages.find(identifier=grant_id, message_id=message_id) print("New email:", message.data.subject, "from", message.data.from_) return "", 200Things to know about new-email webhooks
Section titled “Things to know about new-email webhooks”The message.created trigger has three delivery variants, and your handler should treat all of them as “new mail arrived.” A standard notification includes the message body inline. If the payload would exceed the 1 MB limit, Nylas strips the body and appends .truncated to the type, so you re-fetch with a Get Message request. If you subscribe to message.created.cleaned and have Clean Conversations configured, the body arrives as cleaned markdown instead of raw HTML. You don’t subscribe to .truncated separately, but your code needs a branch for it.
Gmail delivery is faster when you wire up Pub/Sub. For Google grants with gmail.readonly or gmail.labels scopes, Google Pub/Sub feeds push events instead of polling, which shaves seconds off message.created latency. Outlook accounts get comparable speed through Microsoft Graph subscriptions that Nylas manages for you, so neither provider needs you to renew anything.
Two delivery properties shape your handler design. The API guarantees at-least-once delivery, so the same message.created can arrive twice, often because Google and Microsoft Graph send upserts. Dedupe on the message id before you act, and read verify webhook signatures to confirm each payload is genuine using the X-Nylas-Signature HMAC. Ordering isn’t guaranteed either: a message.created and a later message.updated for the same message can land out of order, so trust the fetched message state over the sequence of notifications. The full failure and retry behavior lives in Using webhooks with Nylas.
What’s next
Section titled “What’s next”- Message tracking to add
message.openedengagement notifications - Verify webhook signatures to validate every payload with HMAC-SHA256
- Real-time webhooks for the wider catalog of message, calendar, and engagement triggers
- Using webhooks with Nylas for setup, verification, retries, and failure handling