How to receive email with a webhook and parse inbound mail
Published Updated 7 min readBy the Rasket team

What inbound parsing is
Inbound email parsing is a service accepting mail on your behalf, pulling it apart, and handing you the pieces as JSON. Instead of running a mail server, polling a mailbox over IMAP and decoding MIME yourself, you receive an HTTPS request carrying the sender, the recipients, the subject, the text and HTML parts and a list of attachments.
To receive email with a webhook you point one DNS record at the service and subscribe an endpoint to one event. That is the whole integration. What you do with the event — open a ticket, attach a reply to a thread, file a document — is the part that is actually your product.
How a message becomes an event
Four steps, none of which you operate.
- Somebody sends to an address at the domain you receive on. Their mail server looks up the MX record for that domain and finds ours.
- The message is accepted, checked, and stored. The raw source is kept, so nothing is lost by the parsing.
- It is parsed: headers, the text and HTML parts, and each attachment’s metadata.
- An
email.receivedevent is delivered to every endpoint subscribed to it, signed exactly as a delivery event is.
The signing is not an afterthought. Your endpoint is a public URL, and a webhook carrying an email is a webhook somebody would very much like to forge. The webhooks article covers the verification contract in full; the short version is that you read the raw bytes, verify, and parse afterwards.
Setting it up
- Verify the domain you want to receive on — Add the domain and publish the records Rasket generates for it. Receiving is turned on for a verified domain, so this step comes first even if you never send from it.
- Publish the MX record — Add the MX record for the label you want mail to arrive at. The default label is inbound, so anything@inbound.yourdomain.example reaches us once that record resolves.
- Subscribe a webhook to email.received — Create a webhook endpoint in the dashboard or over the API and subscribe it to the email.received event. Copy the signing secret it returns once; it is not shown again.
- Verify every delivery before you trust it — Read the raw request body as text, verify the signature against it, and only then parse the JSON. A body your framework has already parsed and re-serialised will not match the signature.
- Route on received_for — Read the received_for list, which holds the addresses of yours that the message was actually accepted for, and route on that rather than on the To header, which the sender controls.
- Fetch attachments when you need them — The event lists each attachment's filename, content type and id but carries no bytes. Ask the API for a signed link when you actually need the file, and delete the message when you are done with it.
The record itself is one row. The host on the right is generated for the domain and shown on its page, so copy it from there rather than from an article.
inbound.acme.example. MX 10 (the host the dashboard hands you)A label, not your whole domain
Receiving sits at inbound by default, so only mail addressed to that subdomain reaches us. Pointing the root domain at us instead is possible, and it takes every address on the domain with it — including the ones your team reads in a mail client. Pick that only for a domain whose every address should reach your application.
What the payload carries
{ "type": "email.received", "created_at": "2026-09-09T10:16:44.902Z", "data": { "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c", "created_at": "2026-09-09T10:16:44.902Z", "from": "Ronald Williams <ronald.williams@example.com>", "to": ["support@inbound.acme.example"], "cc": ["billing@example.com"], "bcc": [], "received_for": ["support@inbound.acme.example"], "subject": "Order #1042 arrived damaged", "message_id": "<CAF1042.damaged@mail.example.com>", "attachments": [ { "id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c", "filename": "photo.png", "content_type": "image/png", "content_disposition": "inline", "content_id": "photo-1042@example.com" } ] }}Two fields are worth reading carefully. message_id is the sender’s own identifier for the message, which is what you put in the reply headers if you want a client to thread your answer underneath it. And subject and message_id are empty strings rather than missing when the header was absent, which happens more often than you would expect from machine-generated mail.
| Field | What it is |
|---|---|
email_id | Our id for the message. Use it to fetch anything the event does not carry. |
from | The sender, name and address, as the message presented them. |
to | What the sender typed. Display it; do not route on it. |
received_for | The addresses of yours the message was actually accepted for. |
subject | The subject header, or an empty string when there was none. |
message_id | The sender's own message identifier, for threading a reply. |
attachments | One entry per part: filename, content type, disposition and an id. |
Routing on received_for, not on To
This is the rule that saves the most debugging. The To header is whatever the sender typed, and a sender can type anything. A message can reach you through Bcc, through a mailing list, through a forwarding rule, or with a To header naming somebody else entirely — and in every one of those cases the header is a poor guide to what you should do with it.
if (event.type !== "email.received") return;
const queue = event.data.received_for.some((address) => address.startsWith("support@"),) ? "support" : "unrouted";
await openTicket(queue, { externalId: event.data.email_id, from: event.data.from, subject: event.data.subject, messageId: event.data.message_id,});received_for is the list of your addresses the message was accepted for. Routing on it means a message addressed to support@ reaches your support queue even when it arrived as a blind copy, and a message that merely mentions support@ in a header does not.
Attachments and the raw message
The event lists each attachment’s filename, content type, disposition and id, and carries none of the bytes. That is deliberate: a webhook delivery carrying a ten megabyte PDF is a delivery that times out, and most handlers do not need the file at the moment the event arrives.
Ask the API for a signed link when you actually want the file. An inline attachment — one with a content_id, referenced from the HTML body — is usually an embedded image rather than a document a user meant to send, which is worth knowing before you show a customer a list of “attachments” that is mostly signature logos.
The raw source is kept too, so if you need a header the parsed event does not expose, or you want to verify a signature the sender applied, the original is there to fetch. The receiving reference documents both routes.
Storage and the daily cap
Received mail counts against your plan: a daily cap on messages and a ceiling on inbound storage. A message refused because you have hit one of those comes back with a reason naming which, so a handler can tell a full quota from a rejected sender rather than guessing. Deleting a message is the only way to free storage before retention does, which makes a delete-when-done step worth building on day one rather than the day you hit the cap.
What people build with it
- A support inbox that is part of the product. Mail to
support@becomes a ticket with the sender, the subject and the attachments already structured, and the reply goes back out through the same API. - Reply-to-comment. Give every thread its own address, and a reply from a phone keyboard lands on the right record without anybody visiting the application.
- Forward-to-file. An address that accepts receipts and invoices, pulls the PDF off the message, and files it against the right account.
- Machine mail you cannot control. Plenty of systems only speak email. An inbound address is the cheapest integration with a supplier who will send you a CSV attachment every morning and nothing else.
All four have the same two failure modes: routing on the wrong field, and a handler that is not idempotent. Get received_for and the event id right and most of the rest is your own domain logic. The receiving page shows the loop end to end, and the receiving guide has the setup in detail.
Frequently asked questions
Do I have to move my whole domain's email?
No, and you should not. Receiving sits on a label by default, so only mail addressed to that subdomain reaches us and everything at your root domain keeps going wherever it goes today. Pointing the root at us is possible but it takes every address on the domain with it.
Why route on received_for instead of the To header?
Because the To header is whatever the sender typed, and a sender can type anything. A message can reach you through Bcc, through a mailing list, or with a To header naming somebody else entirely. received_for is the address the message was actually accepted for, which is the only field routing can trust.
Are attachments included in the event?
Their metadata is — filename, content type, disposition and an id — but not their bytes, because an event carrying a ten megabyte PDF would be a delivery that times out. Request a signed link for the attachment you need, and fetch it from there.
What happens to mail I never read?
It stays until you delete it or until retention removes it, and it counts against your inbound storage. Deleting a message is the only way to free that space early, so a product that receives a lot of mail should delete on a schedule rather than waiting to hit the cap.
Can I reply to a message I received?
Yes. A reply is an ordinary send from the same name on your sending domain, with the received message's identifier in the reply headers so that the recipient's client threads it. The Inbox in the dashboard does exactly this, and the API does the same thing.
Is there a limit on how much I can receive?
Yes — your plan sets a daily cap on received messages and a ceiling on inbound storage. A message refused because you have hit either one comes back with a reason naming which, so a handler can tell a full quota from a rejected sender rather than guessing.
Sources
- RFC 5321: Simple Mail Transfer Protocol — IETF, read 2026-09-16
- RFC 2045: Multipurpose Internet Mail Extensions (MIME) Part One — IETF, read 2026-09-16
- RFC 1035: Domain Names — Implementation and Specification — IETF, read 2026-09-16
Related
- Receiving — Receive email at your own domain. A signed webhook names every message; read its headers, text and HTML, and fetch attachments behind signed links.
- Receiving — Inbound mail, and the Inbox: a webhook fires, you read it, you answer it.
- Receiving — Mail sent to you: the message, its attachments, its raw source.
- Email webhooks explained: events and signatures — What an email webhook is, the delivery events and what fires each, the payload shape, verifying the signature, and handling retries and out-of-order events.
- Webhooks — Every delivery, bounce, complaint, open and click posted to your endpoint, signed with a timestamp, retried on failure and replayable from the dashboard.