---
title: "List outbound sent emails"
method: GET
path: "/sent-emails"
tags: ["Sending"]
---

# List outbound sent emails

`GET /sent-emails`

Returns a paginated list of OUTBOUND emails the caller's
org has sent via /send-mail (and /emails/{id}/reply, which
forwards through /send-mail). Includes every recorded
attempt, including gate-denied attempts that the agent
never called and rows still in `queued` state.

For inbound mail received at your verified domains, see
/emails. There is no unified send/receive history endpoint;
the two surfaces are intentionally separate because the
underlying tables, statuses, and lifecycle differ.

Email bodies (`body_text`, `body_html`) are NOT included on
list rows so a 50-row page can't balloon into a multi-MB
response when sends are near the 5MB body cap. Use
/sent-emails/{id} to fetch a single row with bodies, or
cross-reference by `client_idempotency_key` if the caller
already has the body locally.

## Query parameters

- `cursor` string
- `limit` integer
- `status` 'queued' | 'submitted_to_agent' | 'agent_failed' | 'gate_denied' | 'unknown' | 'delivered' | 'bounced' | 'deferred' | 'wait_timeout' — Lifecycle status of a sent_emails row. Possible values: - `queued`: pre-call INSERT; the outbound agent has not yet replied. - `submitted_to_agent`: agent accepted; `queue_id` is set. - `agent_failed`: agent rejected; `error_code` and `error_message` carry the reason. - `gate_denied`: a recipient-scope gate denied the send; the agent was never called. The `gates` array carries the denial detail. /send-mail returns 403 in this case so callers see the denial synchronously; /sent-emails additionally records the row for historical lookup, which is when this status appears in a listing. - `unknown`: terminal indeterminate; the on-box log poller couldn't classify the receiver's response. - `delivered` / `bounced` / `deferred` / `wait_timeout`: terminal delivery outcomes (see DeliveryStatus).
- `request_id` string, uuid
- `idempotency_key` string
- `date_from` string, date-time
- `date_to` string, date-time

## Response `200`

Page of sent-email summaries

