Source: https://cli.nylas.com/guides/email-webhook-events-reference

# Email Webhook Events: Developer Reference

You need real-time events, not polling. This reference covers every Nylas webhook trigger type across email, calendar, contacts, and grants — with payload shapes, retry behavior, HMAC verification, and the CLI commands to register and test them.

Written by [Pouya Sanooei](https://cli.nylas.com/authors/pouya-sanooei) Software Engineer

Updated May 30, 2026

> **TL;DR:** Run [`nylas webhook triggers`](https://cli.nylas.com/docs/commands/webhook-triggers) to list all trigger types. Create an endpoint with [`nylas webhook create --url URL --triggers "message.created,event.updated"`](https://cli.nylas.com/docs/commands/webhook-create). Verify signatures with [`nylas webhook verify`](https://cli.nylas.com/docs/commands/webhook-verify) before parsing. Nylas retries a non-200 response up to 3 times with exponential backoff, the last attempt 10–20 minutes after the first.

Polling the API every N seconds costs quota and adds latency. Webhooks let Nylas push a signed event to your endpoint within seconds of a change. The [Nylas webhooks documentation](https://developer.nylas.com/docs/developer-guide/webhooks/) covers the full platform contract. This page is the quick-reference you bookmark: trigger names, payload shapes, retry rules, and the CLI commands to wire everything up quickly.

## What trigger types are available for email, calendar, and contacts?

A webhook trigger is a dot-separated string that identifies the object type and the change that occurred. Nylas groups triggers into 4 categories: message, event, contact, and grant. Each category has a `created`, `updated`, and `deleted` variant. Grant events are the exception — they use `created`, `updated`, and `expired`. As of Nylas v3, there are several trigger types across four categories.

The fastest way to get the current list is [`nylas webhook triggers`](https://cli.nylas.com/docs/commands/webhook-triggers). Pass `--category` to filter by object type and `--json` to pipe the output into a script or config file.

```bash
# List all trigger types
nylas webhook triggers --json

# Filter to message triggers only
nylas webhook triggers --category message --json

# Filter to calendar event triggers
nylas webhook triggers --category event --json
```

### Trigger reference table

Run `nylas webhook triggers` for the latest list. Use the exact string when passing `--triggers` to [`nylas webhook create`](https://cli.nylas.com/docs/commands/webhook-create).

| Trigger | Category | Fires when |
| --- | --- | --- |
| message.created | message | A new email arrives in the mailbox |
| message.updated | message | Labels, read state, or metadata change on an existing message |
| message.deleted | message | A message is permanently deleted or expunged |
| event.created | event | A new calendar event is added |
| event.updated | event | Title, time, attendees, or status change on an existing event |
| event.deleted | event | A calendar event is removed |
| contact.created | contact | A new contact is added to the address book |
| contact.updated | contact | Name, email, phone, or group membership changes |
| contact.deleted | contact | A contact is removed from the address book |
| grant.created | grant | A user completes OAuth and a grant is issued |
| grant.updated | grant | Token is refreshed or scopes change |
| grant.expired | grant | Access token expires and can no longer be refreshed |

## What does a webhook payload look like?

Every Nylas webhook payload is a JSON object with a top-level `type` field that matches the trigger name, a `data.object` block with the changed resource, and a `id` field you should use as a deduplication key. The shape is consistent across all 4 categories. Your handler can branch on `type` without parsing the full body first.

Use [`nylas webhook test payload`](https://cli.nylas.com/docs/commands/webhook-test-payload) to generate a sample for any trigger. The output is deterministic and safe to commit as a test fixture. The example below shows a `message.created` payload — the same structure applies to all message triggers, with `type` changing to `message.updated` or `message.deleted` as appropriate.

```json
{
  "specversion": "1.0",
  "type": "message.created",
  "id": "8d7f3c2a-1b4e-4a9d-9e0f-5c2b7a8d1e3f",
  "time": 1748563200,
  "webhook_delivery_attempt": 1,
  "data": {
    "application_id": "app_01abc23def456",
    "object": {
      "id": "msg_01abc23def456",
      "grant_id": "grant_01abc23def456",
      "subject": "Weekly report",
      "from": [{ "name": "Alice", "email": "alice@example.com" }],
      "to": [{ "name": "Bob", "email": "bob@example.com" }],
      "date": 1748563200,
      "thread_id": "thread_01abc23def456",
      "unread": true,
      "starred": false,
      "folders": ["INBOX"]
    }
  }
}
```

The `webhook_delivery_attempt` field increments on each retry attempt. A value greater than 1 means Nylas is retrying. Your handler should use `id` as the dedupe key and skip duplicate processing if the event was already handled successfully.

## How do you create a webhook with multiple triggers?

The [`nylas webhook create`](https://cli.nylas.com/docs/commands/webhook-create) command registers a public HTTPS endpoint and binds it to one or more trigger types. Pass a comma-separated list to `--triggers`. A single endpoint can subscribe to every available trigger type, or you can separate concerns across multiple endpoints. Registration is instant and the endpoint starts receiving events immediately.

```bash
# Install Nylas CLI
brew install nylas/nylas-cli/nylas

# Authenticate with your API key
nylas auth config --api-key YOUR_API_KEY

# Create a webhook for email and calendar events
nylas webhook create \
  --url https://api.example.com/nylas/webhooks \
  --triggers "message.created,event.updated"

# Subscribe to all 4 categories at once
nylas webhook create \
  --url https://api.example.com/nylas/webhooks \
  --triggers "message.created,message.updated,event.created,event.updated,grant.expired"

# List active webhooks to confirm registration
nylas webhook list
```

## How does Nylas retry failed webhook deliveries?

Nylas retries a non-200 (or timed-out) delivery two more times — three attempts in total — backing off exponentially, with the final attempt landing 10–20 minutes after the first. According to the [Nylas webhooks documentation](https://developer.nylas.com/docs/v3/notifications/webhooks/), your endpoint must return a `200 OK` response within 10 seconds or Nylas counts the attempt as failed. Separately, if a destination misses about 95% of notifications over a 72-hour window, Nylas marks the endpoint as failed and stops sending to it.

Two rules follow from this. First, your handler should acknowledge immediately and process asynchronously — enqueue the event, return `200`, and do the work in a background job. Second, your handler must be idempotent because Nylas can deliver the same event more than once. The `id` field in the payload is stable across retries, making it the right dedupe key.

### Retry behavior at a glance

| Property | Value |
| --- | --- |
| Success condition | `200 OK` within 10 seconds |
| Retry attempts | 3 total (initial + 2 retries) |
| Backoff strategy | Exponential; final attempt 10–20 min after the first |
| Dedupe key | `id` field in payload root |
| Attempt counter | `webhook_delivery_attempt` in payload |
| After 3 failed attempts | Notification is skipped; ~95% loss over 72h disables the endpoint |

## How does HMAC signature verification work?

Nylas signs every webhook request with HMAC-SHA256 using your webhook secret. The signature appears in the `X-Nylas-Signature` request header. To verify it, compute HMAC-SHA256 of the *raw request body* bytes using the same secret, then compare the result to the header value with a constant-time comparison. If the signature doesn't match, reject the request before parsing JSON or touching any database.

The raw-body requirement is strict: JSON whitespace and field order affect the byte sequence, so re-serializing a parsed object before verifying will fail. The CLI [`nylas webhook verify`](https://cli.nylas.com/docs/commands/webhook-verify) command handles this correctly. Use it locally during development to confirm your secret and payload pair before wiring the same logic into your app.

```bash
# Save raw payload to a file (before JSON parsing)
# Then verify the signature from the X-Nylas-Signature header
export NYLAS_WEBHOOK_SECRET="your_webhook_secret"
export NYLAS_SIGNATURE="header_value_from_request"

nylas webhook verify \
  --payload-file payload.raw.json \
  --signature "$NYLAS_SIGNATURE" \
  --secret "$NYLAS_WEBHOOK_SECRET"
```

A Node.js app should use `express.raw()` to capture the buffer before any middleware touches it. The same applies in Python: read `request.get_data()` before calling `request.get_json()`. Catching a signature mismatch before the payload is parsed catches forged requests, replayed events with stale secrets, and middleware that normalizes whitespace. The HMAC-SHA256 verification is computationally trivial, so there's no performance reason to skip it.

## How do you test webhooks during local development?

Testing webhooks locally requires 3 things: a public tunnel to your localhost, a registered endpoint pointing at that tunnel, and a way to replay signed payloads without waiting for real provider events. The CLI provides all 3 in a single command group. Starting the local receiver takes seconds and works on any port.

The [`nylas webhook server`](https://cli.nylas.com/docs/commands/webhook-server) command starts a local HTTP listener and optionally spins up a Cloudflare tunnel to expose it publicly. Once the tunnel URL is ready, register it with [`nylas webhook create`](https://cli.nylas.com/docs/commands/webhook-create) and use [`nylas webhook test send`](https://cli.nylas.com/docs/commands/webhook-test-send) to deliver a test event to the endpoint.

```bash
# Start a local receiver on port 3000 with a Cloudflare tunnel
export NYLAS_WEBHOOK_SECRET="local_dev_secret"
nylas webhook server \
  --port 3000 \
  --secret "$NYLAS_WEBHOOK_SECRET" \
  --tunnel cloudflared

# In a second terminal: register the tunnel URL as a webhook endpoint
nylas webhook create \
  --url https://your-tunnel-subdomain.trycloudflare.com/webhook \
  --triggers "message.created,event.updated"

# Send a test event to the registered endpoint
nylas webhook test send https://your-tunnel-subdomain.trycloudflare.com/webhook

# Generate a static fixture for unit tests
nylas webhook test payload message.created --json > fixtures/message.created.json
```

For a complete walkthrough of the local development loop, including CI fixture patterns and idempotency tests, see the [local webhook testing guide](https://cli.nylas.com/guides/test-email-webhooks-locally).

## What are the essential webhook handler rules?

Four rules keep webhook handlers reliable in production: respond fast, verify first, deduplicate with the event ID, and process asynchronously. Skipping any one of them causes problems that only appear under load or during retry storms. Nylas's [official webhook guide](https://developer.nylas.com/docs/developer-guide/webhooks/) requires responding within 10 seconds — slow handlers are the most common cause of missed deliveries.

- **Respond in <10 seconds.** Return `200 OK` before doing any meaningful work. Enqueue the verified event and return immediately. Slow handlers trigger exponential backoff retries that can flood your endpoint.
- **Verify the signature before parsing.** Reject any request with an invalid or missing `X-Nylas-Signature` header before touching the body. Never parse JSON from an unverified source.
- **Deduplicate on event ID.** Store the `id` field after processing. If the same ID arrives again, skip the side effects. This prevents duplicate tickets, duplicate notifications, and duplicate agent tasks during retry windows.
- **Handle `grant.expired` explicitly.** This trigger means you can no longer sync the account. Your app should notify the user to re-authenticate rather than continuing to call the API and collecting 401 errors.

## How do you list, update, and delete webhooks?

After creating a webhook, you'll need to manage it over time — updating the URL when you change infrastructure, adding new trigger types as features grow, or deleting test endpoints. Each Nylas application can register multiple webhook endpoints, each with its own URL and trigger set. The CLI provides dedicated subcommands for each operation. Running [`nylas webhook list`](https://cli.nylas.com/docs/commands/webhook-list) first gives you the IDs and current state for every registered endpoint.

```bash
# List all webhooks with full IDs
nylas webhook list --full-ids --json

# Update a webhook — add a new trigger type
nylas webhook update --id wh_abc123 \
  --triggers "message.created,message.updated,event.updated"

# Delete a test webhook
nylas webhook delete --id wh_abc123 --yes
```

## Next steps

- [Test email webhooks locally](https://cli.nylas.com/guides/test-email-webhooks-locally) — set up a tunnel, replay signed fixtures, and move tests into CI
- [Parse inbound email webhooks](https://cli.nylas.com/guides/parse-inbound-email-webhooks) — normalize `message.created` payloads and route message IDs to workers
- [Receive inbound email](https://cli.nylas.com/guides/receive-inbound-email-cli) — create managed addresses and map them to webhook triggers
- [Build an email agent with the CLI](https://cli.nylas.com/guides/build-email-agent-cli) — wire `message.created` webhooks into an agent workflow
- [Command reference](https://cli.nylas.com/docs/commands) — full flags and examples for every webhook subcommand
