---
title: "Wait for a received message"
method: GET
path: "/v1/messages/received/wait"
tags: ["Inbound"]
---

# Wait for a received message

`GET /v1/messages/received/wait`

## Request

- Base URL: `https://api.mails.ai`
- URL: `https://api.mails.ai/v1/messages/received/wait`
- Auth: HTTP bearer

**Requires scope:** `read`. Answers with the newest received message that matches, holding on for up to `timeout` seconds until one arrives. Made for an agent that has just asked a service for a sign-up code, a magic link, a password reset or an invite-accept link: name the sender with `from` and what you need with `has`, then read `message.codes[0].value` or the first `message.links` entry. When you gave the service a plus address of the agent's (`qa.acme+run-1842@in.mails.ai`), pass it as `to`, and only mail sent to it answers. When mail sent to `to` arrived past your plan's plus addresses a day, only a record of it was kept, so the call answers 402 `plan_limit_exceeded` at once instead of waiting. Mail judged forged (`auth_failed`) and quarantined mail never match. Nothing in time is not an error: `message` is null and `timed_out` true.

## Query parameters

- `agent_id` string
- `from` string
- `to` string
- `since` string, date-time
- `has` 'code' | 'link'
- `timeout` integer

## Response `200`

The matching message, or none in time.

- object
  - `message` ReceivedMessage, required
    - `id` string, required: Received message id (`rcv_…`).
    - `agent_id` string, required: Id (`agt_…`) of the agent that received the message.
    - `thread_id` string: The thread (`thrd_…`) the message is filed in. Absent when it was stored without one.
    - `from` object, required: Who the message is from: the first mailbox of its From header, the address `dkim_domain` and `dmarc` are about. It is proven only when `dmarc` is `pass`. When no raw email reached us it is read from the From header our mail server passed on, and the envelope sender stands in only when there was none; on mail received before `envelope_from` was added it can be the envelope sender.
      - `address` string, email, required: The sender's address, in lower case. On mail made with `POST /v1/test/inbound` it is the `from` that was sent, as written, or the raw email's From address.
      - `name` string, nullable: The display name beside that address, decoded. Absent when there is none, or when the envelope sender stands in for `from`.
    - `envelope_from` string, nullable: The envelope sender (SMTP MAIL FROM): where bounces go, which a sending service often sets to its own bounce address. Replies go to the Reply-To or, without one, to `from`, never here. Null on mail received before this field was added and on messages made with `POST /v1/test/inbound`.
    - `to` string, email, required: The agent that received it, by its API address (its `email`; a single string, not an array). People write to an agent at its `receiving_address`.
    - `delivered_to` string, nullable: The address the email was sent to, in lower case: the agent's `receiving_address`, or a plus address of it (`qa.acme+run-1842@in.mails.ai`), which reaches the same agent. What the `to` filter matches. On a message made with `POST /v1/test/inbound` it is that request's `to`, or the `receiving_address`. Null on mail received before 2026-10-16.
    - `subject` string: The email's subject. Absent when it has none, and on a `rate_limited`, `plus_address_limit` or `held_code` record (`over_limit`), which keeps no content.
    - `is_thread_reply` boolean: True when the email's In-Reply-To names one of this agent's own sent messages; false otherwise, even when the message joined a thread by its subject, as mail from someone already in that thread does.
    - `in_reply_to_message_id` string: The email's In-Reply-To header: a bare `msg_…` id when it names one of our own messages, otherwise as received. Absent when there is none, and on mail received over a limit.
    - `thread_root_message_id` string: Id (`msg_…`) of the agent's sent message this one replies to. Present only when `is_thread_reply` is true.
    - `body_text` string: Full body — single-message GET only.
    - `body_html` string: The HTML part — single-message GET only, and only for mail the injection scan read (the scan reads the HTML too). Absent for mail answered by the rules layer, or received while the workspace was over its inbound quota.
    - `body_text_excerpt` string: The first 500 characters of the plain-text body; the whole text is `body_text` on the single-message GET. Absent on a `rate_limited`, `plus_address_limit` or `held_code` record, which keeps no content.
    - `extracted_text` string: Reply text with quoted history stripped.
    - `extracted_html` string: The HTML counterpart of `extracted_text`: the email's HTML part with its quoted history stripped, which for mail that quotes nothing is the whole HTML part. Present only for mail the injection scan read, as `body_html` is; absent when the email had no HTML part or nothing was left once the quoted history was stripped, and on mail received before this field was filled in.
    - `codes` object[], required: Verification codes in the email (sign-up, sign-in, one-time and confirmation codes), most likely first, at most 5. Read by rule from the subject, the text and, when the text has none, the HTML: a code is 4 to 8 digits, a prefixed one such as `G-123456` (its digits), 6 to 9 capitals and digits mixed, or 6 to 10 letters and digits straight after `code is` or `code:`, and counts only beside a word that names it (code, verification, one-time, passcode, OTP and the like), so an order number, a price, a date or a phone number in the same email is not one. Empty when there is none.
      - `value` string, required: The code as it is typed: digits only when it was digits (`123 456` gives `123456`), otherwise as written.
    - `links` object[], required: The email's http(s) links, one entry per address: `verify`, `sign_in` and `reset_password` first, `unsubscribe` last, at most 25. The words on a link are not returned (they are the sender's HTML); `kind` says what they meant.
      - `url` string, uri, required: The address, as the email had it (a tracking link stays one).
      - `kind` 'verify' | 'sign_in' | 'reset_password' | 'unsubscribe' | 'other', required: What the link is for, from the words on it and in its address: `verify` (confirm an email address or an account), `sign_in` (a magic link), `reset_password`, `unsubscribe`, or `other`.
    - `spf_pass` boolean: True when SPF passed, false when it failed. SPF is not checked on real mail, so it is absent there; mail made with `POST /v1/test/inbound` carries the `spf` that was sent (default pass), absent for `unknown`.
    - `dkim_pass` boolean: True when a DKIM signature verified, false when the email was signed and none did; absent when it was unsigned or could not be checked. Mail made with `POST /v1/test/inbound` carries the `dkim` that was sent.
    - `dkim` 'pass' | 'fail' | 'none' | 'unknown': As the inbound webhook events carry it: `pass` when a DKIM signature verifies, whichever domain signed it; `fail`; `none` for an unsigned email; `unknown` when it could not be checked, and on mail received before this field was added.
    - `dkim_domain` string, nullable: The domain of a passing DKIM signature that matches the domain of `from.address`, or null.
    - `dmarc` 'pass' | 'fail' | 'none' | 'unknown': For the domain of `from.address`: `pass` when it publishes DMARC and a DKIM signature aligned with it verifies, the one case in which `from.address` is proven; `none` when it publishes no DMARC record; `unknown` when no aligned signature verifies or it was not checked (as on mail received before this field was added). `fail` only where an SPF verdict is trusted.
    - `auth_failed` boolean: True when the sender was judged forged: no DKIM signature aligned with the domain of `from.address` verifies, that domain's DMARC policy is quarantine or reject, and no listed mail service that forwarded the email vouches for the sender (the event's `trusted_forwarder`). It is the verdict behind `message.received.unauthenticated`, whose `auth_failed` says the same. Mail kept unread because the workspace was over a limit is not checked, and is false. Mail made with `POST /v1/test/inbound` is true when it was sent with `spf` and `dkim` both `fail`.
    - `has_attachment` boolean, required: True when the message carries at least one file attachment (images embedded in the HTML body do not count).
    - `attachments` ReceivedAttachment[], required: Every part of the message that is a file, embedded images included. Download one from `GET /v1/messages/received/{id}/attachments/{attachment_id}`.
      - `id` string, required: Attachment id (`att_…`), unique within the message.
      - `filename` string, required: The file's name, cleaned: no path, no control characters, at most 255 characters.
      - `content_type` string, required: The MIME type the sender declared, e.g. `application/pdf`.
      - `size` integer, required: Size in bytes of the decoded file.
      - `content_id` string, nullable, required: The part's Content-ID without angle brackets, which the HTML body references as `cid:`.
      - `inline` boolean, required: True for an image embedded in the HTML body rather than attached as a file.
    - `attachments_omitted` OmittedAttachment[]: Files the message carried that were not kept, each with the reason. At most 100 are listed; `attachments_omitted_count` counts them all. Present only when there are some.
      - `filename` string, required: The file's name, cleaned like a kept attachment's: no path, no control characters, at most 255 characters. A file the email did not name reads `attachment.<ext>`.
      - `content_type` string, required: The MIME type the sender declared, in lower case; `application/octet-stream` when it was missing or malformed.
      - `size` integer, nullable, required: Size in bytes when known.
      - `reason` 'too_large' | 'budget' | 'over_quota' | 'unavailable' | 'too_many', required: Why the file was not kept: the reasons of `raw_omitted`, or `too_many` for files past the first 100 of one message.
    - `attachments_omitted_count` integer: How many files were not kept, when more than 100 could not all be listed. Present only when above 0.
    - `raw_omitted` 'too_large' | 'budget' | 'over_quota' | 'unavailable': Present when the raw email was not kept, and why: `too_large` (over 25 MiB, or over 3 MB from an edge that could not send it in pieces), `budget` (the workspace kept its day's allowance of raw mail), `over_quota` (received while the workspace was over a limit; `over_limit` says which), `unavailable` (it never reached us, e.g. mail from before raw emails were kept). Without the raw email the message has its text but no downloadable attachments.
    - `over_limit` 'quota_exceeded' | 'subscription_inactive' | 'rate_limited' | 'plus_address_limit' | 'held_code': Present when the message arrived while the workspace was over a limit, and which: `quota_exceeded` (the month's inbound allowance was used up) or `subscription_inactive` (the subscription was canceled or paused), both kept but not classified; `rate_limited` (more mail in an hour or a day than the plan takes), `plus_address_limit` (sent to a new plus address after the plan's plus addresses for the day were used: 20 on Free, 1,000 on Pro, 10,000 on Scale) or `held_code` (sent to a plus address with a sign-in code or link from a service such as Google, Microsoft, Apple, a payment app or a bank, whose codes plus addresses do not receive), each a record of who sent it, to whom and when, without its subject or body (at most 20 such records an hour; past that, such mail is not recorded). Each raises `message.received.over_limit`.
    - `parse_status` 'parsed' | 'quarantined' | 'pending' | 'failed' | 'quota_skipped', required: `quota_skipped`: not classified or scanned, because the workspace was over a limit (`over_limit` says which). It is still in a thread, so it can be trashed and deleted, and raised `message.received.over_limit` instead of `message.received`.
    - `injection_score` number, nullable, required: How likely the message is a prompt-injection attempt, from 0 to 1, as the inbound scan scored it (the same score as the inbound event's `injection_score`). Null when the message was never classified (`parse_status` pending, failed or quota_skipped).
    - `quarantined` boolean, nullable, required: True when the scan scored the message at or above the quarantine line (0.95): it is still delivered, marked, so your agent can skip it. Null when the message was never classified.
    - `received_at` string, date-time, required: When the message reached us (UTC), not the email's own Date header.
    - `raw_url` string: Path to the message as an .eml file — single-message GET only. The exact message as received when it was kept, else one rebuilt from the stored fields.
    - `created_at` string, date-time: When the message's record was written; `received_at` is when the mail arrived.
  - `timed_out` boolean, required: True when the wait ended without a match.

## Other responses

- `400`: Invalid request — malformed JSON or a field failed validation. Also `feature_not_enabled` when this server has a feature switched off (custom domains before they are available).
- `401`: Missing or invalid API key.
- `402`: Payment required: a plan limit (`plan_limit_exceeded`) or a feature the current plan does not include (`feature_not_enabled`). The error object carries `upgrade_url`, the dashboard page where the plan can be changed.
- `403`: The API key lacks the required scope (`insufficient_scope`), or the token belongs to an app connected by sign-in and this operation is closed to connected apps (`connected_app_not_allowed`).
- `409`: Conflict — duplicate resource, an in-progress idempotent request, a draft or scheduled message that started sending while it was being changed, or a thread deleted for good while one of its messages is being sent (`duplicate_resource`); or an agent name more than one agent has (`agent_name_conflict`: use the agent's id); or a status change on an archived agent, since archiving cannot be undone (`agent_archived`).
- `429`: Rate or quota limit exceeded. The body's `resets_at` and `retry_after_seconds` say when the request can succeed. A `Retry-After` header is sent only when that is 60 seconds or less; a longer wait (an hourly, daily or monthly cap) sends none, so retry at `resets_at` or raise the limit instead of sleeping.
- `500`: Internal server error.

---

[API](https://skmtc.dev/mails/apis/mails-ai-api.md) · [All operations](https://skmtc.dev/mails/apis/mails-ai-api/llms.txt) · [OpenAPI document](https://skmtc.dev/mails/apis/mails-ai-api/revisions/4d4930a8c33b?raw)