- object
  - `success` true, required
  - `meta` object, required
    - `total` integer, required — Total number of matching records
    - `limit` integer, required — Page size used for this request
    - `cursor` string, nullable, required — Cursor for the next page, or null if no more results
  - `data` object[], required
    - `id` string, uuid, required
    - `status` 'queued' | 'submitted_to_agent' | 'agent_failed' | 'gate_denied' | 'unknown' | 'delivered' | 'bounced' | 'deferred' | 'wait_timeout', required — Lifecycle status of a sent_emails row. Possible values: - `queued`: pre-call INSERT; the outbound agent has not yet replied. - `submitted_to_agent`: agent accepted; `queue_id` is set. - `agent_failed`: agent rejected; `error_code` and `error_message` carry the reason. - `gate_denied`: a recipient-scope gate denied the send; the agent was never called. The `gates` array carries the denial detail. /send-mail returns 403 in this case so callers see the denial synchronously; /sent-emails additionally records the row for historical lookup, which is when this status appears in a listing. - `unknown`: terminal indeterminate; the on-box log poller couldn't classify the receiver's response. - `delivered` / `bounced` / `deferred` / `wait_timeout`: terminal delivery outcomes (see DeliveryStatus).
    - `status_changed_at` string, date-time, required — Timestamp of the most recent status transition. Polling clients should treat `status='queued'` AND `status_changed_at` older than 5 minutes as "stuck-queued" (the post-tx UPDATE failed and the actual delivery state is recoverable from on-box logs via `queue_id` when populated, or `request_id`).
    - `created_at` string, date-time, required
    - `updated_at` string, date-time, required
    - `client_idempotency_key` string, nullable — Effective idempotency key used for this send. If the caller passed the `Idempotency-Key` header, this is that value; otherwise it's a server-derived hash of the canonical request payload.
    - `content_hash` string, required — Stable hash of the canonical send payload.
    - `from_header` string, required — Raw `From:` header as sent on the wire, including any display name (e.g. `"Acme Support" <agent@acme.test>`).
    - `from_address` string, required — Bare email address parsed from `from_header`.
    - `to_header` string, required — Raw `To:` header as sent on the wire, including any display name.
    - `to_address` string, required — Bare email address parsed from `to_header`.
    - `subject` string, required
    - `body_size_bytes` integer, required — Total UTF-8 byte length of `body_text` + `body_html`. Surfaced on the list endpoint so callers can see "this row has a 4MB body" without fetching it.
    - `content_discarded_at` string, date-time, nullable — Timestamp at which the bodies were discarded by an entitlement-driven retention policy. Null when bodies are still present. The detail endpoint returns null-valued `body_text`/`body_html` for discarded rows.
    - `message_id` string, nullable — Wire-level Message-ID assigned to the outbound message (RFC 5322). Null on rows that never reached signing (queued, gate_denied, agent_failed before signing).
    - `in_reply_to` string, nullable — Wire-level In-Reply-To header value, when this send was a reply.
    - `email_references` string, nullable — Wire-level References header value, when this send was a reply.
    - `in_reply_to_email_id` string, uuid, nullable — Reference to the inbound `emails.id` that this send replied to, when known. Populated when the caller used /emails/{id}/reply or when /send-mail's `in_reply_to` matched a stored inbound message_id in the same org.
    - `thread_id` string, uuid, nullable — Conversation thread this send belongs to. A reply inherits the thread of the inbound it answers; a fresh send starts a new thread. Fetch `/threads/{thread_id}` for the full ordered thread (inbound + outbound interleaved). NULL on gate-denied sends and on sends created before threading was enabled.
    - `queue_id` string, nullable — Message identifier assigned by Primitive's outbound relay once the agent accepts the message. Null on queued, gate_denied, and agent_failed rows.
    - `smtp_response_code` integer, nullable — Receiver's 3-digit SMTP code (e.g. 250, 550, 451). Populated on terminal delivery statuses; may be null on a deferred where the agent never got an SMTP-level response (TCP refused, DNS failed, TLS handshake failed). `smtp_response_text` still carries Postfix's descriptive text in those cases.
    - `smtp_response_text` string, nullable — Free-form text portion of the receiver's SMTP response. The most useful debugging signal on a `bounced` or `deferred` row.
    - `smtp_enhanced_status_code` string, nullable — RFC 3463 enhanced status code (e.g. `5.1.1` for "Bad destination mailbox address"). Distinct from `smtp_response_code`: the basic 3-digit code is coarse (550 = "permanent failure"), the enhanced code is finer-grained.
    - `dkim_selector` string, nullable — DKIM selector used to sign the outbound message. Public DNS data; useful for diagnosing why a downstream verifier rejected the signature.
    - `dkim_domain` string, nullable — DKIM signing domain.
    - `error_code` string, nullable — Stable public error code on `agent_failed` rows. The agent's internal codes are remapped to a stable public taxonomy (see `publicAgentError` in the server) so this field is safe to branch on across agent versions.
    - `error_message` string, nullable — Free-form error message accompanying `error_code`.
    - `gates` object[], nullable — Gate-denial detail on `gate_denied` rows. Mirrors the synchronous /send-mail 403 contract so a caller's GateDenial handler is the same across live denies and historical lookups. Null on every other status.
      - `name` 'send_to_confirmed_domains' | 'send_to_known_addresses', required — Public recipient-scope gate name that denied the send.
      - `reason` 'domain_not_confirmed' | 'recipient_unauthenticated' | 'recipient_not_known', required — Stable machine-readable denial reason.
      - `message` string, required — Human-readable explanation of the gate denial.
      - `subject` string, required — Domain or address the gate evaluated.
      - `fix` object
        - `action` 'confirm_domain' | 'sender_must_fix_authentication' | 'wait_for_inbound', required — Suggested next action for the caller.
        - `subject` string, required — Entity the action applies to.
      - `docs_url` string — Public docs URL with more context.
    - `request_id` string, nullable — Server-issued request identifier from the original /send-mail call. Surfaced as the `X-Request-Id` response header on the live send and recorded here for support escalation.

## Other responses

- `400` — Invalid request parameters
- `401` — Invalid or missing API key

---

[API](https://skmtc.dev/primitive/apis/primitive-api.md) · [All operations](https://skmtc.dev/primitive/apis/primitive-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/primitive/primitive-api/revisions/c2299d28022c/schema)
