---
title: "Cancel a scheduled message"
method: POST
path: "/v3/messages/{id}/cancel"
tags: ["Messages"]
---

# Cancel a scheduled message

`POST /v3/messages/{id}/cancel`

## Request

- Base URL: `https://api.sent.dm`
- URL: `https://api.sent.dm/v3/messages/{id}/cancel`
- Auth: one of:
  - API key in header `x-api-key`
  - HTTP bearer

Calls off a message you scheduled with `scheduled_at`, before it is released for sending. The message moves to `CANCELLED`, which is terminal, and it is never sent.

**Only a SCHEDULED message can be cancelled.** A message that has already been released — queued, in flight, or finished — returns 409. The release sweeper runs continuously, so a cancellation sent within about a minute of the scheduled instant can lose that race and be refused; cancel with time to spare.

**One message per call.** A send to many recipients, or on several channels, becomes one message per recipient × channel, each with its own id; cancel the ones you want stopped. There is no bulk cancellation.

**Cancelling is free**, and nothing was charged for the hold in the first place — a scheduled message is only priced and billed at release.

**A cancellation fires `message.cancelled`** on your webhook, exactly once, carrying the `scheduled_at` that was called off so a consumer can un-record the send it was expecting. Cancelling the same message again returns this same response and fires no second event. `Idempotency-Key` is honoured.

A cancelled message cannot be resent — send a new one.

The response is the message itself, in the same shape `GET /v3/messages/{id}` returns, so the `status` you read back is the one now stored on the row, and `scheduled_at` is the time it would have gone out. `GET /v3/messages/{id}` keeps reporting both afterwards.

`events` is the message lifecycle as it stood immediately before the cancellation: the CANCELLED entry is written asynchronously and lands a moment later, so read `status` for the outcome. If you need the entry itself, re-fetch the message after a few seconds — `GET /v3/messages/{id}` is cached briefly, so an immediate re-fetch can still show the pre-cancellation state.

## Path parameters

- `id` string, uuid, required

## Headers

- `Idempotency-Key` string
- `x-profile-id` string, uuid

## Request body

- SentDmServicesEndpointsCustomerAPIv3MessagesRequestsCancelMessageV3Request: Base request for mutation operations (POST, PUT, PATCH) with sandbox mode support
  - `sandbox` boolean: Sandbox flag - when true, the operation is simulated without side effects Useful for testing integrations without actual execution

## Response `200`

The message is cancelled and will not be sent

