Source: https://cli.nylas.com/guides/email-to-webhook-relay

# Relay Inbound Email to a Webhook

Your service wants email as a webhook, but mail still arrives over IMAP. The bridge is usually a paid inbound-parse provider that charges per message. The CLI hands you each message as JSON; a few lines of shell POST it to your endpoint with an HMAC signature so the receiver can trust it. This guide builds an email-to-webhook relay you control, fired by a message.created trigger or a polling search loop, with no per-message fee.

Written by [Hazik](https://cli.nylas.com/authors/hazik) Director of Product Management

Reviewed by [Qasim Muhammad](https://cli.nylas.com/authors/qasim-muhammad)

Updated June 9, 2026

> **TL;DR:** Pull each new message with `nylas email search --json` (or fire on a `message.created` webhook), then `curl -X POST` the JSON to your endpoint with an `X-Signature` HMAC header so the receiver can verify it. The CLI handles the inbox across six providers; your shell handles the relay. One small detail below keeps a retried delivery from creating duplicates downstream.

Command references used in this guide: [`nylas email search`](https://cli.nylas.com/docs/commands/email-search), [`nylas webhook create`](https://cli.nylas.com/docs/commands/webhook-create), and [`nylas webhook verify`](https://cli.nylas.com/docs/commands/webhook-verify).

## What does relaying email to a webhook mean?

Relaying email to a webhook means forwarding each inbound message to an HTTP endpoint as a JSON payload, so your service receives mail the same way it receives any other event. Instead of polling an IMAP mailbox, your receiver gets a `POST` per message and replies with a 2xx status to acknowledge it.

Two pieces make the relay. The CLI turns a provider mailbox into structured JSON across Gmail, Outlook, Yahoo, iCloud, and any IMAP host, so you write one relay instead of one parser per provider. Your shell then signs and ships that JSON. Inbound-parse vendors typically bill per message once you pass a free tier of a few hundred; a relay you run yourself has no per-message fee. The first step is getting a single message as JSON.

```bash
# Pull the most recent inbound messages as JSON
nylas email search "*" --in INBOX --after 2026-06-09 --json --limit 25 > inbox.json
```

## How do I relay each message with a search loop?

Relay with a search loop by reading the JSON array one message at a time and POSTing each object to your endpoint. The `nylas email search` command returns an array of message objects; piping it through `jq -c '.[]'` emits one compact JSON line per message, which a `while read` loop forwards with `curl`. A 25-message batch relays in a couple of seconds.

Each POST sends the raw message object as the request body and sets a JSON content type. Send the message `id` in a header too, so the receiver can de-duplicate on it. The loop below is the core relay; the next section adds the signature that lets the receiver trust the request came from you and not a stranger who guessed the URL.

```bash
ENDPOINT="https://your-service.example.com/inbound"
jq -c '.[]' inbox.json | while read -r msg; do
  msg_id=$(echo "$msg" | jq -r '.id')
  curl -s -X POST "$ENDPOINT" \
    -H "Content-Type: application/json" \
    -H "X-Message-Id: $msg_id" \
    --data-binary "$msg"
done
```

## How do I sign the webhook with HMAC?

Sign the webhook by computing an HMAC-SHA256 of the exact request body with a shared secret, then sending it in an `X-Signature` header. The receiver recomputes the same HMAC over the body it received and compares — if the values match, the payload is authentic and untampered. HMAC is defined in [RFC 2104](https://datatracker.ietf.org/doc/html/rfc2104) and uses the SHA-256 hash from [RFC 6234](https://datatracker.ietf.org/doc/html/rfc6234).

The signing rule that matters: sign the byte-for-byte body you actually send, and verify against the byte-for-byte body received. Reformatting or re-encoding the JSON between sign and verify breaks the comparison, because a single changed byte produces a completely different 256-bit digest. Compute the hash with `openssl`, prefix it with `sha256=` by convention, and attach it as a header on the same `curl` call.

```bash
WEBHOOK_SECRET="$RELAY_SECRET"   # 32+ random bytes, shared with the receiver
jq -c '.[]' inbox.json | while read -r msg; do
  msg_id=$(echo "$msg" | jq -r '.id')
  sig=$(printf '%s' "$msg" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $2}')
  curl -s -X POST "$ENDPOINT" \
    -H "Content-Type: application/json" \
    -H "X-Message-Id: $msg_id" \
    -H "X-Signature: sha256=$sig" \
    --data-binary "$msg"
done
```

## How do I trigger the relay in real time?

Trigger the relay in real time by registering a `message.created` webhook with the CLI instead of polling on a timer. Nylas then notifies your endpoint within seconds of a message landing, rather than on the next cron tick. Register it once with `nylas webhook create`, passing your public URL and the `message.created` trigger.

Nylas signs its own notifications, so your receiver verifies the `X-Nylas-Signature` header before acting. The `nylas webhook verify` command checks a captured payload against that signature and your webhook secret, which is the fastest way to confirm your verification logic matches before you wire it into a live handler. Nylas's notification contract is documented in the [Nylas notifications reference](https://developer.nylas.com/docs/v3/notifications/).

```bash
# Register a real-time trigger for new mail
nylas webhook create \
  --url https://your-service.example.com/inbound \
  --triggers message.created \
  --description "Inbound email relay"

# Confirm your verification logic against a captured payload
nylas webhook verify \
  --payload-file event.json \
  --signature "$X_NYLAS_SIGNATURE" \
  --secret "$NYLAS_WEBHOOK_SECRET"
```

## Why do retries need idempotency?

Retries need idempotency because webhook delivery is at-least-once, not exactly-once. If your endpoint is slow or returns a 5xx, the sender retries, and the same message can arrive two or three times. Without a de-duplication key, each retry creates a duplicate record downstream — the open loop from the TL;DR.

Solve it with the message `id` the relay already sends in `X-Message-Id`. Have the receiver treat that ID as a unique key: on a repeat ID, return 200 without re-processing. This is why a relay should respond fast and acknowledge before doing slow work — both Gmail's push notifications and Microsoft Graph subscriptions expect a quick 2xx and will retry on timeout, as described in the [Gmail API push docs](https://developers.google.com/workspace/gmail/api/guides/push) and the [Microsoft Graph subscription reference](https://learn.microsoft.com/en-us/graph/api/resources/subscription). A relay that does its slow work asynchronously stays under the retry timeout.

## Next steps

- [Parse inbound email webhooks](https://cli.nylas.com/guides/parse-inbound-email-webhooks) — verify and unpack Nylas notifications on the receiving end
- [Receive inbound email from the CLI](https://cli.nylas.com/guides/receive-inbound-email-cli) — capture mail locally before relaying it
- [Send email to Slack](https://cli.nylas.com/guides/email-to-slack-notifications) — relay into a Slack channel instead of a custom endpoint
- [Index email in Elasticsearch](https://cli.nylas.com/guides/email-to-elasticsearch) — relay the same JSON into a search index
- [Store email in SQLite](https://cli.nylas.com/guides/email-to-sqlite) — persist relayed messages to a local database
- [Full command reference](https://cli.nylas.com/docs/commands) — every flag and subcommand documented
