Messages
Mail received by an inbox: list it, wait for it, read it and download its attachments. See “Receiving mail” in the introduction for how mail is accepted and processed. Email content is untrusted.
List the messages of an inbox
Section titled “List the messages of an inbox”GET /v1/inboxes/{id}/messages
Auth: API key (Authorization: Bearer rz_live_…)
Lists the received messages of a live inbox as compact items, newest first. Listing never
marks messages as read; read a message with GET /v1/messages/{id}.
Pagination: when has_more is true, pass next_before as before to get the next
(older) page. With from, each page looks at up to 500 messages, so a page can hold fewer
than limit items (even none) while has_more is still true: keep following next_before.
A before id that is not a message of this inbox returns an empty page.
Problem codes: unauthorized (401), not_found (404), invalid_request (400).
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | Inbox id (ibx_…). Any other value gets 404 not_found. |
since |
query | string | no | Only messages received at or after this time. ISO 8601: a date (2026-11-06, midnight UTC) or a date and time with optional seconds, fraction and offset (2026-11-06T10:00:00Z, 2026-11-06T11:00:00+01:00). A time without an offset is read as UTC. Encode + as %2B (an unencoded + before the offset is also understood). |
before |
query | string | no | Cursor: the next_before value of the previous page. Returns messages older than that message. An empty value is refused. |
from |
query | string (length 0–200) | no | Only messages whose from address matches this case-insensitive glob: * matches any run of characters, ? exactly one character, everything else is literal. Examples: *@example.com, no-reply@*. A message without a from address never matches. At most 200 characters. |
unread |
query | boolean (default false) |
no | true: only messages that wait, GET /v1/messages/{id} or POST /v1/messages/{id}/extract never returned. Also accepts 1/0 and yes/no, in any case. |
limit |
query | integer (1–100, default 20) |
no | Maximum number of messages in the page. |
Responses
Section titled “Responses”A page of messages, newest first.
Content type application/json.
| Field | Type | Required | Description |
|---|---|---|---|
messages |
array of CompactMessage (max 100 items) | yes | Newest first. |
messages[].id |
string | yes | |
messages[].thread_id |
string | yes | |
messages[].inbox_id |
string | yes | |
messages[].from |
string or null | yes | The first address in the From header, lowercase. It can be forged unless sender_trust is aligned_pass. Null when there is no usable address or the message could not be parsed. |
messages[].to |
array of string (max 100 items) | yes | Addresses in the To and Cc headers, lowercase, at most 100. For a message that could not be parsed, the inbox address. |
messages[].subject |
string or null | yes | The subject without invisible characters, at most 2 KB (UTF-8). Null when empty or missing. Untrusted. |
messages[].snippet |
string (length 0–200) | yes | The first 200 characters (Unicode code points, so an emoji is never cut in half) of reply_text (or of text when reply_text is empty), with whitespace collapsed. Untrusted. |
messages[].received_at |
string (date-time) | yes | When Replyz accepted the message. |
messages[].sender_trust |
string: aligned_pass, dkim_pass, unauthenticated |
yes | How far the sender is authenticated. Today every message is unauthenticated: aligned_pass and dkim_pass are planned (docs/features/safety.md). They will be based on Cloudflare’s own authentication results, located by position; ambiguous cases are unauthenticated.- aligned_pass: Cloudflare reports a DMARC pass for the domain of from. The mail comes from that domain; its content is still untrusted.• dkim_pass: Cloudflare reports a valid DKIM signature but no DMARC pass. The signature can belong to any domain, so from can still be forged.• unauthenticated: everything else. from can be forged.A message that could not be parsed is always unauthenticated. |
messages[].injection_risk |
string: low, medium, high |
yes | Heuristic risk that the content tries to instruct an AI. - high: instruction-like text hidden from a human reader, Unicode tag characters (invisible text), two or more kinds of instruction-like text, one kind together with hidden content, or instructions in a text/plain part that the HTML does not show.• medium: one kind of instruction-like text, a hiding signal (hidden_content, zero_width_chars, long_encoded_blob, text_part_differs), or a message that could not be parsed.• low: none of these. This does not mean the content is safe. |
messages[].otp_like |
boolean | yes | Heuristic: true when the code finder of POST /v1/messages/{id}/extract finds a likely code in the subject and the new text (reply_text, or text when it is empty). A hint only; get the code itself with extraction. |
messages[].has_attachments |
boolean | yes | |
messages[].returned_before |
boolean | yes | True if wait, GET /v1/messages/{id} or POST /v1/messages/{id}/extract had returned this message before this call. |
has_more |
boolean | yes | True when older messages may follow; get them with before set to next_before. |
next_before |
string or null | yes | The before value for the next page; null when has_more is false. |
Example: First page with limit=2
{ "messages": [ { "id": "msg_01M6R7HAANT4D11FHFZPYG0C2V", "thread_id": "thr_01M6R6JZDERMT2DVG0T8ABTX4J", "inbox_id": "ibx_01M6R4T180ZADNV1160NS1SRNP", "from": "ana@example.com", "to": [ "atlas-wo7tw5@youreplyz.com" ], "subject": "Re: Dinner on Friday?", "snippet": "Also, can you bring the wine? Thanks!", "received_at": "2026-11-06T10:47:40.117Z", "sender_trust": "unauthenticated", "injection_risk": "low", "otp_like": false, "has_attachments": false, "returned_before": false }, { "id": "msg_01M6R6JZDERMT2DVG0T8ABTX4J", "thread_id": "thr_01M6R6JZDERMT2DVG0T8ABTX4J", "inbox_id": "ibx_01M6R4T180ZADNV1160NS1SRNP", "from": "ana@example.com", "to": [ "atlas-wo7tw5@youreplyz.com" ], "subject": "Dinner on Friday?", "snippet": "Hi! Are you free for dinner on Friday at 8? I booked a table at the usual place. The menu is attached. Ana", "received_at": "2026-11-06T10:31:05.902Z", "sender_trust": "unauthenticated", "injection_risk": "low", "otp_like": false, "has_attachments": true, "returned_before": true } ], "has_more": true, "next_before": "msg_01M6R6JZDERMT2DVG0T8ABTX4J"}Example: Next page (before=msg_01M6R6JZDERMT2DVG0T8ABTX4J)
{ "messages": [ { "id": "msg_01M6R51QPXKNV8E3Y0VE3JMR0J", "thread_id": "thr_01M6R51QPXKNV8E3Y0VE3JMR0J", "inbox_id": "ibx_01M6R4T180ZADNV1160NS1SRNP", "from": "no-reply@accounts.example.com", "to": [ "atlas-wo7tw5@youreplyz.com" ], "subject": "Your Example verification code", "snippet": "Your verification code is 482913. It expires in 10 minutes.", "received_at": "2026-11-06T10:04:12.381Z", "sender_trust": "unauthenticated", "injection_risk": "low", "otp_like": true, "has_attachments": false, "returned_before": false } ], "has_more": false, "next_before": null}invalid_request: a query parameter is invalid (since is not an ISO 8601 time, before
is not a message id, from is longer than 200 characters, unread is not a boolean, or
limit is not an integer from 1 to 100). The message names the parameter.
Problem object (application/problem+json). Codes: invalid_request.
Example: invalid_request: since
{ "type": "https://docs.replyz.io/errors/invalid_request", "code": "invalid_request", "status": 400, "message": "Invalid query parameter \"since\".", "fix": "Use an ISO 8601 time, e.g. 2026-11-06T10:00:00Z (encode '+' as %2B).", "docs": "https://docs.replyz.io/errors/invalid_request.md"}Example: invalid_request: before
{ "type": "https://docs.replyz.io/errors/invalid_request", "code": "invalid_request", "status": 400, "message": "Invalid query parameter \"before\".", "fix": "Use next_before from the previous page.", "docs": "https://docs.replyz.io/errors/invalid_request.md"}Example: invalid_request: limit
{ "type": "https://docs.replyz.io/errors/invalid_request", "code": "invalid_request", "status": 400, "message": "Invalid query parameter \"limit\".", "fix": "Use an integer from 1 to 100.", "docs": "https://docs.replyz.io/errors/invalid_request.md"}unauthorized: the Authorization header is missing, is not Bearer rz_live_…, or holds an
unknown or revoked key.
| Header | Description |
|---|---|
WWW-Authenticate |
Always Bearer realm="replyz". |
Problem object (application/problem+json). Codes: unauthorized.
Example: unauthorized: missing or malformed key
{ "type": "https://docs.replyz.io/errors/unauthorized", "code": "unauthorized", "status": 401, "message": "Missing or malformed API key.", "fix": "Send the header Authorization: Bearer rz_live_… with the api_key from POST /v1/signup.", "docs": "https://docs.replyz.io/errors/unauthorized.md"}not_found: no live inbox with this id in this account.
Problem object (application/problem+json). Codes: not_found.
Example: not_found: inbox
{ "type": "https://docs.replyz.io/errors/not_found", "code": "not_found", "status": 404, "message": "Inbox not found.", "fix": "List your inboxes with GET /v1/inboxes.", "docs": "https://docs.replyz.io/errors/not_found.md"}internal_error: something failed on Replyz’s side. Retry after a few seconds.
Problem object (application/problem+json). Codes: internal_error.
Example: internal_error
{ "type": "https://docs.replyz.io/errors/internal_error", "code": "internal_error", "status": 500, "message": "Something went wrong on our side.", "fix": "Retry in a few seconds.", "docs": "https://docs.replyz.io/errors/internal_error.md"}Wait for mail (long-poll)
Section titled “Wait for mail (long-poll)”GET /v1/inboxes/{id}/wait
Auth: API key (Authorization: Bearer rz_live_…)
Returns matching messages as soon as there are any, newest first, and marks them as read.
- Messages received since
since(default: 10 minutes before the call) match at once, so mail that arrived just before the call is not missed. Each item tells whether it had been returned before (returned_before); passexclude_returned=trueto get only messages never returned. - If nothing matches, the request is held for up to
timeoutseconds (default 25, at most 60) and returns as soon as a matching message arrives. If none arrives, it returnsstatus: "timeout"with no messages: call it again.timeout=0checks once without holding. - Set your HTTP client timeout above
timeout(for exampletimeoutplus 10 seconds). - An account can hold at most 5 requests at once, shared with
GET /v1/events?wait=; one more gets429 too_many_waits(withRetry-After: 1). A call that does not need to hold (mail is already there, ortimeout=0) never counts. - At most
limitmessages are returned (the newest ones). Thefromandsubjectfilters are applied to the newest 500 messages that match the other filters. - For an
expiredorsuspendedaccount (including asandboxaccount past itsexpires_at) the call never holds:timeoutis still validated, then treated as 0. extract=otp,extract=linkorextract=otp,linkaddsextractedto each returned message, with only the requested parts (see the Extraction tag). Asandboxaccount flagged for an OTP flood may run at most 10 extractions per UTC hour; past that budget, messages come withextracted: nullandextract_limited: trueinstead of an error. Messages are marked as read either way.
Problem codes: unauthorized (401), not_found (404), invalid_request (400),
too_many_waits (429).
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | Inbox id (ibx_…). Any other value gets 404 not_found. |
since |
query | string | no | Only messages received at or after this time. Default: 10 minutes before the call. Same format as in GET /v1/inboxes/{id}/messages: ISO 8601 date, or date and time with optional seconds, fraction and offset; a time without an offset is read as UTC; encode + as %2B. |
from |
query | string (length 0–200) | no | Only messages whose from address matches this case-insensitive glob (* any run of characters, ? one character), for example *@example.com. A message without a from address never matches. At most 200 characters. |
subject |
query | string (length 0–200) | no | Only messages whose subject contains this text (case-insensitive). At most 200 characters. |
thread_id |
query | string | no | Only messages of this thread. A thread of another inbox matches nothing. An empty value is refused. |
exclude_returned |
query | boolean (default false) |
no | true: skip messages that wait, GET /v1/messages/{id} or POST /v1/messages/{id}/extract already returned. Also accepts 1/0 and yes/no, in any case. |
limit |
query | integer (1–50, default 10) |
no | Maximum number of messages returned. |
timeout |
query | integer (0–60, default 25) |
no | Seconds to hold the request while nothing matches. 0 checks once and answers at once. Validated for every account, but treated as 0 for an expired or suspended account. |
extract |
query | array of string: otp, link |
no | Add the extraction result to each returned message: otp (the code: otp and otp_candidates), link (the links: primary_link and links) or otp,link (all four). Parts are case-insensitive and spaces around them are ignored. An empty value, or any other part, is refused with invalid_request. See the Extraction tag. |
Responses
Section titled “Responses”status: "ok" with the matching messages (now marked as read), or status: "timeout"
with none.
Content type application/json.
| Field | Type | Required | Description |
|---|---|---|---|
status |
string: ok, timeout |
yes | ok: at least one message. timeout: nothing matched in time; call again. |
messages |
array of WaitMessage (max 50 items) | yes | Newest first; empty on timeout. These messages are now marked as read. |
messages[].id |
string | yes | |
messages[].thread_id |
string | yes | |
messages[].inbox_id |
string | yes | |
messages[].from |
string or null | yes | The first address in the From header, lowercase. It can be forged unless sender_trust is aligned_pass. Null when there is no usable address or the message could not be parsed. |
messages[].to |
array of string (max 100 items) | yes | Addresses in the To and Cc headers, lowercase, at most 100. For a message that could not be parsed, the inbox address. |
messages[].subject |
string or null | yes | The subject without invisible characters, at most 2 KB (UTF-8). Null when empty or missing. Untrusted. |
messages[].snippet |
string (length 0–200) | yes | The first 200 characters (Unicode code points, so an emoji is never cut in half) of reply_text (or of text when reply_text is empty), with whitespace collapsed. Untrusted. |
messages[].received_at |
string (date-time) | yes | When Replyz accepted the message. |
messages[].sender_trust |
string: aligned_pass, dkim_pass, unauthenticated |
yes | How far the sender is authenticated. Today every message is unauthenticated: aligned_pass and dkim_pass are planned (docs/features/safety.md). They will be based on Cloudflare’s own authentication results, located by position; ambiguous cases are unauthenticated.- aligned_pass: Cloudflare reports a DMARC pass for the domain of from. The mail comes from that domain; its content is still untrusted.• dkim_pass: Cloudflare reports a valid DKIM signature but no DMARC pass. The signature can belong to any domain, so from can still be forged.• unauthenticated: everything else. from can be forged.A message that could not be parsed is always unauthenticated. |
messages[].injection_risk |
string: low, medium, high |
yes | Heuristic risk that the content tries to instruct an AI. - high: instruction-like text hidden from a human reader, Unicode tag characters (invisible text), two or more kinds of instruction-like text, one kind together with hidden content, or instructions in a text/plain part that the HTML does not show.• medium: one kind of instruction-like text, a hiding signal (hidden_content, zero_width_chars, long_encoded_blob, text_part_differs), or a message that could not be parsed.• low: none of these. This does not mean the content is safe. |
messages[].otp_like |
boolean | yes | Heuristic: true when the code finder of POST /v1/messages/{id}/extract finds a likely code in the subject and the new text (reply_text, or text when it is empty). A hint only; get the code itself with extraction. |
messages[].has_attachments |
boolean | yes | |
messages[].returned_before |
boolean | yes | True if wait, GET /v1/messages/{id} or POST /v1/messages/{id}/extract had returned this message before this call. |
messages[].extracted |
Extraction or null | no | Only with extract: the requested parts of the extraction result (see Extraction). Null when the message was not extracted: because the account’s extraction budget for this UTC hour was used up (extract_limited is then true), or because the message could no longer be read (for example its inbox was being deleted). |
messages[].extracted.otp |
ExtractedOtp or null | no | The most likely one-time code, or null when the mail has no likely code. |
messages[].extracted.otp.code |
string (length 4–9) | yes | The code as written in the mail, for example 482 913, 123-456 or 7KQ2XM. |
messages[].extracted.otp.value |
string | yes | The code without spaces and hyphens, ready to type, for example 482913. |
messages[].extracted.otp.confidence |
number (0–1) | yes | How likely this is the code the mail asks for, from 0 to 1 (two decimals). Returned codes score at least 0.5. |
messages[].extracted.otp_candidates |
array of ExtractedOtp (max 4 items) | no | Other plausible codes, best first, each with a different value from otp. Usually empty. |
messages[].extracted.otp_candidates[].code |
string (length 4–9) | yes | The code as written in the mail, for example 482 913, 123-456 or 7KQ2XM. |
messages[].extracted.otp_candidates[].value |
string | yes | The code without spaces and hyphens, ready to type, for example 482913. |
messages[].extracted.otp_candidates[].confidence |
number (0–1) | yes | How likely this is the code the mail asks for, from 0 to 1 (two decimals). Returned codes score at least 0.5. |
messages[].extracted.primary_link |
object or null | no | The main action of the mail: the most prominent link of kind verify, magic_login or reset, or null. It is one of the mail’s links, so it also appears in links unless it comes after the 20th link. |
messages[].extracted.primary_link.url |
string | yes | The URL as written in the mail (http or https); for a bare URL, punctuation at its end (such as ., , or )) is left out. Untrusted. |
messages[].extracted.primary_link.kind |
any: verify, magic_login, reset |
yes | |
messages[].extracted.primary_link.text |
string or null (length 0–120) | yes | For a link from the HTML part, the text in front of the URL in text: normally the link’s anchor text (it can start with a few words before the anchor). Null for a bare URL. Untrusted. |
messages[].extracted.primary_link.confidence |
number (0–1) | yes | How sure the classifier is about kind, from 0 to 1 (two decimals). |
messages[].extracted.links |
array of ExtractedLink (max 20 items) | no | Links of the visible text, in order of appearance, each URL once. |
messages[].extracted.links[].url |
string | yes | The URL as written in the mail (http or https); for a bare URL, punctuation at its end (such as ., , or )) is left out. Untrusted. |
messages[].extracted.links[].kind |
string: verify, magic_login, reset, unsubscribe, other |
yes | What a link does, judged from its own text, its URL path and the words around it: - verify: confirms an email address or an action.• magic_login: signs in without a password (only links that carry a token).• reset: password reset or account recovery.• unsubscribe: unsubscribe or email preferences.• other: anything else, including home pages, help pages, privacy and terms links. |
messages[].extracted.links[].text |
string or null (length 0–120) | yes | For a link from the HTML part, the text in front of the URL in text: normally the link’s anchor text (it can start with a few words before the anchor). Null for a bare URL. Untrusted. |
messages[].extracted.links[].confidence |
number (0–1) | yes | How sure the classifier is about kind, from 0 to 1 (two decimals). |
messages[].extract_limited |
boolean true |
no | Present, and true, only when extracted is null because a sandbox account flagged for an OTP flood already ran its 10 extractions this UTC hour. The message is still marked as read; read it with GET /v1/messages/{id}. |
next |
string | no | Only on timeout, a hint for the agent. |
Example: A verification mail arrived
{ "status": "ok", "messages": [ { "id": "msg_01M6R51QPXKNV8E3Y0VE3JMR0J", "thread_id": "thr_01M6R51QPXKNV8E3Y0VE3JMR0J", "inbox_id": "ibx_01M6R4T180ZADNV1160NS1SRNP", "from": "no-reply@accounts.example.com", "to": [ "atlas-wo7tw5@youreplyz.com" ], "subject": "Your Example verification code", "snippet": "Your verification code is 482913. It expires in 10 minutes.", "received_at": "2026-11-06T10:04:12.381Z", "sender_trust": "unauthenticated", "injection_risk": "low", "otp_like": true, "has_attachments": false, "returned_before": false } ]}Example: The same mail with extract=otp,link
{ "status": "ok", "messages": [ { "id": "msg_01M6R51QPXKNV8E3Y0VE3JMR0J", "thread_id": "thr_01M6R51QPXKNV8E3Y0VE3JMR0J", "inbox_id": "ibx_01M6R4T180ZADNV1160NS1SRNP", "from": "no-reply@accounts.example.com", "to": [ "atlas-wo7tw5@youreplyz.com" ], "subject": "Your Example verification code", "snippet": "Your verification code is 482913. It expires in 10 minutes.", "received_at": "2026-11-06T10:04:12.381Z", "sender_trust": "unauthenticated", "injection_risk": "low", "otp_like": true, "has_attachments": false, "returned_before": false, "extracted": { "otp": { "code": "482913", "value": "482913", "confidence": 0.78 }, "otp_candidates": [], "primary_link": null, "links": [] } } ]}Example: Extraction budget used up (sandbox account flagged for an OTP flood)
{ "status": "ok", "messages": [ { "id": "msg_01M6R51QPXKNV8E3Y0VE3JMR0J", "thread_id": "thr_01M6R51QPXKNV8E3Y0VE3JMR0J", "inbox_id": "ibx_01M6R4T180ZADNV1160NS1SRNP", "from": "no-reply@accounts.example.com", "to": [ "atlas-wo7tw5@youreplyz.com" ], "subject": "Your Example verification code", "snippet": "Your verification code is 482913. It expires in 10 minutes.", "received_at": "2026-11-06T10:04:12.381Z", "sender_trust": "unauthenticated", "injection_risk": "low", "otp_like": true, "has_attachments": false, "returned_before": false, "extracted": null, "extract_limited": true } ]}Example: Nothing matched before the timeout
{ "status": "timeout", "messages": [], "next": "No matching mail yet. Call wait again."}invalid_request: a query parameter is invalid (extract is empty or has a part other than
otp and link, thread_id is not a thread id, since is not an ISO 8601 time, from or
subject is longer than 200 characters, exclude_returned is not a boolean, limit is not
an integer from 1 to 50, or timeout is not an integer from 0 to 60). The message names the
parameter; the first invalid one, in that order, is reported.
Problem object (application/problem+json). Codes: invalid_request.
Example: invalid_request: timeout
{ "type": "https://docs.replyz.io/errors/invalid_request", "code": "invalid_request", "status": 400, "message": "Invalid query parameter \"timeout\".", "fix": "Use an integer from 0 to 60.", "docs": "https://docs.replyz.io/errors/invalid_request.md"}Example: invalid_request: thread_id
{ "type": "https://docs.replyz.io/errors/invalid_request", "code": "invalid_request", "status": 400, "message": "Invalid query parameter \"thread_id\".", "fix": "Use a thread id (thr_…).", "docs": "https://docs.replyz.io/errors/invalid_request.md"}Example: invalid_request: from
{ "type": "https://docs.replyz.io/errors/invalid_request", "code": "invalid_request", "status": 400, "message": "Invalid query parameter \"from\".", "fix": "Use at most 200 characters.", "docs": "https://docs.replyz.io/errors/invalid_request.md"}Example: invalid_request: extract
{ "type": "https://docs.replyz.io/errors/invalid_request", "code": "invalid_request", "status": 400, "message": "Invalid query parameter \"extract\".", "fix": "Use extract=otp, extract=link or extract=otp,link.", "docs": "https://docs.replyz.io/errors/invalid_request.md"}unauthorized: the Authorization header is missing, is not Bearer rz_live_…, or holds an
unknown or revoked key.
| Header | Description |
|---|---|
WWW-Authenticate |
Always Bearer realm="replyz". |
Problem object (application/problem+json). Codes: unauthorized.
Example: unauthorized: missing or malformed key
{ "type": "https://docs.replyz.io/errors/unauthorized", "code": "unauthorized", "status": 401, "message": "Missing or malformed API key.", "fix": "Send the header Authorization: Bearer rz_live_… with the api_key from POST /v1/signup.", "docs": "https://docs.replyz.io/errors/unauthorized.md"}not_found: no live inbox with this id in this account.
Problem object (application/problem+json). Codes: not_found.
Example: not_found: inbox
{ "type": "https://docs.replyz.io/errors/not_found", "code": "not_found", "status": 404, "message": "Inbox not found.", "fix": "List your inboxes with GET /v1/inboxes.", "docs": "https://docs.replyz.io/errors/not_found.md"}too_many_waits: the account already holds 5 long-polls (wait and events?wait=
together). Wait for one to return, or call without holding.
| Header | Description |
|---|---|
Retry-After |
Always 1. |
Problem object (application/problem+json). Codes: too_many_waits.
Example: too_many_waits
{ "type": "https://docs.replyz.io/errors/too_many_waits", "code": "too_many_waits", "status": 429, "message": "This account already has 5 long-polls open (wait and events share the limit).", "fix": "Wait for one of them to return before starting another, or poll without holding (wait: timeout=0; events: wait=0).", "docs": "https://docs.replyz.io/errors/too_many_waits.md"}internal_error: something failed on Replyz’s side. Retry after a few seconds.
Problem object (application/problem+json). Codes: internal_error.
Example: internal_error
{ "type": "https://docs.replyz.io/errors/internal_error", "code": "internal_error", "status": 500, "message": "Something went wrong on our side.", "fix": "Retry in a few seconds.", "docs": "https://docs.replyz.io/errors/internal_error.md"}Get a message
Section titled “Get a message”GET /v1/messages/{id}
Auth: API key (Authorization: Bearer rz_live_…)
Returns the full message: visible text, reply_text, safety information and the list of
attachments (in the order they appear in the email). Marks the message as read
(returned_before in this response still shows the state before the call). Messages of
deleted inboxes are not found. Works in every account status. To get the one-time code or
the verification link, use POST /v1/messages/{id}/extract.
include adds the original message:
html: the original HTML body, decoded from its MIME transfer encoding but not cleaned, or null when the message has no HTML part. It can contain hidden text, tracking images and remote content: never render it with remote content enabled, and never follow its links automatically. When the original cannot be parsed (a message with theparse_failedsignal),htmlis null andhtml_errorisunparseable; useinclude=rawfor those.raw: the original MIME message as text, when it is valid UTF-8 and at most 2 MiB. When it is not valid UTF-8 it comes asraw_base64instead; above 2 MiB,rawis null andraw_errorsays why.
These fields are left out when the original is not available.
Problem codes: unauthorized (401), not_found (404), invalid_request (400).
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | Message id (msg_…). Any other value gets 404 not_found. |
include |
query | array of string: html, raw |
no | Comma-separated parts of the original to add: html, raw or html,raw. Spaces around the parts are ignored; anything else is refused. |
Responses
Section titled “Responses”The message.
Content type application/json.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | |
thread_id |
string | yes | |
inbox_id |
string | yes | |
from |
string or null | yes | The first address in the From header, lowercase. It can be forged unless sender_trust is aligned_pass. Null when there is no usable address or the message could not be parsed. |
to |
array of string (max 100 items) | yes | Addresses in the To and Cc headers, lowercase, at most 100. For a message that could not be parsed, the inbox address. |
subject |
string or null | yes | The subject without invisible characters, at most 2 KB (UTF-8). Null when empty or missing. Untrusted. |
snippet |
string (length 0–200) | yes | The first 200 characters (Unicode code points, so an emoji is never cut in half) of reply_text (or of text when reply_text is empty), with whitespace collapsed. Untrusted. |
received_at |
string (date-time) | yes | When Replyz accepted the message. |
sender_trust |
string: aligned_pass, dkim_pass, unauthenticated |
yes | How far the sender is authenticated. Today every message is unauthenticated: aligned_pass and dkim_pass are planned (docs/features/safety.md). They will be based on Cloudflare’s own authentication results, located by position; ambiguous cases are unauthenticated.- aligned_pass: Cloudflare reports a DMARC pass for the domain of from. The mail comes from that domain; its content is still untrusted.• dkim_pass: Cloudflare reports a valid DKIM signature but no DMARC pass. The signature can belong to any domain, so from can still be forged.• unauthenticated: everything else. from can be forged.A message that could not be parsed is always unauthenticated. |
injection_risk |
string: low, medium, high |
yes | Heuristic risk that the content tries to instruct an AI. - high: instruction-like text hidden from a human reader, Unicode tag characters (invisible text), two or more kinds of instruction-like text, one kind together with hidden content, or instructions in a text/plain part that the HTML does not show.• medium: one kind of instruction-like text, a hiding signal (hidden_content, zero_width_chars, long_encoded_blob, text_part_differs), or a message that could not be parsed.• low: none of these. This does not mean the content is safe. |
otp_like |
boolean | yes | Heuristic: true when the code finder of POST /v1/messages/{id}/extract finds a likely code in the subject and the new text (reply_text, or text when it is empty). A hint only; get the code itself with extraction. |
has_attachments |
boolean | yes | |
returned_before |
boolean | yes | True if wait, GET /v1/messages/{id} or POST /v1/messages/{id}/extract had returned this message before this call. |
direction |
string: inbound |
yes | Always inbound in this version. |
message_id_header |
string or null (length 0–998) | yes | The Message-ID header as sent (with its angle brackets), or null. |
in_reply_to |
string or null (length 0–998) | yes | The In-Reply-To header as sent, or null. |
text |
string | yes | The visible text: HTML converted to text with hidden elements removed and links kept as text <url> (only the first 1,000,000 characters of the HTML are converted), or the text/plain part when there is no visible HTML. At most 256 KB (UTF-8). Untrusted. |
reply_text |
string | yes | Only the new part of a reply: quoted history (such as “On … wrote:” lines in several languages, Outlook separators and trailing > lines) and the signature after -- are removed. When that would leave nothing (for example a forwarded message), it is the whole text. At most 64 KB (UTF-8). Untrusted. |
safety |
MessageSafety | yes | |
safety.injection_risk |
string: low, medium, high |
yes | Heuristic risk that the content tries to instruct an AI. - high: instruction-like text hidden from a human reader, Unicode tag characters (invisible text), two or more kinds of instruction-like text, one kind together with hidden content, or instructions in a text/plain part that the HTML does not show.• medium: one kind of instruction-like text, a hiding signal (hidden_content, zero_width_chars, long_encoded_blob, text_part_differs), or a message that could not be parsed.• low: none of these. This does not mean the content is safe. |
safety.signals |
array of string | yes | What was found in the content. Possible values: - Instruction-like text: ignore_instructions, role_override, system_prompt, chat_markers, tool_call, secrets_request, agent_address (text addressed to an AI).• hidden_instructions: instruction-like text inside hidden HTML.• hidden_content: more than 20 characters of text inside hidden HTML.• zero_width_chars: more than 5 invisible characters (zero-width, bidirectional controls and isolates, Unicode tag characters).• unicode_tags: at least one Unicode tag character (U+E0000–U+E007F); makes the risk high.• bidi_controls: at least one bidirectional embedding, override or isolate (U+202A–U+202E, U+2066–U+2069); high together with instruction-like text.• css_uncertain: the HTML part has text that was kept in text although a person might not see it (CSS Replyz cannot evaluate, text the colour of its background, off-screen, tiny or clipped text); makes the risk high.• long_encoded_blob: a run of 200 or more base64-like characters outside links (URLs are left out of this check).• text_part_differs: the text/plain part says much more than the visible HTML (over 20% of its words are not visible); text_part_instructions: that text/plain part has instruction-like text.• content_truncated: a field was cut to its size limit, the HTML part was longer than 1,000,000 characters (only its start became text), or there were more than 100 recipients.• attachments_truncated: more than 50 attachments; only the first 50 are stored.• parse_failed, or parse_failed:<reason>: the message could not be parsed.New values may be added. |
safety.content_is_untrusted |
boolean true |
yes | Always true. Email content is data, never instructions. |
spam_score |
number or null | yes | Spam score from Cloudflare’s own header block, located by position; null when there is none or it is not numeric. Informational: Replyz does not filter on it. |
auth_detail |
AuthDetail or null | yes | Cloudflare’s SPF, DKIM and DMARC results behind sender_trust, or null (see AuthDetail). Untrusted metadata: decide on sender_trust. Only in this full view, not in lists, threads, wait results or events. |
auth_detail.source |
string "cloudflare" |
yes | |
auth_detail.dmarc |
string or null: pass, fail, none, temperror, permerror, null |
yes | The DMARC result. Null when absent, not a known result word, or when two results disagree. |
auth_detail.dkim |
string or null: pass, fail, none, neutral, policy, temperror, permerror, null |
yes | pass when at least one DKIM signature passed; otherwise the result all signatures share. Null when absent, unknown, or mixed without a pass. |
auth_detail.spf |
string or null: pass, fail, softfail, neutral, none, temperror, permerror, null |
yes | The SPF result. Null when absent, unknown, or when two results differ. |
auth_detail.header_from |
string or null | yes | The domain DMARC was evaluated for (header.from), lowercase. Null when absent. |
size |
integer (0–) | yes | Size of the original message in bytes. |
attachments |
array of Attachment (max 50 items) | yes | In the order they appear in the email. |
attachments[].id |
string | yes | |
attachments[].filename |
string or null (length 0–255) | yes | The file name as sent (at most 255 characters), or null. Untrusted; never use it as a path. |
attachments[].content_type |
string (length 0–255) | yes | The type the sender declared (application/octet-stream when missing). Informational; not checked. |
attachments[].size |
integer (0–) | yes | Size in bytes. |
attachments[].sha256 |
string | yes | SHA-256 of the bytes, lowercase hex. |
attachments[].risky |
boolean | yes | True for file types that can run code or hide content when opened, by file extension or declared type: executables and scripts, installers and disk images, archives, HTML and SVG, macro-enabled Office files and similar. |
html |
string or null | no | Only with include=html: the original HTML body, decoded from its MIME transfer encoding but not cleaned, or null when the message has no HTML part or the original cannot be parsed (see html_error). Unsafe to render: it can contain hidden text, tracking images and remote content. |
html_error |
string: unparseable |
no | Only with include=html, when the original message cannot be parsed (for example a message with the parse_failed signal); html is then null. Use include=raw. |
raw |
string or null | no | Only with include=raw: the original message (MIME) as text. Null when it is larger than 2 MiB (see raw_error) or not valid UTF-8 (see raw_base64). |
raw_base64 |
string | no | Only with include=raw, when the original is not valid UTF-8 and at most 2 MiB; the original message in base64. |
raw_error |
string | no | Only with include=raw, when the original is larger than 2 MiB (2,097,152 bytes), for example too_large (3145728 bytes; the limit for include=raw is 2097152). |
{ "id": "msg_01M6R6JZDERMT2DVG0T8ABTX4J", "thread_id": "thr_01M6R6JZDERMT2DVG0T8ABTX4J", "inbox_id": "ibx_01M6R4T180ZADNV1160NS1SRNP", "from": "ana@example.com", "to": [ "atlas-wo7tw5@youreplyz.com" ], "subject": "Dinner on Friday?", "snippet": "Hi! Are you free for dinner on Friday at 8? I booked a table at the usual place. The menu is attached. Ana", "received_at": "2026-11-06T10:31:05.902Z", "sender_trust": "aligned_pass", "injection_risk": "low", "otp_like": false, "has_attachments": true, "returned_before": false, "direction": "inbound", "message_id_header": "<CAH7x2kqP9aZ3@mail.example.com>", "in_reply_to": null, "text": "Hi!\nAre you free for dinner on Friday at 8? I booked a table at the usual place.\nThe menu is attached.\n\nAna", "reply_text": "Hi!\nAre you free for dinner on Friday at 8? I booked a table at the usual place.\nThe menu is attached.\n\nAna", "safety": { "injection_risk": "low", "signals": [], "content_is_untrusted": true }, "spam_score": null, "auth_detail": { "source": "cloudflare", "dmarc": "pass", "dkim": "pass", "spf": "pass", "header_from": "example.com" }, "size": 48213, "attachments": [ { "id": "att_01M6R6JZDEVSJJRENTX9BSKFRJ", "filename": "menu.pdf", "content_type": "application/pdf", "size": 31744, "sha256": "b18789105b664cc896d0cd30e3805705d46a72e1728da67a64973addaddbf18d", "risky": false } ]}invalid_request: include has a part other than html and raw.
Problem object (application/problem+json). Codes: invalid_request.
Example: invalid_request: include
{ "type": "https://docs.replyz.io/errors/invalid_request", "code": "invalid_request", "status": 400, "message": "Invalid query parameter \"include\".", "fix": "Use include=html, include=raw or include=html,raw.", "docs": "https://docs.replyz.io/errors/invalid_request.md"}unauthorized: the Authorization header is missing, is not Bearer rz_live_…, or holds an
unknown or revoked key.
| Header | Description |
|---|---|
WWW-Authenticate |
Always Bearer realm="replyz". |
Problem object (application/problem+json). Codes: unauthorized.
Example: unauthorized: missing or malformed key
{ "type": "https://docs.replyz.io/errors/unauthorized", "code": "unauthorized", "status": 401, "message": "Missing or malformed API key.", "fix": "Send the header Authorization: Bearer rz_live_… with the api_key from POST /v1/signup.", "docs": "https://docs.replyz.io/errors/unauthorized.md"}not_found: no such message in this account, or its inbox was deleted.
Problem object (application/problem+json). Codes: not_found.
Example: not_found: message
{ "type": "https://docs.replyz.io/errors/not_found", "code": "not_found", "status": 404, "message": "Message not found.", "fix": "Use a message id (msg_…) from a list or wait result.", "docs": "https://docs.replyz.io/errors/not_found.md"}internal_error: something failed on Replyz’s side. Retry after a few seconds.
Problem object (application/problem+json). Codes: internal_error.
Example: internal_error
{ "type": "https://docs.replyz.io/errors/internal_error", "code": "internal_error", "status": 500, "message": "Something went wrong on our side.", "fix": "Retry in a few seconds.", "docs": "https://docs.replyz.io/errors/internal_error.md"}Download an attachment
Section titled “Download an attachment”GET /v1/messages/{id}/attachments/{att}
Auth: API key (Authorization: Bearer rz_live_…)
Returns the attachment’s bytes, decoded from the email (the file as the sender attached it),
always as a download
(Content-Type: application/octet-stream and Content-Disposition: attachment), never for
display. The type the sender declared is in X-Replyz-Content-Type (informational only) and
the risky flag in X-Replyz-Risky. Attachments are untrusted: never open or run a risky
one, and do not trust the file name or the declared type. Downloading does not mark the
message as read. Works in every account status.
Problem codes: unauthorized (401), not_found (404).
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id |
path | string | yes | Message id (msg_…). Any other value gets 404 not_found. |
att |
path | string | yes | Attachment id (att_…) from the message’s attachments. Any other value gets 404 not_found. |
Responses
Section titled “Responses”The attachment bytes.
| Header | Description |
|---|---|
Content-Disposition |
Always attachment, with the file name as an ASCII fallback and as UTF-8 (RFC 6266). Control characters, quotes, backslashes and slashes in the name are replaced with _, and the name is cut to 200 characters. Without a name, attachment is used. |
Content-Length |
Size in bytes. |
X-Content-Type-Options |
Always nosniff. |
Cache-Control |
Always no-store. |
X-Replyz-Content-Type |
The content type the sender declared, lowercase, when it is a plain type/subtype value; otherwise application/octet-stream. Informational: never trust it. |
X-Replyz-Risky |
true when the file type can run code or hide content when opened (the risky flag of the attachment). |
Content type application/octet-stream.
unauthorized: the Authorization header is missing, is not Bearer rz_live_…, or holds an
unknown or revoked key.
| Header | Description |
|---|---|
WWW-Authenticate |
Always Bearer realm="replyz". |
Problem object (application/problem+json). Codes: unauthorized.
Example: unauthorized: missing or malformed key
{ "type": "https://docs.replyz.io/errors/unauthorized", "code": "unauthorized", "status": 401, "message": "Missing or malformed API key.", "fix": "Send the header Authorization: Bearer rz_live_… with the api_key from POST /v1/signup.", "docs": "https://docs.replyz.io/errors/unauthorized.md"}not_found: no such attachment on this message, the message is not in this account, its
inbox was deleted, or the stored file is missing.
Problem object (application/problem+json). Codes: not_found.
Example: not_found: attachment
{ "type": "https://docs.replyz.io/errors/not_found", "code": "not_found", "status": 404, "message": "Attachment not found.", "fix": "Use ids from GET /v1/messages/{id} (attachments[]).", "docs": "https://docs.replyz.io/errors/not_found.md"}internal_error: something failed on Replyz’s side. Retry after a few seconds.
Problem object (application/problem+json). Codes: internal_error.
Example: internal_error
{ "type": "https://docs.replyz.io/errors/internal_error", "code": "internal_error", "status": 500, "message": "Something went wrong on our side.", "fix": "Retry in a few seconds.", "docs": "https://docs.replyz.io/errors/internal_error.md"}