---
title: "List received emails"
method: GET
path: "/inbound"
tags: ["Inbound"]
---

# List received emails

`GET /inbound`

## Request

- Base URL: `https://api.oveyon.com/v1`
- URL: `https://api.oveyon.com/v1/inbound`
- Auth: HTTP bearer

## Query parameters

- `limit` integer
- `cursor` string
- `recipient` string
- `sender` string
- `domain` string
- `dmarc` string
- `has_attachments` boolean
- `outcome` string
- `thread` string, uuid
- `agent_safety` string
- `from` string
- `to` string

## Response `200`

Page

- InboundList
  - `data` InboundMessage[]
    - `id` string, uuid
    - `receivedAt` string, date-time
    - `from` string, nullable: Envelope sender (MAIL FROM); `""` is a bounce (null sender).
    - `fromHeader` string, nullable
    - `fromName` string, nullable
    - `subject` string, nullable
    - `domain` string, nullable
    - `thread` InboundThreadRef, nullable: The conversation this message belongs to (grouped by In-Reply-To/References within your account, including replies you sent through OVEYON). null = not resolved (fail-open) or older than the backfill.
      - `id` string, uuid, required: Thread id — GET /v1/inbound/threads/{id}; filter the list with ?thread=.
    - `recipients` object[]: One item per mailbox that received the message. Each recipient is judged by the policies and the security quarantine of ITS mailbox: one copy can be delivered while another is held.
      - `id` string
      - `to` string
      - `outcome` string: This copy: `accepted` (delivered) or `quarantined` (held for review). Messages from before per-mailbox policies carry the message outcome here.
      - `heldBy` 'agent_safety' | 'policy' | 'null', nullable: Who held this copy (kept after release, as history).
      - `policy` InboundPolicyInfo, nullable: The inbound policy outcome for this message (F4): the effective action, the name of the policy that closed the chain (or the first one that matched), and the accumulated tags. `would_<action>:<name>` tags (e.g. `would_hold:…`, `would_restrict_channels:…`) mean a policy WOULD have held/muted/restricted it, but it did not apply — see the decision feed for why; an agent should treat them as the rule's intent. null = no policy matched.
        - `action` 'hold' | 'mute' | 'tag' | 'restrict_channels' | 'pass' | 'notify'
        - `name` string, nullable
        - `tags` string[]
      - `spamFiltered` boolean: This copy's mailbox had the spam filter ON when it arrived: the message `spamFilter` applies to it (and its policies judged `spam_filter.score`). false = this mailbox did not ask; a score, if any, belongs to another mailbox of the same message.
    - `authentication` Authentication: null = not evaluated.
      - `spf` string, nullable
      - `dkim` string, nullable
      - `dmarc` string, nullable
    - `spamScore` integer, nullable
    - `spamFilter` InboundSpamFilter, nullable: The spam filter of the MAILBOX (rspamd at our receiving edge) — OPT-IN per mailbox: null when no mailbox of this message switched it on (or the message predates the filter), which is «no opinion», never «clean». It only annotates: we never refuse, hold or discard because of it; what to do with the score is your inbound policy (`spam_filter.score`). Different from `spamScore` (our own 0–100 heuristic, computed for every message).
      - `status` 'scanned' | 'unavailable' | 'skipped', required: `scanned` = there is a score; `unavailable` = the mailbox asked and the filter did not answer in time (delivered WITHOUT a score — fail-open); `skipped` = not scanned on purpose (`reason`).
      - `score` number, nullable, required: The rspamd score (decimal; negative = looks legitimate). Common thresholds: 6 (probable spam), 15 (spam). null unless `scanned`.
      - `action` 'no action' | 'greylist' | 'add header' | 'rewrite subject' | 'soft reject' | 'reject' | 'null', nullable, required: What rspamd SUGGESTS. Informational: we never execute it.
      - `reason` 'too_large' | 'bypass' | 'timeout' | 'error' | 'null', nullable, required: `skipped`: too_large (over 10 MiB) or bypass (mail from one of our own domains, on an explicit list, DKIM-signed by that same domain and passing DMARC — subdomains are not on it); `unavailable`: timeout or error.
      - `symbols` object[]: The rules that weighed, heaviest first (detail and webhook; omitted from the paginated list).
        - `name` string
        - `score` number
    - `agentSafety` AgentSafety, nullable
      - `verdict` 'clean' | 'suspicious' | 'dangerous': clean | suspicious | dangerous. Treat `null` (field absent) as «not evaluated», never as safe.
      - `score` integer: 0 = nothing found, 100 = maximum risk. The thresholds are 25 (suspicious) and 60 (dangerous).
      - `coverage` AgentSafetyCoverage, nullable: What the verdict actually looked at. null = message from before coverage was recorded. Read it before trusting `clean`.
        - `bodySampled` boolean, required: true when the scanner saw a head+tail sample (128 KiB) instead of the whole text/HTML, or a textual attachment was cut at its cap. A payload planted in the unsampled middle is not seen. Also true when an absurd header (a field over 64 KiB, a header block over 512 KiB, or over 5000 lines — 1000 in a MIME part) was cut before reading, or the message has more than 1000 MIME parts (the first 1000 are read).
        - `sanitized` boolean, required: true when hidden characters (Unicode Tags, zero-width, bidi overrides) were stripped from the text we hand you — the body, the subject or the sender display name (`fromName`). The raw copy (/raw) keeps them, for forensics.
        - `unscannedAttachments` integer, required: Attachments whose content the scanner did not read because they are not text: PDFs, images, spreadsheets, archives. Extract them yourself before a model reads them. Attached messages (.eml) ARE decoded and scanned.
      - `signals` object[]: Why the score is what it is. Present in the webhook payload and in GET /v1/inbound/{uid}; omitted from the paginated list. Each entry names one finding.
        - `type` string: Machine name of the finding, e.g. unicode_tags, imperativo_oculto, bidi, homoglifo. texto_ilegivel is a passage that cannot be normalized safely — too long without a single ASCII character (a very long paragraph of unbroken Chinese or Japanese, for example) or with too many combining marks — and is read in pieces; on its own, it leads at most to `suspicious`.
        - `points` integer: How much this finding added to the score.
        - `detail` string: Human-readable evidence — including the hidden text, decoded, so you can see what your agent would have read.
    - `sizeBytes` integer
    - `attachmentCount` integer
    - `expiresAt` string, date-time, nullable
    - `outcome` string: `accepted`, `blocked`, `quarantined` (every copy held) or `partial` (some copies delivered, some held — see `recipients[].outcome`) — branch on this, never on blockedReason. `policy` at message level is the first recipient's.
    - `blockedReason` string, nullable
    - `quarantine` InboundQuarantineInfo, nullable: Present ONLY on a message that was held by the agent-safety verdict (or, later, a reception policy) and then released by a human — panel or POST /v1/inbound/{uid}/release. Absent (null) on a normal delivery. Release is not absolution: agentSafety travels with it.
      - `reason` 'agent_safety_dangerous' | 'policy', required
      - `heldAt` string, date-time, required
      - `releasedAt` string, date-time, required
      - `releasedBy` string, nullable: `portal:<user>` or `api:<key prefix>`.
      - `agentSafety` object, nullable
        - `verdict` string
        - `score` integer
      - `policy` object, nullable: When one of your inbound policies held it (reason `policy`): its name and action.
        - `name` string
        - `action` string, nullable
    - `policy` InboundPolicyInfo, nullable: The inbound policy outcome for this message (F4): the effective action, the name of the policy that closed the chain (or the first one that matched), and the accumulated tags. `would_<action>:<name>` tags (e.g. `would_hold:…`, `would_restrict_channels:…`) mean a policy WOULD have held/muted/restricted it, but it did not apply — see the decision feed for why; an agent should treat them as the rule's intent. null = no policy matched.
      - `action` 'hold' | 'mute' | 'tag' | 'restrict_channels' | 'pass' | 'notify'
      - `name` string, nullable
      - `tags` string[]
  - `next_cursor` string, nullable

