# Get a webhook for new email

Source: https://developer.nylas.com/docs/cookbook/use-cases/build/new-email-webhook/

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](/docs/cookbook/use-cases/build/realtime-webhooks/).

## 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.

```bash
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": ["alerts@yourapp.com"]
  }'
```

```js [newEmailWebhook-Node.js]


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",
    notificationEmailAddresses: ["alerts@yourapp.com"],
  },
});

console.log("Webhook created:", webhook.data.id);
```

```python
from nylas import Client
from 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",
        "notification_email_addresses": ["alerts@yourapp.com"],
    }
)

print(webhook.data.id)
```

## Subscribe from the terminal

Getting notified of new mail is a single `message.created` subscription. The [Nylas CLI](https://cli.nylas.com/docs/commands) sets it up directly: `nylas webhook create --triggers message.created` registers your URL for inbound-message events, so the moment mail arrives, your endpoint gets a `POST` instead of you polling.

```bash
nylas webhook create \
  --url https://yourapp.example.com/webhooks/nylas \
  --triggers message.created
```

The `message.created` payload matches every other grant's, so one handler works across providers. Your endpoint must return a `200` response, or the delivery is retried twice more over the next 20 minutes. See the [`webhook create`](https://cli.nylas.com/docs/commands/webhook-create) command reference, and run `nylas webhook triggers --category message` to see related message events.

## 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.

```js [challengeHandler-Node.js]


const app = express();
app.use(express.json());

// Challenge handshake: echo the raw value within 10 seconds
app.get("/webhooks/nylas", (req, res) => {
  res.status(200).send(req.query.challenge);
});

// Notifications land here once verification passes
app.post("/webhooks/nylas", (req, res) => {
  res.status(200).end();
  // process req.body asynchronously
});

app.listen(3000);
```

```python
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 "", 200
```

## Handle a new-email notification

Once verified, Nylas `POST`s 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.

```js [newEmailNotification-Node.js]
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);
});
```

```python
@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 "", 200
```

## 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](/docs/reference/api/messages/get-messages-id/). If you subscribe to `message.created.cleaned` and have [Clean Conversations](/docs/v3/email/parse-messages/) 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](/docs/provider-guides/google/connect-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](/docs/cookbook/use-cases/build/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](/docs/v3/notifications/).

## What's next

- [Message tracking](/docs/v3/email/message-tracking/) to add `message.opened` engagement notifications
- [Verify webhook signatures](/docs/cookbook/use-cases/build/verify-webhook-signatures/) to validate every payload with HMAC-SHA256
- [Real-time webhooks](/docs/cookbook/use-cases/build/realtime-webhooks/) for the wider catalog of message, calendar, and engagement triggers
- [Using webhooks with Nylas](/docs/v3/notifications/) for setup, verification, retries, and failure handling