# Inbound email webhook without changing your MX: how?

> Your own mailbox to a webhook: subject, HTML, up to 20 attachments and thread ids as signed JSON. No MX change, unlike SendGrid Inbound Parse.

Source: https://unlimitedmessaging.app/articles/email-inbound-webhook

Inbound email webhook

# Turn the emails in your own inbox  
_into parsed JSON on your server._

Inbound parse services want your MX records or a forwarding address. Connect the mailbox you already use instead: each new email arrives at your URL with its subject, HTML, attachments and thread, and you reply from that same address.

[Create a free account](https://app.unlimitedmessaging.app)[See paid plans](https://unlimitedmessaging.app/pricing)

The short answer

## How do you send incoming emails to a webhook?

Connect the mailbox itself. Unlimited Messaging logs into your existing mailbox over IMAP (Gmail with an app password, your own domain, any IMAP host), watches its inbox, and posts every new email to your URL as a signed JSON event, message.received, with subject, text, HTML, attachments and thread ids. Your MX records stay where they are, unlike [SendGrid Inbound Parse](https://www.twilio.com/docs/sendgrid/for-developers/parsing-email/setting-up-the-inbound-parse-webhook). Webhooks are on the paid Medium and Max plans.

Inbound parse services sit in front of the mail. SendGrid asks you to point the MX record of a dedicated hostname at mx.sendgrid.net, then posts multipart/form-data (or the raw MIME) to your URL. Postmark gives each server an address like yourhash@inbound.postmarkapp.com, which you forward to from Gmail or route with inbound domain forwarding, and its inbound processing comes with the Pro plan, from $16.50 a month. Either way the address your customers write to changes, or its mail is relayed.

Unlimited Messaging reads the mailbox your customers already write to. A bridge keeps one IMAP IDLE connection on its inbox, read-only, so a new email is picked up as soon as the server announces it. The email is stored, its attachments too (up to 20 per email), and your webhook receives one JSON body: externalId is the Message-ID, conversation.id the root of its thread, replyTo the message it answers.

The reply closes the loop: POST /message with replyToMessageId sends from the same mailbox, through its own SMTP server, with Re: and the In-Reply-To and References headers, and files a copy in its Sent folder. The thread stays one thread in your customer's mail app.

Before you connect: [set up Gmail over IMAP with an app password](https://unlimitedmessaging.app/articles/gmail-imap-smtp-app-password), and [why sending from your own mailbox matters](https://unlimitedmessaging.app/articles/send-from-your-own-mailbox-and-number).

![Illustration: a grid of small icons with one highlighted, with the headline “Your inbox, as JSON”.](https://unlimitedmessaging.app/_next/image?url=%2Fassets%2Farticles%2Fcovers%2Femail-inbound-webhook-en.png&w=3840&q=75)

Definition
Inbound email webhook

An HTTP POST that delivers each incoming email to your server as structured data (sender, subject, text, HTML, attachments, thread headers), so your code never has to speak IMAP or parse MIME.

Also searched as email to webhook, receive email API, email parsing webhook or inbound parse.

[Source: SendGrid, Inbound Parse webhook setup↗](https://www.twilio.com/docs/sendgrid/for-developers/parsing-email/setting-up-the-inbound-parse-webhook)

The events

## What does the inbound email webhook contain?

Email shares the envelope of every channel and adds three fields: subject, html and attachments.

1. 01  
### message.received  
A new email landed in the inbox. content is the text body, or the HTML converted to text when the email has no text part; html is the body as received, untrusted, to sanitise before you render it.
2. 02  
### attachments  
Every file in order, up to 20 per email: url (the stored copy), filename, mimeType, size, and contentId for an inline image the HTML references as cid:. The first one is also mirrored on mediaUrl.
3. 03  
### Threading  
externalId is the RFC 5322 Message-ID without angle brackets. conversation.id is the thread root (first References entry, else In-Reply-To, else the email itself), so every email of a thread shares it.
4. 04  
### message.sent and message.status  
Your replies are announced as message.sent, and a hard bounce turns into message.status UNDELIVERABLE. The bounced address is then paused for 30 days on that mailbox to protect its reputation.
5. 05  
### account.status\_changed  
The mailbox disconnected, for instance because the server refused the credentials after a password change. Subscribe to it so a silent inbox gets noticed.

POST https://example.com/webhooks/umJSON

```
{
  "type": "message.received",
  "eventId": "whd_9a4f2e61-7c3b-4d85-b0e9-5f1a8c2d7e34",
  "occurredAt": "2026-11-09T14:05:12.000Z",
  "data": {
    "message": {
      "id": "msg_3f8a1c2e-9d4b-4e7a-8b6c-2d1e0f9a8b7c",
      "externalId": "CAF8k2Lp9xQ3@mail.example.com",
      "direction": "IN",
      "fromMe": false,
      "content": "Hi, please find the signed quote attached. Claire",
      "sentAt": "2026-11-09T14:05:12.000Z",
      "receivedAt": "2026-11-09T14:05:12.000Z",
      "sender": "claire@example.com",
      "senderId": "claire@example.com",
      "senderName": null,
      "participant": null,
      "replyTo": { "externalId": "quote-1042.20261108@yourcompany.example", "participant": null },
      "mediaUrl": "https://api.unlimitedmessaging.app/message/media/stored/4c1d...",
      "mediaType": "document",
      "mimeType": "application/pdf",
      "mediaFilename": "quote-1042-signed.pdf",
      "transcript": null,
      "subject": "Re: Quote #1042",
      "html": "<p>Hi, please find the signed quote attached.</p><p>Claire</p>",
      "attachments": [
        {
          "url": "https://api.unlimitedmessaging.app/message/media/stored/4c1d...",
          "filename": "quote-1042-signed.pdf",
          "mimeType": "application/pdf",
          "size": 184320,
          "contentId": null
        }
      ]
    },
    "channel": {
      "type": "EMAIL",
      "account": { "id": "acc_8b2e4f6a-1c3d-4e5f-a7b9-0c1d2e3f4a5b", "name": "sales@yourcompany.example" },
      "conversation": {
        "id": "quote-1042.20261108@yourcompany.example",
        "isGroup": false,
        "address": "claire@example.com"
      }
    }
  }
}
```

A real message.received body for an email, built from the published schema (media URLs shortened). sender is the From address; senderName is not filled for email.

Set it up

## How do you parse incoming emails in Next.js?

Connect the mailbox, register an endpoint, verify each POST. No MIME parser, no IMAP library in your code.

1. 01  
### Connect your mailbox  
In the web app, add an email account with the address and its password (an app password for Gmail). IMAP and SMTP settings are detected from the address.
2. 02  
### Register the endpoint  
POST /webhooks with your URL and eventTypes, or add it on the Webhooks page of the dashboard. The secret (whsec\_...) is shown once.
3. 03  
### Verify, store, reply  
Check the webhook-signature header, upsert on eventId, fetch attachments from their url when you need them, reply with replyToMessageId.

POST /webhookscurl

```
curl https://api.unlimitedmessaging.app/webhooks \
  -H "Authorization: Bearer $UM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/um",
    "eventTypes": ["message.received", "account.status_changed"],
    "description": "Inbound email"
  }'

# The response carries "secret": "whsec_..." once. Store it as UM_WEBHOOK_SECRET.
```

Add accountIds to scope the endpoint to one mailbox, for instance support@ but not billing@. Without it, every account is covered.

app/api/webhooks/um/route.tsNext.js route handler

```
// app/api/webhooks/um/route.ts (Next.js App Router)
import crypto from 'node:crypto';
import { UnlimitedMessagingApiClient } from '@unlimited-messaging/sdk';

const um = new UnlimitedMessagingApiClient({ token: process.env.UM_API_KEY! });
const key = Buffer.from(process.env.UM_WEBHOOK_SECRET!.replace(/^whsec_/, ''), 'base64');

// Standard Webhooks: HMAC-SHA256 of "id.timestamp.rawBody", base64.
function verify(raw: string, id: string, ts: string, header: string) {
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = crypto.createHmac('sha256', key).update(`${id}.${ts}.${raw}`).digest('base64');
  return header.split(' ').some((entry) => {
    const sig = entry.startsWith('v1,') ? entry.slice(3) : '';
    return sig.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  });
}

export async function POST(req: Request) {
  const raw = await req.text(); // the raw body, before any JSON parsing
  const h = req.headers;
  if (!verify(raw, h.get('webhook-id') ?? '', h.get('webhook-timestamp') ?? '',
    h.get('webhook-signature') ?? '')) {
    return new Response('bad signature', { status: 401 });
  }

  const event = JSON.parse(raw);
  if (event.type === 'message.received' && event.data.channel.type === 'EMAIL') {
    const m = event.data.message;
    const thread = event.data.channel.conversation.id; // same id for the whole thread
    const files = m.attachments ?? []; // [{ url, filename, mimeType, size, contentId }]
    await saveTicket({ eventId: event.eventId, thread, from: m.sender, subject: m.subject,
      text: m.content, files }); // upsert on eventId: delivery is at least once

    // Answer in the same thread, from the same mailbox.
    await um.messages.messageSend({ replyToMessageId: m.id, text: 'Thanks, received. We reply within a day.' });
  }
  return new Response(null, { status: 200 }); // within 10 s; queue slow work
}
```

req.text() keeps the raw body the signature is computed over (Standard Webhooks, HMAC-SHA256 over id.timestamp.rawBody). saveTicket is yours. The reply goes out with Re: and the thread headers, from the same mailbox.

Vibe coding

## Building with Cursor, Claude Code, Lovable or Bolt? Paste this

Your AI editor gets the route, the signature check and the email fields it needs, without inventing a MIME parser.

Prompt to copy

Add an Unlimited Messaging webhook to this app so incoming emails from my own mailbox become support tickets.

1. Create a POST route (Next.js: app/api/webhooks/um/route.ts) that reads the RAW body with req.text().
2. Verify the Standard Webhooks signature: key = base64-decode(UM_WEBHOOK_SECRET without "whsec_"); expected = base64(HMAC-SHA256(key, `${webhook-id}.${webhook-timestamp}.${rawBody}`)); accept if any space-separated entry of the webhook-signature header equals `v1,${expected}` (constant-time compare); reject timestamps more than 5 minutes off; return 401 otherwise.
3. On type "message.received" with data.channel.type "EMAIL": upsert a ticket keyed on body.eventId with data.channel.conversation.id (thread id), data.message.sender, data.message.subject, data.message.content (text) and data.message.attachments ([{ url, filename, mimeType, size, contentId }]). Treat data.message.html as untrusted: sanitise before rendering.
4. To answer, install @unlimited-messaging/sdk, read UM_API_KEY from env, and call `client.messages.messageSend({ replyToMessageId: data.message.id, text })`: it replies in the same thread from the same mailbox.
5. Return 200 within 10 seconds; queue slow work.
6. Put UM_API_KEY and UM_WEBHOOK_SECRET in .env and .env.example.

Docs: https://docs.unlimitedmessaging.app (Webhooks). Register with POST https://api.unlimitedmessaging.app/webhooks and eventTypes ["message.received"].

What people build

## What can you build on inbound email?

Anything that today means someone copying an email into another tool.

### A helpdesk on support@

One ticket per conversation.id, every reply appended, answers sent from support@ in the same thread. No ticketing address to publish.

### AI triage and auto-reply

Give subject and content to an LLM, tag and route the email, or send a first answer with replyToMessageId and let a human take over.

1. 01  
### Invoices and documents in  
Pull the PDF from attachments\[\].url and send it to your accounting or OCR pipeline.
2. 02  
### Lead capture  
Turn contact-form notifications or direct emails into CRM leads, keyed on the sender address.
3. 03  
### One inbox with WhatsApp and SMS  
The same handler takes WhatsApp and SMS events: only data.channel.type changes.
4. 04  
### No-code flows  
Point the webhook at n8n, Make or Zapier and branch on subject or sender without a server.

The table

## Inbound email to webhook: SendGrid vs Postmark vs Unlimited Messaging

Parse services receive mail for a new address or hostname. Unlimited Messaging reads the mailbox you already have.

__Inbound email to webhook: SendGrid vs Postmark vs Unlimited Messaging__
| [SendGrid Inbound Parse](https://www.twilio.com/docs/sendgrid/for-developers/parsing-email/setting-up-the-inbound-parse-webhook)MX on a dedicated hostname | [Postmark Inbound](https://postmarkapp.com/developer/user-guide/inbound/configure-an-inbound-server)Inbound address or domain | Unlimited MessagingYour mailbox over IMAP, paid plans   |                                                              |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------ |
| Address that receives                                                                                                                                      | Any address on a hostname dedicated to parsing                                                                                | yourhash@inbound.postmarkapp.com, or forwarded to it    | Your existing address                                        |
| DNS change                                                                                                                                                 | MX record to mx.sendgrid.net                                                                                                  | None with forwarding; DNS for inbound domain forwarding | None                                                         |
| Body format                                                                                                                                                | multipart/form-data, or the raw MIME message                                                                                  | JSON; HTTP basic auth in the URL as protection          | JSON, signed per Standard Webhooks                           |
| Thread ids                                                                                                                                                 | In the headers you parse                                                                                                      | In the parsed headers                                   | conversation.id (thread root) and replyTo, ready to use      |
| Reply from the same address                                                                                                                                | Through SendGrid sending, set up separately                                                                                   | Through Postmark sending, set up separately             | replyToMessageId, from your mailbox's own SMTP, copy in Sent |
| Price                                                                                                                                                      | Part of a SendGrid plan                                                                                                       | Inbound on Pro, from $16.50 a month                     | Webhooks on Medium or Max; no per-email fee                  |
| WhatsApp and SMS in the same webhook                                                                                                                       | No                                                                                                                            | No                                                      | Yes, same envelope                                           |

Checked on 9 November 2026 on SendGrid's Inbound Parse setup guide and Postmark's inbound server guide and pricing page (links in the sources).

The catch

## What are the limits of a mailbox-to-webhook bridge?

Reading your own mailbox removes the DNS work. It brings the rules of that mailbox with it.

1. 01  
### Webhooks are a paid feature  
On the Free plan, incoming emails show up in the web app inbox, grouped by thread. Pushing them to your server needs Medium (€19 a month, or €17 a month billed yearly) or Max.
2. 02  
### Inbox only  
The bridge watches INBOX. An email your provider files as spam, or that a filter moves to another folder, is never seen.
3. 03  
### No Outlook or Microsoft 365 yet  
Outlook, Hotmail and Microsoft 365 require OAuth, which is planned but not available. Gmail works with an app password, which needs 2-Step Verification on the Google account.
4. 04  
### Up to 20 attachments per email  
Files beyond the 20th are not stored. The HTML is passed as received: sanitise it before you show it anywhere.
5. 05  
### At least once, not in order  
The same event can arrive twice, and events are not delivered in the order things happened: upsert on eventId and sort on sentAt.

How to choose

### A parse service or your own mailbox?

You receive mail for thousands of generated addresses (one per user, one per project) on a domain you control: an inbound parse service with an MX record is built for that. Your customers write to one or a few real addresses, and you want those emails in your code with replies from the same address: connect the mailbox and subscribe to message.received.

FAQ

## Questions about inbound email webhooks

Either point a domain's MX at a parse service (SendGrid, Postmark) or connect the mailbox over IMAP. Unlimited Messaging does the second: each new email in your inbox is posted to your URL as signed JSON, message.received, on the Medium or Max plan.

No. The mailbox keeps receiving mail as before; Unlimited Messaging reads it over IMAP and sends from it over SMTP. Nothing changes in your DNS.

Yes, with a Google app password: turn on 2-Step Verification, create the password at myaccount.google.com/apppasswords and paste it with your address. Mailboxes on your own domain and other IMAP/SMTP providers work too; Outlook and Microsoft 365 do not yet.

Each email lists its files in attachments, up to 20: a url serving the stored copy, filename, mimeType, size and contentId for inline images. Fetch the url when you need the bytes.

Use data.channel.conversation.id: it is the Message-ID of the thread's first email, taken from References, so every reply carries the same value. replyTo.externalId names the exact email being answered.

Yes, per Standard Webhooks: webhook-id, webhook-timestamp and webhook-signature headers, with an HMAC-SHA256 of id.timestamp.rawBody keyed by your whsec\_ secret. Reject timestamps more than 5 minutes off.

429, 5xx, timeouts and network errors are retried after 30 s, 2 min, 10 min, 1 h, 6 h, 1 day and 2 days: 8 attempts over a little more than 3 days. Other 4xx responses are final.

Yes. POST /message with replyToMessageId set to data.message.id sends from the same mailbox, with Re: and the In-Reply-To and References headers, and keeps a copy in its Sent folder.

Sources

## Sources & references

To check the information in this article for yourself:

1. [01Setting up the Inbound Parse webhook (MX to mx.sendgrid.net, payload)↗Twilio SendGrid Docs](https://www.twilio.com/docs/sendgrid/for-developers/parsing-email/setting-up-the-inbound-parse-webhook)
2. [02Configure an inbound server (inbound address, JSON, forwarding)↗Postmark](https://postmarkapp.com/developer/user-guide/inbound/configure-an-inbound-server)
3. [03Pricing (inbound processing on Pro, from $16.50 a month)↗Postmark](https://postmarkapp.com/pricing)
4. [04RFC 5322, Internet Message Format (Message-ID, In-Reply-To, References)↗IETF](https://www.rfc-editor.org/rfc/rfc5322)
5. [05Sign in with app passwords↗Google Account Help](https://support.google.com/accounts/answer/185833)
6. [06Standard Webhooks specification↗standardwebhooks.com](https://www.standardwebhooks.com)
7. [07Unlimited Messaging API reference - Webhooks↗Unlimited Messaging](https://docs.unlimitedmessaging.app)

![Ilian, senior software engineer](https://ilianazz.com/images/iliazz.png)

Written by

[Ilian](https://unlimitedmessaging.app/authors/ilian) · Senior Software Engineer

A senior software engineer, he founds and runs several tech companies. He builds products around messaging APIs, automation and AI integration, and shares here what he learns while building.

[View LinkedIn profile↗](https://www.linkedin.com/in/ilianAzz/)Updated November 9, 2026

![](https://unlimitedmessaging.app/assets/logos/whatsapp.svg)![](https://unlimitedmessaging.app/assets/logos/email.svg)![](https://unlimitedmessaging.app/assets/logos/sms.svg)

## Your inbox, as clean JSON.

Create an account, connect your mailbox, and turn on webhooks with Medium or Max. Sending email stays free on every plan.

[Create a free account→](https://app.unlimitedmessaging.app)

Similar articles
* [![](https://unlimitedmessaging.app/_next/image?url=%2Fassets%2Farticles%2Fcovers%2Fone-webhook-whatsapp-email-sms-en.png&w=3840&q=75)One webhook for WhatsApp, email and SMS: how far does it go?Every reply on your number, mailbox and phone in one signed JSON envelope: build a support inbox, an AI responder or a CRM sync.](https://unlimitedmessaging.app/articles/one-webhook-whatsapp-email-sms)
* [![](https://unlimitedmessaging.app/_next/image?url=%2Fassets%2Farticles%2Fcovers%2Fgmail-imap-smtp-app-password-en.png&w=3840&q=75)Connect Gmail by IMAP/SMTP in 3 steps with an app passwordGmailConnect your own Gmail over IMAP/SMTP in three steps with a Google app password, free on every plan.](https://unlimitedmessaging.app/articles/gmail-imap-smtp-app-password)
* [![](https://unlimitedmessaging.app/_next/image?url=%2Fassets%2Farticles%2Fcovers%2Fsend-from-your-own-mailbox-and-number-en.png&w=3840&q=75)Why your own mailbox and number beat a shared senderWhy sending from your own address and WhatsApp number beats a shared sender: your reputation, your identity, your replies, free on every plan.](https://unlimitedmessaging.app/articles/send-from-your-own-mailbox-and-number)
