Guide

Test Email Webhooks Locally

Local email webhook testing fails when the tunnel, trigger list, signature secret, and payload shape are tested as separate problems. This guide gives you a single 6-step loop for running a local receiver, registering message triggers, verifying signatures, replaying payloads, and moving the same checks into CI.

Written by Qasim Muhammad Staff SRE

Reviewed by Nick Barraclough

VerifiedCLI 3.1.10 · last tested May 14, 2026

Webhooks are the safer alternative to polling when your app needs to react to new email, message updates, or calendar changes. The official Nylas webhooks documentation covers the platform contract; this guide focuses on the local developer loop that catches broken routes, missing tunnels, and signature mistakes before production.

For the local network path, read Cloudflare's TryCloudflare tunnel guide. For signature implementation details in Node services, compare your handler with crypto.createHmac before you parse or mutate the raw body.

How do you test email webhooks in 3 local modes?

A useful local webhook loop has 3 modes: a receiver mode that prints inbound events, a fixture mode that gives you deterministic payloads, and a signature mode that proves your handler rejects unsigned or tampered requests. Testing only one mode leaves gaps. A tunnel can be healthy while signature verification is broken, and a static fixture can pass while the public callback URL is not reachable.

The CLI gives you all 3 modes from one command group. Use webhook server while you build the route, webhook test payload when you need stable JSON for unit tests, and webhook verify when you need to prove your HMAC check is wired correctly. The full command reference lists every webhook subcommand.

How do you start 1 signed receiver on port 3000?

Start with a local server even if your real app is already running in Next.js, Rails, Express, or FastAPI. The standalone receiver separates webhook delivery from your application logic, which makes tunnel and signature failures easier to isolate in the first 5 minutes.

Pick a shared secret before starting the receiver. The same secret should be used by your app handler and by any replay tests you add later, so keep it in an environment variable instead of pasting it into source files.

export NYLAS_WEBHOOK_SECRET="local_webhook_secret_32_chars"

nylas webhook server \
  --port 3000 \
  --secret "$NYLAS_WEBHOOK_SECRET" \
  --tunnel cloudflared

The command prints a public tunnel URL and listens locally at http://localhost:3000/webhook. Keep that terminal open; every inbound payload should appear there before you involve your application framework.

Which 5 webhook commands should every handler test?

A handler is not ready because it returns 200 once. It is ready when 5 checks pass: trigger discovery, endpoint registration, sample payload generation, signed delivery, and signature verification. Those checks map directly to 5 CLI commands.

Run this sequence from a clean shell after your receiver is listening. It uses email triggers first because message webhooks are the most common production path, then it leaves space for calendar or contact triggers once the route is stable.

# 1. Discover available message triggers
nylas webhook triggers --category message --json

# 2. Register a public HTTPS endpoint
nylas webhook create \
  --url https://example-tunnel.trycloudflare.com/webhook \
  --triggers message.created,message.updated \
  --description "local-email-webhook-test"

# 3. Generate a deterministic sample payload
nylas webhook test payload message.created --json > message.created.json

# 4. Send a test event to the callback URL
nylas webhook test send https://example-tunnel.trycloudflare.com/webhook

# 5. Verify a captured signature against the raw body
nylas webhook verify \
  --payload-file message.created.json \
  --signature "$NYLAS_SIGNATURE" \
  --secret "$NYLAS_WEBHOOK_SECRET"

The important split is between payload generation and delivery. Fixture JSON belongs in unit tests. Public delivery belongs in integration tests because it checks DNS, TLS, path routing, and the tunnel.

How do you register 2 email triggers safely?

Begin with 2 triggers: message.created and message.updated. They cover the two paths most email applications care about first: a new inbound message and a later state change such as read, unread, starred, moved, or metadata update. Adding every trigger on day 1 makes local logs noisy and hides the one event your handler fails to parse.

Use a narrow description so you can delete the local webhook later without touching production endpoints. Check the list after registration and copy the returned ID into your cleanup notes.

nylas webhook create \
  --url https://example-tunnel.trycloudflare.com/webhook \
  --triggers message.created,message.updated \
  --description "local email webhook smoke test"

nylas webhook list --full-ids --json

If your app also syncs calendars, add event.created and event.updated only after message events are green. The payload families are different enough that one handler should not assume the other shape.

How do you verify 1 signature before parsing JSON?

Signature verification must run before JSON parsing, database writes, queue publishes, or agent prompts. That order matters because a webhook endpoint is public. A handler that parses first can still waste CPU or log private data before it discovers the request is not authentic.

In local development, capture the raw body and signature header from one request. Then verify the pair from the terminal before wiring the same secret into application code.

