Managing Inbound Email
List, fetch, retry, export, and stream inbound email from your project
Mail that arrived on the MX listener
is stored per project and read through these routes.
They are ordinary /api/v1 endpoints under the inbound permission — an API key carries its project, a session needs
the X-Mailyard-Project-Id header.
Listing
GET /api/v1/inbound-emails?limit=50&status=received
| Param | Notes |
|---|---|
status | One of the three below, without regard to case. Anything else is a 400 |
limit | Default 50, maximum 200 |
cursor | The next_cursor of the previous page |
sender | Part of the From header, without regard to case |
recipient | Part of any envelope recipient |
match | contains (the default) or exact. Exact makes sender and recipient one whole address each, the sender found bare or inside Name <address> |
search | Part of the subject, without regard to case |
There are three statuses, and no others:
| Status | Means |
|---|---|
received | Stored and parsed |
rejected | Refused at ingest — a suppressed sender, or a DMARC failure on a p=reject domain |
failed | The MIME tree could not be parsed. The raw bytes are still there |
Paging is a keyset cursor, like the outbound log, and for the same reason: this list grows on its own. Follow
next_cursor until it comes back empty. The cursor encodes received_at and the id together, so two messages sharing
a received_at across a page boundary are neither repeated nor skipped. There is no total and no offset.
curl -G http://localhost:3000/api/v1/inbound-emails \
-H "Authorization: Bearer myk_..." \
--data-urlencode "limit=50" \
--data-urlencode "status=received"
Counts
GET /api/v1/inbound-emails/stats?from=2026-08-01&to=2026-08-31
from and to are optional and bound received_at the way the list’s cursor does.
{
"counts": {
"received": 812,
"rejected": 40,
"failed": 3
}
}
Get an Inbound Email
GET /api/v1/inbound-emails/{id}
{id} is the inbound email UUID. Returns the full record:
{
"inbound_email": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "81af718e-f0ae-4780-a0d7-9f05b34dabcc",
"domain_id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
"domain": "yourdomain.com",
"message_id": "abc123@example.com",
"sender": "Jane Doe <jane@example.com>",
"bounce_address": "bounces@example.com",
"recipients": [
"inbox@yourdomain.com"
],
"subject": "Hello",
"text_body": "Plain text body",
"html_body": "<p>HTML body</p>",
"headers": {
"Received": "from mail.example.com ..."
},
"attachments": [
{
"filename": "invoice.pdf",
"content_type": "application/pdf",
"size": 18244
}
],
"auth": {
"spf": "pass",
"dkim": "pass",
"dmarc": "pass",
"aligned": true,
"client_ip": "203.0.113.10"
},
"has_raw": true,
"size": 2048,
"status": "received",
"received_at": "2026-01-01T00:00:00Z",
"created_at": "2026-01-01T00:00:00Z"
}
}
sender is the From header, who the message says it is from, and it is empty when the message could not be parsed.
bounce_address is the SMTP MAIL FROM - where bounces for the message go and what SPF is checked against - which is
often not the sender. The stored headers carry it as Return-Path, written at receipt. domain is the name of the
domain the mail arrived for. The sender search parameter on the list matches sender only, and recipient the
envelope recipients.
auth carries the SPF, DKIM and DMARC verdicts stamped at ingest. aligned is the field worth acting on - a valid
signature from some other domain is not authentication. See Receiving
.
Attachment entries carry metadata only. Fetch the bytes through the download endpoint below.
Delete an Inbound Email
DELETE /api/v1/inbound-emails/{id}
Removes the record and best-effort deletes its blob-stored attachments. Returns 204 No Content.
Re-dispatch
POST /api/v1/inbound-emails/{id}/retry
{
"emitted": true
}
This re-sends the webhook, it does not re-process the mail
The name says retry, and what it actually does is emit the inbound.received
webhook event
again for a message already stored. The message is not re-parsed and its
status does not change.
It works on any stored message regardless of status, and it is not conditional on anything having failed. So it is
the tool for “my receiver was down when that arrived” — replaying the notification — and not for recovering a failed
parse, which nothing re-runs.
emitted: true means the event was handed to the dispatcher, not that your endpoint answered. Check
delivery tracking
for that.
Download the Message as .eml
GET /api/v1/inbound-emails/{id}/eml
Answers the message as message/rfc822 with a .eml filename. For a message that failed to parse this is the
original wire bytes, kept for exactly this. For every other message it is rebuilt from the stored headers, bodies and
attachments: embedded images keep their Content-ID, signatures computed over the original bytes (DKIM-Signature,
ARC-*) are left out, and the multipart structure is ours rather than the sender’s. Returns 404 when nothing of the
content is stored, as after the content retention sweep.
Download an Attachment
GET /api/v1/inbound-emails/{uuid}/attachments/{idx}
Streams an attachment by zero-based index for an inbound email you own. The response uses the attachment’s original
Content-Type and filename. Where the bytes are actually stored depends on MAILYARD_STORAGE_BACKEND, but this
endpoint reads them the same way either way.
Live updates
Each arrival is published on the console’s live event stream as email.inbound.received. That stream reaches only the
viewers connected to the node that received the message. To be notified machine-side, subscribe to
inbound.received with a webhook
. It carries metadata only - no bodies or attachments -
so fetch those with the endpoints above. See Event Types
for the payload.