Guide
Parse Inbound Email Webhooks into JSON
Turn each inbound email webhook into a signed JSON event. Register message.created, verify the raw payload, then read the message when your handler needs full body or attachment data.
Written by Qasim Muhammad Staff SRE
Reviewed by Qasim Muhammad
What does an inbound email webhook need to do?
An inbound email webhook should do three jobs in order: prove the request came from Nylas, normalize the event into the fields your app needs, and fetch the full email only when the webhook payload is not enough.
This keeps your handler fast. The webhook wakes the workflow, and the CLI or API read step pulls the full body, headers, and attachments when needed.
1. Register message.created
Create the endpoint with nylas webhook create and the public HTTPS URL your app exposes. Use a separate endpoint per environment so staging and production never share a secret.
nylas webhook create \
--url "https://api.example.com/nylas/webhooks" \
--triggers message.created \
--description "Inbound email parser" \
--jsonFor local development, pair this with the local webhook testing guide so the endpoint uses a tunnel and signed fixtures before you deploy it.
2. Verify the raw webhook body
Verification must use the exact raw body that arrived over HTTP. Do not stringify parsed JSON and verify that copy; whitespace and field order matter.
nylas webhook verify \
--payload-file payload.raw.json \
--signature "$X_NYLAS_SIGNATURE" \
--secret "$NYLAS_WEBHOOK_SECRET"In an Express app, keep the raw buffer and parse JSON only after the signature check passes.
import express from "express"
import { execFileSync } from "node:child_process"
import { writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
const app = express()
app.post(
"/nylas/webhooks",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = String(req.header("x-nylas-signature") ?? "")
const payloadPath = join(tmpdir(), "nylas-webhook-payload.json")
writeFileSync(payloadPath, req.body)
execFileSync("nylas", [
"webhook",
"verify",
"--payload-file",
payloadPath,
"--signature",
signature,
"--secret",
process.env.NYLAS_WEBHOOK_SECRET!,
])
const event = JSON.parse(req.body.toString("utf8"))
res.status(202).json({ ok: true, id: event.id })
}
)3. Route the message event
After verification, normalize the fields you need. Most inbound processors store the event ID, grant ID, message ID, timestamp, and trigger name, then hand the message ID to a worker.
type InboundEmailEvent = {
trigger: "message.created"
messageId: string
grantId: string
}
function normalizeWebhook(event: any): InboundEmailEvent {
return {
trigger: event.type,
messageId: event.data.object.id,
grantId: event.data.object.grant_id,
}
}The worker can call nylas email read with the message ID when it needs full body HTML, plain text, headers, or MIME output.
What contract should an inbound email webhook expose?
An inbound email webhook should become a small internal contract. The rest of your system should not need to know the full provider payload shape. After verification, normalize the trigger name, grant ID, message ID, mailbox address, received timestamp, and any routing fields your app needs. Store the raw event separately for audit and replay.
That contract keeps workers simple. A parser worker can receive `{ messageId, grantId, trigger }`, read the full message only when needed, and decide whether to classify, archive, respond, or hand the message to another system. If every worker reads the provider payload directly, small schema changes become harder to contain.
Add a version to the internal event if the workflow will live for a long time. `inbound_email.v1` is enough. Versioning gives you room to add normalized fields later without breaking older workers. It also helps when replaying old webhook events from storage because the worker knows which contract it received.
Where should signature verification happen?
Verify the webhook at the edge of your application before any business logic runs. The handler should reject requests that do not have a valid signature, then pass only verified events to queues, workers, logs, or analytics. This keeps untrusted payloads from entering the rest of the system.
The raw body rule is strict because signatures are computed over bytes, not over the object your framework parses. If Express, Fastify, Next.js, or another framework parses and reserializes JSON first, the byte sequence can change. Preserve the raw body for verification, then parse JSON after the check passes.
Treat verification failures as security signals but avoid logging full payloads. Log the endpoint, timestamp, request ID, and reason. Do not dump message bodies, headers, or secrets into application logs. Inbound email can contain personal data, contracts, reset links, invoices, and other sensitive content.
Why should webhook handlers hand work to a queue?
Webhook handlers should respond quickly. The handler's job is to verify, normalize, store, enqueue, and return a success status. Reading the full message, downloading attachments, running AI classification, or calling a CRM can happen in a worker. That separation reduces webhook retries caused by slow downstream systems.
Use a dedupe key based on the webhook event ID or the message ID plus trigger. Providers can retry delivery when your endpoint times out or returns an error. Your queue consumer should be able to see that it already processed a message and skip duplicate side effects. This matters for auto-replies, ticket creation, and CRM updates.
Store the processing state. A simple table with `received`, `verified`, `queued`, `processed`, and `failed` states gives operators a clear path during incidents. If parsing fails because an attachment service is down, you can retry the worker without asking Nylas to resend the webhook.
When should the worker read the full message?
Read the full message only when the webhook payload does not contain enough information. Many routing decisions need only the message ID, grant ID, sender, and subject. Full body reads are useful when you need to parse text, extract attachments, classify intent, detect signatures, or build a support ticket.
Separate the raw message from the derived fields. Keep the message ID and provider metadata as source facts. Store parsed fields such as invoice number, company name, request type, or signature phone number as derived values. That makes it easier to rerun parsing logic later without losing the original reference.
If the worker downloads attachments, enforce size limits and file-type checks. Email is an untrusted input channel. A signed webhook proves the event came through Nylas, but it does not prove the sender is safe or the attachment is harmless. Treat attachments as external user uploads and scan or quarantine them according to your product rules.
What should you log for inbound email parsing?
Log identifiers, not full content. Useful fields include event ID, message ID, grant ID, trigger, endpoint version, processing state, and worker duration. Avoid logging message bodies, full headers, attachment contents, or authorization secrets. Inbound email often contains data that users did not expect to appear in logs.
Add metrics for delivery volume, verification failures, duplicate events, queue latency, full-message read failures, and parser failure categories. These numbers help you separate provider delivery issues from application parsing issues. If webhook volume drops to zero, the endpoint may be misconfigured. If verification failures spike, the secret or raw-body handling may be wrong.
Keep a replay path. Store enough event metadata to replay a normalized event into the worker after a code fix. A good replay path does not require resending the original HTTP request. It should enqueue the verified internal contract and let the worker read the current message state with `nylas email read` when needed.
How should inbound mail be routed after parsing?
Routing should happen after verification and normalization. The parser can inspect sender, recipient, subject, headers, and body markers, then choose a queue or handler. Common routes include support tickets, invoice processing, sales replies, approval workflows, bounce handling, and agent inbox tasks.
Keep routing rules ordered and explicit. A billing inbox may receive invoices, payment questions, and vendor onboarding mail. The first rule can catch known invoice senders, the second can catch subject patterns, and the fallback can create a manual review task. A clear fallback is better than dropping messages that do not match a narrow rule.
Record the route decision with the message ID. When a user asks why an email became a ticket or why it was ignored, the route log should show the rule name and parser version. That makes future tuning possible and prevents routing behavior from becoming tribal knowledge.
How should webhook workflows handle attachments?
Webhook payloads should wake attachment processing, not perform it inline. Attachments can be large, slow to download, or unsafe. The verified event should enqueue work, and the worker should download attachments with size limits, file-type checks, and a storage path that keeps raw files separate from parsed fields.
Do not assume attachment names are safe. Filenames can include unexpected characters, repeated names, or misleading extensions. Normalize names before storage and keep the original name as metadata. If the file moves into user-visible storage, apply the same validation rules you use for direct uploads.
Parse attachments only after the message route requires them. A support auto-reply may not need an attachment at all. An invoice workflow likely does. Skipping unnecessary downloads reduces cost, latency, and privacy exposure. It also makes webhook retries less risky because fewer side effects happen per event.
How do you test inbound webhook parsing?
Test the verifier with saved raw payload fixtures and signatures. One fixture should pass. Another should fail after a one-character body change. That proves the handler verifies bytes, not parsed JSON. Add a test that rejects missing signatures and missing secrets too.
Test the normalizer separately from HTTP handling. Feed it a representative message.created event and assert the internal contract. Then feed it a malformed event and assert a clear failure. This keeps parser logic easy to test without launching a web server or tunnel.
Finally, run one end-to-end test through a tunnel or preview endpoint. Send an email to the managed inbox, receive the webhook, verify it, enqueue it, read the message, and assert the route. That one test proves the moving parts work together without making every parser case depend on live delivery.
What failure modes should the handler expect?
Expect duplicate deliveries, late deliveries, malformed payloads, expired secrets, provider read failures, queue outages, and parser exceptions. The handler should return success only after it has safely recorded or queued the event. If it cannot do that, returning an error gives the sender a chance to retry.
Separate permanent failures from temporary failures. An invalid signature is permanent and should not be retried by your worker. A queue outage is temporary and may be retried by the webhook sender or your own infrastructure. A message read failure may be temporary if the message is not yet available or permanent if permissions are wrong.
Expose a manual recovery path. Operators should be able to find a message ID, inspect its processing state, and replay the normalized event. Without that path, small parser bugs become data loss incidents because the only copy of the workflow trigger was the original webhook request.
How should the parser evolve over time?
Inbound parsers usually start narrow and grow as real mail arrives. Add fields deliberately. Each new extracted value should have a source, confidence rule, and consumer. If no workflow uses the field, do not store it just because it is present in the message.
Keep parser versions in code and data. A message parsed by version one may produce different fields than the same message parsed by version three. Version labels make reprocessing safer because you can compare old and new output before replacing stored values.
Use production samples carefully. Real inbound mail is the best way to improve routing, but it can contain private data. Redact fixtures before committing them, or keep them in a secure test store. The parser needs realistic examples without turning emails into source-code artifacts.
Review parser changes with the teams that consume the output. Support, sales, finance, and security teams may all depend on different fields. A field that looks optional to engineering can be the value another team uses to route urgent mail.
Retire fields as deliberately as you add them. If a parser output stops being used, remove it from the internal contract after consumers migrate. Smaller event shapes are easier to reason about and easier to protect.
Publish parser changes with examples. A short before-and-after event sample helps downstream teams see exactly what changed without reading parser code. Include the trigger type, message ID, grant ID, extracted fields, and any confidence labels. That makes schema changes reviewable by the people who own the workflows that consume the parsed email.
References for this workflow
- Nylas webhooks documentation -- endpoint registration, delivery, and webhook headers
- Nylas webhook notification security -- why handlers should verify signed payloads
- Nylas Agent Account mailboxes -- how managed inboxes map to message events
Next steps
- Receive inbound email -- create managed addresses for inbound workflows
- Test email webhooks locally -- tunnel, fixtures, and signature tests
- Build an email agent with the CLI -- hand inbound messages to an agent workflow
- Command reference -- webhook and email read flags