export NYLAS_SIGNATURE="signature_header_from_request"

nylas webhook verify \
  --payload-file message.created.json \
  --signature "$NYLAS_SIGNATURE" \
  --secret "$NYLAS_WEBHOOK_SECRET"

Once this passes, add a negative test that changes 1 byte in the body and expects verification to fail. That single mutation catches a surprising class of bugs where middleware normalizes whitespace, reorders fields, or parses and reserializes the request before signature validation.

How do tunnel tests differ from production in 3 ways?

Tunnel tests differ from production in 3 ways: the hostname changes often, the tunnel process can stop when your laptop sleeps, and the local handler usually lacks production middleware. A passing tunnel test proves reachability and parsing, not production readiness.

Treat the tunnel as a delivery lab. It is the right place to confirm that the callback path is correct, the handler returns a success response, and signature verification uses the raw body. It is not the right place to prove production TLS configuration, queue retries, or database idempotency. Those need a deployed environment with the same reverse proxy and middleware stack the final handler will use.

Keep both tests in the release checklist. A local tunnel catches route and payload mistakes in under 5 minutes. A production smoke test catches deployment mistakes such as missing secrets, wrong path prefixes, and load balancer body limits.

How do you avoid 4 webhook fixture mistakes?

Avoid 4 fixture mistakes: hand-writing payloads, deleting unknown fields, testing only the happy path, and signing a normalized JSON string instead of the raw body. These mistakes create tests that pass while the real webhook fails.

Generate fixtures with nylas webhook test payload so field names and event shapes start from the CLI, not from memory. Keep at least one fixture with unknown fields because production payloads can grow over time. Keep one malformed fixture to prove parser errors return a controlled response. Keep one bad-signature fixture to prove the handler rejects unauthenticated requests before parsing.

Fixture files should be small enough to review in a pull request. A 40-line sample that covers one trigger is more useful than a 700-line payload nobody reads. If a new trigger is added, add a new fixture instead of overloading the old one.

How do you replay 3 payload paths in CI?

CI should replay 3 paths: a valid message payload, a malformed payload, and a valid payload with a bad signature. That gives you coverage for happy path behavior, parser failure handling, and auth rejection without waiting for a provider-side event.

Save the sample payload as a fixture, then post it to the local app during CI. The exact HTTP command depends on your framework, but the CLI still owns fixture generation and signature checks.

nylas webhook test payload message.created --json > test/fixtures/message.created.json

# Your app's CI step can post the fixture to a local route.
curl -sS \
  -X POST http://127.0.0.1:3000/webhook \
  -H "content-type: application/json" \
  --data-binary @test/fixtures/message.created.json

Keep the fixture small and explicit. A fixture that mirrors 1 real trigger is easier to update than a hand-written mega-payload that pretends to cover every provider shape.

Add one idempotency assertion to the CI replay as well. Post the same valid fixture twice and confirm your handler creates 1 downstream job, not 2. Webhook retry behavior is normal in production, so a handler that only passes first-delivery tests can still duplicate notifications, tickets, or agent tasks during a network retry.

What are the 4 fastest local webhook debug checks?

When nothing arrives, debug in this order: local listener, public tunnel, registered URL, then trigger selection. The order prevents wasted time in provider settings when the real issue is a closed terminal or a typo in the callback path.

Keep the first debug pass under 10 minutes by testing one layer at a time. Restarting the tunnel, changing the webhook registration, and editing handler code in the same pass creates 3 moving parts. A good local session changes exactly 1 layer, sends 1 test payload, and writes down the result before moving to the next layer.

  • Listener: confirm the terminal running nylas webhook server is still open and bound to the expected port.
  • Tunnel: open the public URL and confirm it reaches the local process instead of a generic tunnel error page.
  • Registration: run nylas webhook list --full-ids --json and verify the callback URL matches the current tunnel, not yesterday's URL.
  • Triggers: run nylas webhook triggers --category message --json and confirm the event type you expect is actually registered.

What are the 4 next steps after local webhook tests pass?

Move from local to production in 4 steps: replace the tunnel URL with an HTTPS production URL, store the webhook secret in your secret manager, keep fixture tests in CI, and document the exact triggers your handler supports. Do not leave a temporary tunnel webhook registered after the test.

For related command surfaces, keep nylas webhook server, nylas webhook create, nylas webhook test send, nylas webhook verify, and the full command reference nearby. If the webhook will wake an AI agent, pair this page with the MCP email server security checklist before you allow write actions.

If webhook delivery is part of inbound mail setup, continue with Receive Inbound Email so the local test maps to a managed address and production trigger list.