---
title: "Get a sent email by id"
method: GET
path: "/sent-emails/{id}"
tags: ["Sending"]
---

# Get a sent email by id

`GET /sent-emails/{id}`

Returns the full sent-email record by id, including
`body_text` and `body_html` (omitted from the listing
endpoint to keep paginated responses small). Use this when
diagnosing a specific send, e.g. inspecting the receiver's
SMTP response on a `bounced` row or pulling the gate
denial detail on a `gate_denied` row.

## Path parameters

- `id` string, uuid, required

## Response `200`

Sent-email detail

- object
  - `success` true, required
  - `data` object, required — List-row projection of a sent-email record. Drops `body_text` and `body_html` to keep paginated responses small; fetch /sent-emails/{id} for the full record with bodies.
    - `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.
    - `body_text` string, nullable — Plain-text body sent on the wire. Null when the send carried only an HTML body, or when bodies have been discarded post-send (`content_discarded_at` set).
    - `body_html` string, nullable — HTML body sent on the wire. Null when the send carried only a plain-text body, or when bodies have been discarded post-send.

## Other responses

- `400` — Invalid request parameters
- `401` — Invalid or missing API key
- `404` — Resource not found

---

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