---
title: "Cancel a scheduled send"
method: POST
path: "/sent-emails/{id}/cancel"
tags: ["Sending"]
---

# Cancel a scheduled send

`POST /sent-emails/{id}/cancel`

## Request

- Base URL: `https://api.primitive.dev/v1`
- URL: `https://api.primitive.dev/v1/sent-emails/{id}/cancel`
- Auth: HTTP bearer

Cancels a STILL-SCHEDULED send (status `scheduled`), moving it
to the terminal `canceled` status. Nothing is dispatched and
the row is kept for historical lookup with `canceled_at` set.

Uses the same compare-and-swap guard as reschedule: once the
scheduler has claimed the row for execution, or it was already
canceled or executed, the call returns a 409 `not_scheduled`
conflict naming the row's current status. Canceling can
therefore never race an in-progress execution; a send that
reports `canceled` was never handed to the delivery path.

Returns the full updated sent-email record on success.

## Headers

- `Idempotency-Key` string

## Response `200`

Canceled sent-email detail

- object
  - `success` true, required
  - `data` SentEmailDetail, 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' | 'scheduled' | 'canceled', 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). - `scheduled`: created with a future `scheduled_at` and not yet executed; `scheduled_at` carries the pending execution time. Reschedulable via PATCH /sent-emails/{id} and cancelable via /sent-emails/{id}/cancel while in this status. - `canceled`: terminal; a scheduled send canceled before execution. `canceled_at` carries the cancellation time and nothing was dispatched.
    - `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` GateDenial[], 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` GateFix
        - `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.
    - `scheduled_at` string, date-time, nullable: Requested execution time for a scheduled send. Kept after execution as the historical schedule; null on ordinary immediate sends.
    - `canceled_at` string, date-time, nullable: When a scheduled send was canceled. Null unless the row reached the `canceled` status.
    - `presence_control` PresenceControl, nullable
      - `status` 'verified' | 'pending' | 'rejected', required
      - `valid_for_ms` integer, required
    - `repeat` object, nullable: Set when Primitive sent this message as part of a repeating send. Resolved from Primitive's own records, never from the message content. Null on every other message and on servers that predate repeating sends.
      - `repeat_id` string, uuid, required
      - `sequence` integer, required
    - `sender_member` object, nullable
      - `address` string, email, required
      - `user_id` string, required
      - `name` string, nullable, required
    - `delivery_recipients` object[], nullable: Per-recipient SMTP outcomes, including partial failures even when the aggregate send status is delivered. Sender-only; empty when withheld from another recipient or when legacy metadata has no outcomes. Null means stored delivery metadata is unreadable. Optional on older payloads.
      - `address` string, required
      - `result` 'delivered' | 'bounced' | 'deferred', required
      - `smtp_response_text` string, nullable, required
      - `smtp_enhanced_status_code` string, nullable, required
    - `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.
    - `headers` IncludedMessageHeaders: Stored addressing and threading headers. A member is null when absent or withheld.
      - `from` string, nullable, required
      - `to` string[], nullable, required
      - `cc` string[], nullable, required
      - `bcc` string[], nullable, required
      - `reply_to` string[], nullable, required
      - `in_reply_to` string[], nullable, required
      - `references` string[], nullable, required
      - `automation` object, nullable, required: Received mail only.
    - `attachments` object[]: Metadata for submitted inline attachments; download availability is reported separately. Metadata may exist when the archive is unavailable, and legacy sends may have no metadata. Offloaded payload files are not included in the inline archive.
      - `filename` string, nullable, required
      - `content_type` string, required
      - `size_bytes` integer, required
      - `sha256` string, required
      - `part_index` integer, required
      - `tar_path` string, required
    - `truncated` boolean: With `include`: true when a body on this row was cut or left out.
    - `truncated_fields` string[]: With `include`: the body fields on this row that were cut or left out.
    - `interaction_hint` 'status' | 'card' | 'none', required: How to place this send in a timeline without downloading it, decided when it was sent from the `interaction.json` part it carried (no authentication check: the sending organization wrote it). Same values as on received email except that a send is never `pending`. Decided once by the server from the message's authoritative `interaction.json` part, and for received mail only when a passing DKIM signature covering From and Content-Type authenticates the From that wrote it; the `X-Primitive-Interaction` header is never used, so an unsigned or forged message is always `none`. `status`: a pure status signal (`read`, `working`, `typing`, or an `ack` that is the strict signal shape), to be shown as a property of the message it is about, not as a row. `card`: an interaction that renders as its own timeline item (`ack-request`, `wake.dispatch`, `primitive.contact`, `repeat.tick`, `repeat.stop`, and the reserved `approval` and `schedule.send`). `none`: ordinary mail, including any interaction kind not listed. Treat an unfamiliar value as `none`.
    - `interaction_kind` string, nullable, required: The `<protocol>/<version>` the hint was decided from when `interaction_hint` is `status` or `card`; null otherwise.
    - `snippet` string, nullable, required: A plain-text preview of `body_text` (never `body_html`), computed once when the message is stored: quoted replies (an "On ... wrote:" line, an "Original Message" separator, a From/Sent header block, `>` lines) and a `-- ` signature are cut off, whitespace is collapsed to single spaces, and the result is at most 200 characters. A message with no text body, or with nothing left once quotes and the signature are cut (a quote-only reply), has no snippet. It is not a summary and may end mid-word; read the full body from the detail endpoint. Null when the send has no text body or its content was discarded.
    - `recipient_count` integer, nullable: Every recipient of the send, To, Cc and Bcc together, each address counted once: the size of `to_addresses`, `cc` and `bcc` combined without duplicates. On a mail relay send `bcc` lists every hidden recipient. Null when the request uses an address-bound agent connection key whose address is not the sender, because the count would reveal Bcc recipients.
    - `hold` object: Only on a held scheduled send: its OAuth grant was revoked after refresh token reuse, so it waits until released (POST /sent-emails/{id}/release) or canceled; canceled after 7 days. Status stays scheduled.
      - `reason` string, required: Open: credential_reuse_hold.
      - `since` string, date-time, nullable, required
    - `attachments_size_bytes` integer: Original decoded attachment byte total accepted for this send, not the compressed archive size. It may exceed the retained inline total after offloading.
    - `attachments_complete` boolean: True only when the returned attachments enumerate the complete original submitted attachment set, including zero-byte and inline CID parts, with retained content and a published archive for nonempty inventory. False when any submitted payload-reference, offloaded, or visibility-filtered attachment is omitted, or completeness is unknown. False or absent does not prove absence of attachments. This field does not grant download access.
    - `attachments_download_available` boolean: Whether an inline attachment archive has been successfully retained and is available for download. Download authorization is checked separately; address-bound agent connection keys cannot download archives.
    - `to_addresses` string[], nullable: Every normalized To recipient of the send. Always populated on new sends (a single-recipient send carries a one-element array); null only on records that predate this field. `to_address` keeps carrying the first recipient.
    - `cc` string[], nullable: Normalized Cc address list. Null when the send had no Cc recipients.
    - `bcc` string[], nullable: Normalized Bcc address list, surfaced on the sending organization's own record. Bcc recipients are never written into the transmitted message. Null when the send had no Bcc recipients. Also null when the request uses an address-bound agent connection key whose address is not the sender, because Bcc is withheld from recipients.
    - `reply_to` string[], nullable: Normalized Reply-To address list. Null when the send had none.
    - `tags` object[], nullable: Customer metadata tags stored verbatim at send time. Null when the send had none. Also null when the request uses an address-bound agent connection key whose address is not the sender, because tags are withheld from recipients.
      - `name` string, required
      - `value` string, required

## Other responses

- `400`: Invalid request parameters
- `401`: Invalid or missing API key
- `404`: Resource not found
- `409`: The send exists but is no longer in `scheduled` status, so it cannot be canceled. `error.details` carries the row's `sent_email_id` and current `status`.

## Changes

> 32 revisions in range; 1 not diffed.

- **2026-10-21** `0360ea8d4f3d`: 52 warning
  - added the new `agent_changed` enum value to the `error/code` response property for the response status `400`
  - added the new `agent_changed` enum value to the `error/code` response property for the response status `401`
  - added the new `agent_changed` enum value to the `error/code` response property for the response status `404`
  - added the new `agent_changed` enum value to the `error/code` response property for the response status `409`
  - …48 more
- **2026-10-21** `5279a7bed7ac`: 44 warning, 7 info
  - added the new `archive_busy` enum value to the `error/code` response property for the response status `400`
  - added the new `archive_busy` enum value to the `error/code` response property for the response status `401`
  - added the new `archive_busy` enum value to the `error/code` response property for the response status `404`
  - added the new `archive_busy` enum value to the `error/code` response property for the response status `409`
  - …47 more
- **2026-10-09** `6c9e8a907166`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/primitive/apis/primitive-api/changes/sent-emails/:id/cancel/post.md)

---

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