- SentDmServicesEndpointsCustomerAPIv3SharedApiResponseOfCancelMessageV3Response: Standard API response envelope for all v3 endpoints
  - `success` boolean: Indicates whether the request was successful
  - `data` SentDmServicesEndpointsCustomerAPIv3MessagesResponsesCancelMessageV3Response: The cancelled message, in the shape GET /v3/messages/{id} reports it: a cancellation answers with the message it acted on rather than a bespoke acknowledgement, so a client can hand the body to the same reader it already has. status is read off the row the claim just wrote, not asserted by the endpoint. It carries scheduled_at for the same reason ScheduledMessageResponse does, and with the same meaning — the instant the message was being held for. A cancelled message was necessarily held, so the key is present on every real cancellation, and the two endpoints agree on what they report about the same message. It is a property here rather than inheritance because ScheduledMessageResponse is sealed.
    - `id` string, uuid
    - `customer_id` string, uuid
    - `contact_id` string, uuid
    - `phone` string
    - `phone_international` string
    - `region_code` string
    - `template_id` string, uuid, nullable
    - `template_name` string, nullable
    - `template_category` string, nullable
    - `channel` string
    - `message_body` SentDmServicesCommonEntitiesMessageBody: Structured message body format for database storage. Preserves channel-specific components (header, header media, body, footer, buttons, MMS subject and media). Persisted as the messageBody jsonb column on Messages. Every write path goes through MessageUtils.MessageBodyJsonOptions, which writes nulls, so the envelope shape is stable regardless of channel or status. Anything that rebuilds this object field by field — the four IMessageBodyStrategy implementations and MessageUtils.BuildSegmentBody — has to carry every member, or that member is silently dropped on whichever path forgot it.
      - `header` string, nullable
      - `content` string
      - `footer` string, nullable
      - `buttons` SentDmServicesCommonEntitiesMessageButton[], nullable
        - `type` string
        - `text` string, nullable
        - `value` string
        - `postbackData` string, nullable
      - `subject` string, nullable: MMS subject line. Null on every other channel.
      - `media` SentDmServicesCommonEntitiesMessageMedia[], nullable: MMS attachments, as the publicly fetchable URLs handed to the carrier. Null on every other channel. Persisted rather than derived because a resend and a curfew release rebuild the send from the stored row — MessageReplayCommandBuilder reads templateId and templateVariables and nothing else — so media that lives only on the original request would silently turn a replayed MMS into a text message.
        - `url` string, nullable: Where the file lives. Outbound: the URL the customer gave us and the carrier fetched. Inbound: the URL the carrier hosts it at, relayed unchanged.
        - `mediaType` string, nullable: One of MmsMediaTypes when the content type is known. Advisory — a reader should trust the fetched object's own Content-Type.
        - `mimeType` string, nullable: Content type as the provider declared it. Null when it declared none.
        - `sizeBytes` integer, nullable: Size as the provider declared it. Never measured here — nothing downloads the file.
        - `sourceHashSha256` string, nullable: Inbound only: the SHA-256 the provider declared alongside the attachment, when it declared one. Relayed to the customer so they can verify what they fetch matches what the carrier said it sent. It is the only integrity signal available on an attachment nobody here has read.
      - `headerMedia` SentDmServicesCommonEntitiesMessageHeaderMedia: The media asset that rode a message's header, recorded as sent.
        - `type` string: "image", "video" or "document" — taken from the header's media variable.
        - `url` string: The https URL the caller supplied for this send. Never the template's stored props.sample, which is Meta's expiring header_handle rather than what was delivered.
    - `status` string
    - `direction` string
    - `created_at` string, date-time
    - `price` number, nullable
    - `active_contact_price` number, nullable
    - `events` SentDmServicesEndpointsCustomerAPIv3MessagesResponsesMessageEventV3[], nullable
      - `status` string, required
      - `timestamp` string, date-time, required
      - `description` string, nullable
      - `reason_code` string, nullable: Why the message reached this status, as a stable platform code such as DELIVERY_007. Present on FAILED, FILTERED and BLOCKED events; omitted on every status that needs no explanation. Same wire name and vocabulary as on the activities list and the webhook.
      - `reason` string, nullable: A human-readable sentence for reason_code. Omitted whenever reason_code is.
    - `reason_code` string, nullable: Why the message is at its current status, as a stable platform code such as DELIVERY_007, BUSINESS_003 or DELIVERY_003. Present when the current status is FAILED, FILTERED or BLOCKED and the lifecycle was loaded; omitted otherwise. Switch on this rather than on reason: the code is stable, the wording may be improved. It is the platform's classification of the outcome, never a carrier or vendor code.
    - `reason` string, nullable: A human-readable sentence for reason_code, for example "Insufficient balance". Omitted whenever reason_code is.
    - `scheduled_at` string, date-time, nullable: The send time that was called off, in UTC — the scheduled_at the message was being held for, whether you set it or the recipient's quiet hours moved it there. Omitted only for a held message that carried no recorded instant.
  - `error` SentDmServicesEndpointsCustomerAPIv3SharedApiError: Error information
    - `code` string: Machine-readable error code (e.g., "RESOURCE_001")
    - `message` string: Human-readable error message
    - `details` object, nullable: Additional validation error details (field-level errors)
    - `doc_url` string, nullable: URL to documentation about this error
  - `meta` SentDmServicesEndpointsCustomerAPIv3SharedApiMeta: Request and response metadata
    - `request_id` string: Unique identifier for this request (for tracing and support)
    - `timestamp` string, date-time: Server timestamp when the response was generated
    - `version` string: API version used for this request

## Other responses

- `400`: Invalid message ID
- `401`: Unauthorized - Invalid API credentials
- `403`: Forbidden
- `404`: Message not found
- `409`: Message is not scheduled, so there is nothing to cancel (already released, in flight, or finished)
- `429`: Rate limit exceeded
- `500`: Internal server error

## Changes

- **2026-10-16** `6c829543a93e`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/sentdm/apis/sent-dm-api-v3/changes/v3/messages/:id/cancel/post.md)

---

[API](https://skmtc.dev/sentdm/apis/sent-dm-api-v3.md) - [All operations](https://skmtc.dev/sentdm/apis/sent-dm-api-v3/llms.txt) - [OpenAPI document](https://skmtc.dev/sentdm/apis/sent-dm-api-v3/revisions/bdcca958cac7?raw)
