Skip to content
Not available yet. Replyz is built and tested, but not deployed: https://api.replyz.io is the planned address and does not answer yet. These pages describe the API as it is built today and may change before launch.

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.

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).

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.

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"
}

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); pass exclude_returned=true to get only messages never returned.
  • If nothing matches, the request is held for up to timeout seconds (default 25, at most 60) and returns as soon as a matching message arrives. If none arrives, it returns status: "timeout" with no messages: call it again. timeout=0 checks once without holding.
  • Set your HTTP client timeout above timeout (for example timeout plus 10 seconds).
  • An account can hold at most 5 requests at once, shared with GET /v1/events?wait=; one more gets 429 too_many_waits (with Retry-After: 1). A call that does not need to hold (mail is already there, or timeout=0) never counts.
  • At most limit messages are returned (the newest ones). The from and subject filters are applied to the newest 500 messages that match the other filters.
  • For an expired or suspended account (including a sandbox account past its expires_at) the call never holds: timeout is still validated, then treated as 0.
  • extract=otp, extract=link or extract=otp,link adds extracted to each returned message, with only the requested parts (see the Extraction tag). A sandbox account flagged for an OTP flood may run at most 10 extractions per UTC hour; past that budget, messages come with extracted: null and extract_limited: true instead of an error. Messages are marked as read either way.

Problem codes: unauthorized (401), not_found (404), invalid_request (400), too_many_waits (429).

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.

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 /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 the parse_failed signal), html is null and html_error is unparseable; use include=raw for 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 as raw_base64 instead; above 2 MiB, raw is null and raw_error says why.

These fields are left out when the original is not available.

Problem codes: unauthorized (401), not_found (404), invalid_request (400).

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.

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"
}

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).

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.

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"
}