## Other responses

- `400`: Invalid request — fix it before retrying. Includes `invalid_json`: with `Content-Type: application/json` the body must be valid JSON and cannot be empty; and `request_too_complex` (with `limit`): the body has a list of more than 10,000 items, more than 60,000 values in total, more than 64 levels of nesting or a number/literal longer than 32 characters — text does not count, so a large attachment is one value. An address outside ASCII (accents, a full-width @, a control character, an international domain not in xn-- form) is refused here with `reason: address_not_ascii` and the `field` it came in (`error` stays the family of that field: `bad_recipient` for to/cc/bcc, `invalid_email` for contacts, `bad_request` otherwise).
- `401`: Missing or invalid API key.
- `403`: Insufficient scope (`insufficient_scope`, with `required` and `have`), a scope the key holds but cannot use while its human-approval lock is on (`scope_blocked_by_approval_lock`, with `required` — approve:holds, write:send-policies, manage:webhooks, write:inbound, write:templates, write:surveys, write:domains, write:contacts), API key switched off by its owner in the panel (`key_disabled` — the key is valid; turn it back on or use another one, do not rotate), sending disabled, account suspended (`account_suspended`) or unknown (`account_unknown`), or the key used from an IP outside its allowed ranges (`ip_not_allowed`, with the `allowed` list — see /v1/credentials/ip-rules). 403 is not a limit: do not retry.
- `429`: Per-key limit (`rate_limited`, 600 requests/min by default per API key — or per MCP OAuth connection —, from any IP), per-IP limit for requests WITHOUT a valid credential (`rate_limited`, 600/min by default), too many unknown or malformed credentials from your address (`too_many_auth_failures`, 20/min by default, counted per IPv4 address or per IPv6 /48; a missing `Authorization`, an expired OAuth token or a disabled key do not count, and anything without the shape of one of our keys counts without being checked; from an address over that limit a valid key still gets through within 120 credential checks/min, and above them every credential not validated in the last 5 minutes gets this 429 until the minute turns) — all checked before the route, on every operation —, account quota (`quota_exceeded` with `window`, `used`, `limit`), warmup cap (`warmup_cap_reached`) or saturation (`queue_full`). Honor `Retry-After` with exponential backoff and jitter.
- `500`: internal_error — an unexpected failure on our side. The detail stays in our logs, never in the body. Retry reads; retry a write only when it is idempotent or carries an idempotency key.
- `503`: ip_rules_unavailable — the IP ranges bound to your key could not be checked right now (caution, not a denial). Retry after `Retry-After` (30 s).

---

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