Receiving Inbound Mail
Create inbound addresses, receive each parsed message as a webhook, fetch its bodies and attachments, and reply in the same thread.
Mailcrate can receive email as well as send it: replies to your campaigns, mail to a support address, anything you point at it. Each message is parsed into a structured record, matched to the contact who sent it, linked to the message it answers, and pushed to your system as a webhook. A typical use is turning customer replies into tickets, or into a conversation view in your own product.
Setting up receiving in the app is covered in the user guides: Receiving Mail and Inbound Addresses. Every route on this page needs an API key with the inbound scope: inbound:read to read, inbound:write to manage addresses.
01
Mail arrives
At an inbound address, or a domain you receive on.
02
Parsed
Bodies, attachments, sender, thread.
03
inbound.parsed
Webhook to your endpoint.
04
You reply
Through the sending API, in the same thread.
Where mail comes in
There are two ways to receive:
- An inbound address is a Mailcrate address like
3f9a1c07b2e84d65@inbound.mailcrate.io. It works the moment it is created and needs no DNS changes. Forward your own address (saysupport@example.com) to it from your mail provider, or use it directly as a reply-to address. - Your own domain can receive too, once its MX records point at Mailcrate. That is set up in the app, on the sending domain's page.
Either way, the messages are the same and reach you the same way.
Manage inbound addresses
curl -X POST https://api.mailcrate.io/public/inbound-addresses \
-H "Authorization: Api-Key $MAILCRATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "label": "Support" }'{
"id": "b7e2c4a1-5d3f-4e8a-9b6c-0f1e2d3c4b5a",
"label": "Support",
"address": "3f9a1c07b2e84d65@inbound.mailcrate.io",
"domain": "inbound.mailcrate.io",
"status": "active",
"expectedForwardingSource": null,
"verifiedAt": null,
"senderIdentityId": null,
"created": "2026-08-03T09:15:02.441Z",
"updated": "2026-08-03T09:15:02.441Z"
}| Field | What it is |
|---|---|
label | Required, up to 120 characters. Your name for the address, such as "Support" or "Sales". |
senderIdentityId | Optional. The sender identity you want to reply from, recorded on the address for your integration to read. Mailcrate does not apply it to sends by itself. |
The random part of the address is what keeps strangers from mailing into your workspace, so treat the address as you would any unlisted inbox. The other routes:
GET /public/inbound-addresseslists every address in the workspace, as a plain array.PUT /public/inbound-addresses/{id}changes thelabel, or setssenderIdentityId(nullclears it).DELETE /public/inbound-addresses/{id}removes the address. Mail sent to it afterwards is dropped. Messages already received stay.
Get each message as a webhook
Subscribe a webhook endpoint to inbound.parsed. It fires once per message, after parsing, with the whole message under data.inboundMessage:
{
"id": "0f6c5f8a-6e2f-4a56-9b0e-6d3f3d2a1c77",
"event": "inbound.parsed",
"timestamp": "2026-08-03T11:04:21.512Z",
"workspaceId": "81c55e4e-cabb-4824-b0db-869d25a863a7",
"data": {
"inboundMessage": {
"id": "5a8e1d2c-3b4f-4c6d-8e9f-0a1b2c3d4e5f",
"contactId": "3f0d8a52-1c4e-4b8e-9a51-7f3c2b6d9e10",
"inboundAddressId": "b7e2c4a1-5d3f-4e8a-9b6c-0f1e2d3c4b5a",
"fromAddress": "ada@example.com",
"toAddress": "3f9a1c07b2e84d65@inbound.mailcrate.io",
"originalRecipient": "support@example.com",
"subject": "Re: Your order has shipped",
"messageId": "<CAF8x2k@mail.example.com>",
"inReplyTo": "<0102018f2c…@eu-west-1.amazonses.com>",
"references": ["<0102018f2c…@eu-west-1.amazonses.com>"],
"inReplyToDeliveryId": "6f1c2a9e-3b7d-4c55-9a1e-2d8f0b4c7e11",
"inReplyToInboundMessageId": null,
"threadRootId": "6f1c2a9e-3b7d-4c55-9a1e-2d8f0b4c7e11",
"threadRootType": "delivery",
"bodyText": "Thanks! Can I change the address?\n\nOn Mon, 3 Aug … wrote:\n> Your order …",
"bodyHtml": "<div>Thanks! Can I change the address?</div><blockquote>…</blockquote>",
"bodyTextStripped": "Thanks! Can I change the address?",
"bodyHtmlStripped": "<div>Thanks! Can I change the address?</div>",
"isAutoReply": false,
"attachments": [
{ "id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f", "filename": "label.pdf", "contentType": "application/pdf", "size": 48213 }
],
"spamVerdict": "PASS",
"virusVerdict": "PASS",
"dmarcVerdict": "PASS",
"receivedAt": "2026-08-03T11:04:18.000Z"
}
}
}Verify the signature and acknowledge quickly, exactly as for any other webhook. Retries can deliver the same message twice, so de-duplicate on inboundMessage.id.
Reading a message
Who sent it. fromAddress is the sender. contactId is set when that address belongs to a contact in the workspace, compared case-insensitively. An unknown sender does not become a contact, so spam cannot fill your audience; create the contact yourself if you want one.
Where it arrived. toAddress is the address Mailcrate received it on. When mail was forwarded to an inbound address, originalRecipient is the address the sender actually wrote to, such as support@example.com, which is what to route on when several of your addresses forward to one inbound address. It is null for mail received on your own domain. inboundAddressId names the inbound address, or is null for your own domain.
What it answers. Mailcrate follows the message's In-Reply-To and References headers:
| Field | Set when |
|---|---|
inReplyToDeliveryId | It replies to an email you sent through Mailcrate: a campaign, a journey step or an API send. |
inReplyToInboundMessageId | It replies to an earlier message Mailcrate received. |
threadRootId, threadRootType | Always. The oldest message in the conversation, and whether that is a delivery or an inbound message. Group a conversation on this. |
A message with no recognised headers is its own thread root. Mailcrate does not guess threads from subject lines.
What they wrote. bodyText and bodyHtml are the full bodies. bodyTextStripped and bodyHtmlStripped are the new text with the quoted history removed, or null when Mailcrate could not separate the two with confidence, in which case show the full body.
Whether to act on it. isAutoReply is a best-effort flag for machine-generated mail such as out-of-office replies and bounces; skip those before opening a ticket or replying automatically. The spamVerdict, virusVerdict and dmarcVerdict values (PASS, FAIL, GRAY and similar) come from the receiving mail server's checks.
A message that cannot be parsed sends no webhook. It is still listed by the API, with status parse-failed.
Mail from a known contact also drives journeys
When a message from a known contact arrives (not an auto-reply), it counts as a contact.replied event for journeys: a journey can start on it, and a step waiting for a reply moves on. See Triggering Journeys.
Fetch messages over the API
The webhook is the way to learn about new mail. These routes are for backfilling, for reconciling after an outage, and for the files the webhook does not carry.
GET /public/inbound-messageslists messages, newest first and paginated. Filter withcontactId(a Mailcrate contact id) orquery(matches the subject and sender). List items leave out the bodies and attachments.GET /public/inbound-messages/{id}returns one message with its bodies, its attachments, its thread root andstorageExpiresAt.GET /public/inbound-messages/{id}/repliesreturns the direct replies to a message, in both directions and in time order: each entry has adirectionofoutbound(a reply you sent) orinbound(a further message from them). For a whole conversation, group onthreadRootId.
Attachments and the original message
The webhook and the message carry attachment metadata only. Download the bytes when you need them:
Both answer 302 with a link to the file that works for 15 minutes; /raw is the complete original as an .eml file. Follow the redirect without your Authorization header (most HTTP clients drop it on a redirect to another host anyway) and download straight away rather than storing the link.
curl -L -o label.pdf \
https://api.mailcrate.io/public/inbound-messages/5a8e1d2c-3b4f-4c6d-8e9f-0a1b2c3d4e5f/attachments/c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f/download \
-H "Authorization: Api-Key $MAILCRATE_API_KEY"Attachments and original messages are kept for a limited time, 90 days by default; storageExpiresAt on the message gives the exact moment. After it, both routes answer 410 Gone. The parsed bodies and the rest of the message stay available, so copy any file you need to keep.
Reply in the same thread
Send the reply through the sending API with the received message's id as inReplyToInboundMessageId:
curl -X POST https://api.mailcrate.io/public/sending/mail \
-H "Authorization: Api-Key $MAILCRATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contactEmail": "ada@example.com",
"fromAddress": "Acme Support <support@example.com>",
"subject": "Re: Your order has shipped",
"html": "<p>Of course. Reply with the new address and we will update it.</p>",
"inReplyToInboundMessageId": "5a8e1d2c-3b4f-4c6d-8e9f-0a1b2c3d4e5f",
"lane": "transactional"
}'Mailcrate then:
- sets the
In-Reply-ToandReferencesheaders, so the reply appears under the original in the customer's mail client; - sets the reply-to to the address the message arrived on, unless the send already has one, so the customer's next answer comes back to Mailcrate as a new
inbound.parsedwithinReplyToDeliveryIdpointing at your reply; - links the two, so the reply is listed by
GET /public/inbound-messages/{id}/replies.
An id that is not a received message in this workspace is a 404.
Next steps
- Webhooks: signatures, retries and every other event.
- Sending Transactional Email: everything else a reply can carry.
- The Inbound API reference.