Guide
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 Software Engineer
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 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. Pass --category to filter by object type and --json to pipe the output into a script or config file.
# 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 --jsonTrigger reference table
Run nylas webhook triggers for the latest list. Use the exact string when passing --triggers to nylas 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 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.
{
"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 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.
# 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 listHow 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, 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 command handles this correctly. Use it locally during development to confirm your secret and payload pair before wiring the same logic into your app.
# 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 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 and use nylas webhook test send to deliver a test event to the endpoint.
# 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.jsonFor a complete walkthrough of the local development loop, including CI fixture patterns and idempotency tests, see the local webhook testing guide.
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 requires responding within 10 seconds — slow handlers are the most common cause of missed deliveries.
- Respond in <10 seconds. Return
200 OKbefore 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-Signatureheader before touching the body. Never parse JSON from an unverified source. - Deduplicate on event ID. Store the
idfield 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.expiredexplicitly. 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 first gives you the IDs and current state for every registered endpoint.
# 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 --yesNext steps
- Test email webhooks locally — set up a tunnel, replay signed fixtures, and move tests into CI
- Parse inbound email webhooks — normalize
message.createdpayloads and route message IDs to workers - Receive inbound email — create managed addresses and map them to webhook triggers
- Build an email agent with the CLI — wire
message.createdwebhooks into an agent workflow - Command reference — full flags and examples for every webhook subcommand