# Local Webhook Development for Agent Inboxes

> Source: <https://dev.to/qasim157/local-webhook-development-for-agent-inboxes-jpl>
> Published: 2026-06-16 11:06:07+00:00

The first webhook integration I ever built failed before it received a single notification. The handler was solid — JSON parsing, logging, the lot — but the subscription itself showed up as failed seconds after I created it. The reason turned out to be boring: my endpoint was `http://localhost:3000`

, and the platform calling it lives on the internet, where my laptop does not. Every email-agent developer hits some version of this on day one, so here's the local development loop that actually works.

The setup: you're building an agent on an [Agent Account](https://developer.nylas.com/docs/v3/getting-started/agent-accounts/) — a hosted mailbox your app owns, currently in beta — and inbound mail fires a `message.created`

webhook you need to handle. The webhook URL must be public HTTPS, because Nylas calls it from the internet within 10 seconds to verify it. Localhost fails that check by definition.

You need an address that mail can actually land on. The trial-domain route means no DNS work at all: register a `*.nylas.email`

subdomain from the Dashboard, then mint an account on it:

```
nylas agent account create test@your-application.nylas.email
```

The CLI prints the new grant ID — every webhook payload you receive will carry it, and every follow-up fetch will use it. For production you'd register your own domain with MX and TXT records, but for a local dev loop the trial domain is live immediately and disposable afterward.

Any HTTPS tunnel works — ngrok, Cloudflare Tunnel, your own reverse proxy. The shape is the same:

``` php
ngrok http 3000
# Forwarding: https://f3a1-203-0-113-7.ngrok-free.app -> http://localhost:3000
```

That public URL is what you register, with `/webhooks/nylas`

(or whatever your route is) appended.

Order matters here, and this is the part the failed-subscription story teaches. The moment you create a webhook, a verification request arrives — so the handler has to be up and tunneled *first*. The verification is a GET request carrying a `challenge`

query parameter, and your endpoint has 10 seconds to echo the exact value back in a `200 OK`

body. The rules are strict: raw value only, no quotes, no JSON wrapper, no chunked encoding. Get any of it wrong and the endpoint is marked failed on the first attempt, with no retry — you delete and recreate.

A minimal Express handler covers both the handshake and the notifications:

``` python
import express from "express";

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(); // acknowledge first
  console.dir(req.body, { depth: null }); // inspect everything while developing
});

app.listen(3000);
```

A passing handshake also generates your `webhook_secret`

— hold onto it, it's the key for verifying the `X-Nylas-Signature`

HMAC on every notification once you harden the handler.

One CLI command or one API call:

```
nylas webhook create \
  --url https://f3a1-203-0-113-7.ngrok-free.app/webhooks/nylas \
  --triggers message.created
```

If the handshake passes, the webhook goes active. Now send an email from your phone to the agent's address and watch the POST arrive in your terminal — usually within seconds of the message landing.

This is the payoff of local development: you see real notification bodies at full fidelity before writing any parsing logic. Things to notice while you're staring at them:

`data.object`

, which carries the `grant_id`

and message `id`

you'll need for follow-up fetches. The `application_id`

sits on `data`

, one level up.`.truncated`

suffix. You don't subscribe to that variant separately; your handler just needs a branch that re-fetches via `GET /v3/grants/{grant_id}/messages/{message_id}`

.`created`

and a later `updated`

can land out of order. Seeing this locally, before production, is exactly why this loop is worth setting up.The `res.status(200).end()`

*before* the logging isn't an accident either. Notifications share the 10-second response window, and acknowledging first means a slow downstream call — say, an LLM request you add later — never times out the delivery.

Once payload parsing works, add the signature check — locally, while you can still test failure paths by hand. Every notification carries an `X-Nylas-Signature`

header: a hex-encoded HMAC-SHA256 of the raw request body, signed with the `webhook_secret`

from the handshake. The catch in Express is that `express.json()`

consumes the raw body, so capture it in the parser's `verify`

hook:

``` python
import crypto from "crypto";

app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf; },
}));

app.post("/webhooks/nylas", (req, res) => {
  const expected = crypto
    .createHmac("sha256", process.env.NYLAS_WEBHOOK_SECRET)
    .update(req.rawBody)
    .digest("hex");
  if (expected !== req.headers["x-nylas-signature"]) {
    return res.status(401).end();
  }
  res.status(200).end();
  console.dir(req.body, { depth: null });
});
```

Curl your own tunnel with a garbage signature and confirm the 401. That two-minute test is much cheaper than discovering in production that anyone on the internet can make your agent send email.

Free tunnel tiers mint a new URL on every restart, and your webhook subscription points at the old one. Three ways to deal with it:

`nylas webhook update <WEBHOOK_ID>`

(find the ID with `nylas webhook list`

).`notification_email_addresses`

when creating the webhook and Nylas emails you when deliveries start failing, which is usually how you find out the tunnel died while you were at lunch.Either way, remember each new URL re-triggers the challenge handshake — handler up first, always.

For the full handler lifecycle, including signature verification and the delivery variants, the [new email webhook recipe](https://developer.nylas.com/docs/cookbook/use-cases/build/new-email-webhook/) is the reference I'd keep open in a tab.

Tonight's exercise: get the handshake passing, then email your agent and diff two consecutive payloads for the same thread. The structure you see is the schema you'll be coding against for the life of the project — better to meet it in your terminal than in production logs.
