List inbound messages
Poll the inbound mailbox of your workspace with GET /v3/inbound/messages — oldest first, cursor-paginated exactly like the events feed.
Returns the messages received on your inbound-enabled domains that a route stored, in ascending (received_at, id) order — the natural order for a poller that wants everything after the last message it saw. Pagination is the same contract as `GET /v3/events`: since / until (inclusive), cursor, and a response of items, has_more, next_cursor, total_count.
GET /v3/inbound/messages
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
since | ISO-8601 | Optional | Only messages received at or after this timestamp (inclusive; offset or Z required). |
until | ISO-8601 | Optional | Only messages received at or before this timestamp (inclusive). |
cursor | string | Optional | The next_cursor of the previous page. An opaque value — never build one by hand; an unrecognised cursor answers 400 invalid_cursor. |
limit | integer | Optional | Page size, 1–200. Default 50. |
domain | string | Optional | Only messages addressed to this recipient domain (case-insensitive). |
recipient | string | Optional | Exact envelope recipient (case-insensitive) — matches to or any envelope_to entry. |
route_id | uuid | Optional | Only messages the given route matched; none = messages no route matched (implicit store). |
order | string | Optional | asc (default, the polling order) or desc (newest first). A cursor only continues a walk in the same order. |
auth | string | Optional | Authentication filter: spf_fail, dkim_fail, dmarc_fail, any_fail or all_pass. |
q | string | Optional | A message id or Message-ID matches exactly; anything else is a case-insensitive substring of to / envelope_to. |
forward | string | Optional | failing — only messages whose forward is retrying or failed. |
has_attachments | boolean | Optional | true = only messages with attachments; false = only messages without. |
kept | string | Optional | stored (default) = messages a route kept; all = also forward-only messages (stored: false, kept 72 hours). |
fields | string | Optional | full (default) or summary — no bodies or headers (text/html null, headers {}) plus a 160-character snippet. |
The InboundMessage object
| Parameter | Type | Required | Description |
|---|---|---|---|
id | uuid | Required | Message id — stable, use it to de-duplicate across polls and forwards. |
domain | string | Required | Recipient domain (lower-case) that accepted the message. |
route_id | uuid | null | Required | The route that matched, or null when no route matched (implicit store). |
message_id | string | Required | RFC 5322 Message-ID without angle brackets; generated as <uuid>@inbound.wemail.io when the sender omitted one. |
from | string | Required | Sender as Name <address>, or the bare address. |
to | string | Required | First accepted envelope recipient on this domain. |
envelope_to | string[] | Required | Every accepted envelope recipient on this domain. |
cc | string[] | Required | Addresses from the Cc header. |
subject | string | Required | Subject (empty string when absent). |
text / html | string | null | Required | Plain-text and HTML bodies, each truncated at 1 MB — see text_truncated / html_truncated. |
headers | object | Required | Every header, lower-cased name → first value (message-id, in-reply-to, references, date, auto-submitted, …); values capped at 8 KB. |
attachments | object[] | Required | { filename, content_type, size, url } per attachment — metadata only, url is always null in this version. |
auth | object | Required | { spf, dkim, dmarc }, each pass · fail · softfail · neutral · none · unknown. Informational. |
size_bytes | integer | Required | Raw message size. |
received_at | ISO-8601 | Required | When the MX accepted the message (millisecond precision — the cursor key). |
created_at | ISO-8601 | Required | When the row was written. |
stored | boolean | Required | List / get only: true when a route kept the message; false = forward-only (listed with kept=all). |
forward | object | Required | List / get only: push state of the route forward actions — { status, attempts, last_attempt_at, next_retry_at, last_status_code, last_duration_ms, last_url }, status one of none · ok · retrying · failed. Not part of the forward POST body. |
snippet | string | null | Optional | Only with fields=summary: the first 160 characters of the body. |
Cursor pagination
Pages are keyed on (received_at, id), so a page never repeats or skips a message even when several arrive in the same millisecond. Keep requesting with cursor=next_cursor until has_more is false; total_count is the number of messages matching the filters regardless of the cursor. Messages are kept for 30 days; only messages a route stored are listed — a forward-only or stop route leaves nothing to poll. Requires a full_access or read_only key (a send_only key is denied); 120 calls per minute per key.
Responses
| Status | Description |
|---|---|
200 | OK. { items, has_more, next_cursor, total_count }, ascending. |
400 | validation_error — bad filter or timestamp; invalid_cursor — the cursor is not a next_cursor value. |
401 | Missing or invalid API key. |
403 | insufficient_scope — a send_only key cannot read inbound mail. |
429 | rate_limited — more than 120 calls in a minute on this key. |
Code examples
# Poll once a minute: everything received since your last message (inclusive — de-duplicate on id)
curl -G https://api.wemail.io/v3/inbound/messages \
-H "Authorization: Bearer afn_live_…" \
--data-urlencode "since=2026-11-03T08:00:00Z" \
--data-urlencode "domain=leads.example.com" \
--data-urlencode "limit=100"
# Next page: pass next_cursor back as cursor while has_more is true
curl -G https://api.wemail.io/v3/inbound/messages \
-H "Authorization: Bearer afn_live_…" \
--data-urlencode "cursor=2026-11-03T08:14:21.417Z|6f1c2a1e-7d3b-4b7e-9f0e-2a4c8d9b1e55"// Walk every page since the last message you processed (ascending, keyset cursor).
let cursor = null;
const since = "2026-11-03T08:00:00Z";
do {
const qs = new URLSearchParams({ since, limit: "100", ...(cursor ? { cursor } : {}) });
const res = await fetch("https://api.wemail.io/v3/inbound/messages?" + qs, {
headers: { Authorization: `Bearer ${process.env.WEMAIL_API_KEY}` },
});
const page = await res.json();
for (const m of page.items) console.log(m.id, m.from, m.subject, m.headers["in-reply-to"]);
cursor = page.has_more ? page.next_cursor : null;
} while (cursor);import os, requests
cursor = None
while True:
params = {"since": "2026-11-03T08:00:00Z", "limit": 100}
if cursor:
params["cursor"] = cursor
r = requests.get(
"https://api.wemail.io/v3/inbound/messages",
headers={"Authorization": f"Bearer {os.environ['WEMAIL_API_KEY']}"},
params=params,
)
page = r.json()
for m in page["items"]:
print(m["id"], m["from"], m["subject"], m["headers"].get("in-reply-to"))
if not page["has_more"]:
break
cursor = page["next_cursor"]client, _ := wemail.NewClient(os.Getenv("WEMAIL_API_KEY"))
// Generated layer (client.API.*): pass the key in the context.
ctx := context.WithValue(context.Background(), wemail.ContextAccessToken, os.Getenv("WEMAIL_API_KEY"))
since, _ := time.Parse(time.RFC3339, "2026-11-03T08:00:00Z")
page, _, err := client.API.InboundAPI.ListInboundMessages(ctx).
Since(since).Domain("leads.example.com").Limit(100).Execute()
for _, m := range page.Items {
fmt.Println(m.Id, m.From, m.Subject)
}
// next page: .Cursor(page.GetNextCursor()) while page.HasMore{
"items": [
{
"id": "6f1c2a1e-7d3b-4b7e-9f0e-2a4c8d9b1e55",
"domain": "leads.example.com",
"route_id": "0b7e4c2d-9a1f-4e6b-8c3d-5f2a1b9e7d40",
"message_id": "CAF3k9Qx7Z@mail.gmail.com",
"from": "Maria Papadopoulou <maria@efood.gr>",
"to": "kostas@leads.example.com",
"envelope_to": ["kostas@leads.example.com"],
"cc": [],
"subject": "Re: Quick question about your ESP",
"text": "Hi Kostas, yes — let's talk next week.\n\n> On Mon, Kostas wrote: …",
"html": "<div dir=\"ltr\">Hi Kostas, yes — let's talk next week.</div>",
"text_truncated": false,
"html_truncated": false,
"headers": {
"message-id": "<CAF3k9Qx7Z@mail.gmail.com>",
"in-reply-to": "<msg_01HBYE7K9Z4PMQDR3W@leads.example.com>",
"references": "<msg_01HBYE7K9Z4PMQDR3W@leads.example.com>",
"date": "Mon, 3 Nov 2026 10:14:19 +0200",
"from": "Maria Papadopoulou <maria@efood.gr>",
"to": "kostas@leads.example.com",
"subject": "Re: Quick question about your ESP",
"auto-submitted": "no"
},
"attachments": [
{ "filename": "brief.pdf", "content_type": "application/pdf", "size": 48211, "url": null }
],
"auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass" },
"size_bytes": 71904,
"received_at": "2026-11-03T08:14:21.417Z",
"created_at": "2026-11-03T08:14:21.603Z"
}
],
"has_more": false,
"next_cursor": null,
"total_count": 1
}