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

ParameterTypeRequiredDescription
sinceISO-8601OptionalOnly messages received at or after this timestamp (inclusive; offset or Z required).
untilISO-8601OptionalOnly messages received at or before this timestamp (inclusive).
cursorstringOptionalThe next_cursor of the previous page. An opaque value — never build one by hand; an unrecognised cursor answers 400 invalid_cursor.
limitintegerOptionalPage size, 1–200. Default 50.
domainstringOptionalOnly messages addressed to this recipient domain (case-insensitive).
recipientstringOptionalExact envelope recipient (case-insensitive) — matches to or any envelope_to entry.
route_iduuidOptionalOnly messages the given route matched; none = messages no route matched (implicit store).
orderstringOptionalasc (default, the polling order) or desc (newest first). A cursor only continues a walk in the same order.
authstringOptionalAuthentication filter: spf_fail, dkim_fail, dmarc_fail, any_fail or all_pass.
qstringOptionalA message id or Message-ID matches exactly; anything else is a case-insensitive substring of to / envelope_to.
forwardstringOptionalfailing — only messages whose forward is retrying or failed.
has_attachmentsbooleanOptionaltrue = only messages with attachments; false = only messages without.
keptstringOptionalstored (default) = messages a route kept; all = also forward-only messages (stored: false, kept 72 hours).
fieldsstringOptionalfull (default) or summary — no bodies or headers (text/html null, headers {}) plus a 160-character snippet.

The InboundMessage object

Fields
ParameterTypeRequiredDescription
iduuidRequiredMessage id — stable, use it to de-duplicate across polls and forwards.
domainstringRequiredRecipient domain (lower-case) that accepted the message.
route_iduuid | nullRequiredThe route that matched, or null when no route matched (implicit store).
message_idstringRequiredRFC 5322 Message-ID without angle brackets; generated as <uuid>@inbound.wemail.io when the sender omitted one.
fromstringRequiredSender as Name <address>, or the bare address.
tostringRequiredFirst accepted envelope recipient on this domain.
envelope_tostring[]RequiredEvery accepted envelope recipient on this domain.
ccstring[]RequiredAddresses from the Cc header.
subjectstringRequiredSubject (empty string when absent).
text / htmlstring | nullRequiredPlain-text and HTML bodies, each truncated at 1 MB — see text_truncated / html_truncated.
headersobjectRequiredEvery header, lower-cased name → first value (message-id, in-reply-to, references, date, auto-submitted, …); values capped at 8 KB.
attachmentsobject[]Required{ filename, content_type, size, url } per attachment — metadata only, url is always null in this version.
authobjectRequired{ spf, dkim, dmarc }, each pass · fail · softfail · neutral · none · unknown. Informational.
size_bytesintegerRequiredRaw message size.
received_atISO-8601RequiredWhen the MX accepted the message (millisecond precision — the cursor key).
created_atISO-8601RequiredWhen the row was written.
storedbooleanRequiredList / get only: true when a route kept the message; false = forward-only (listed with kept=all).
forwardobjectRequiredList / 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.
snippetstring | nullOptionalOnly 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

StatusDescription
200OK. { items, has_more, next_cursor, total_count }, ascending.
400validation_error — bad filter or timestamp; invalid_cursor — the cursor is not a next_cursor value.
401Missing or invalid API key.
403insufficient_scope — a send_only key cannot read inbound mail.
429rate_limited — more than 120 calls in a minute on this key.

Code examples

cURL
# 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"
Node.js
// 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);
Python
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"]
Go
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
ResponseExample response
{
  "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
}