# Cancel a scheduled transactional message

Source: https://kirora.io/docs/api/reference/cancelScheduledTransactionalMessage

`POST /v1/transactional/messages/{id}/cancel`

Requires an API key with the `transactional:send` scope. [About keys and scopes](https://kirora.io/docs/api/reference/authentication)

Works with integration and partner keys, not agent keys.

Safe to retry: a repeat returns the current state, or 409 once it has moved on from it. [About idempotency](https://kirora.io/docs/api/reference/idempotency)

Stops a message sent with `sendAt` before it goes: it becomes `skipped` with `skip.reason` `cancelled`, its email or push goes back to the plan allowance, and `message.skipped` is published. Cancelling an email also cancels its `cc` and `bcc` copies. Cancelling it again answers 200 with the message as it is. Once a worker has started sending it, or it went at once, the response is 409 `CONFLICT`. Agent keys are refused with 403.

## Path parameters

- `id` (string, required, uuid) — The message's id.

## Header parameters

- `Kirora-Workspace` (string, optional, pattern `^[A-Za-z0-9._:-]{1,128}$`) — Partner keys only: the child workspace to act in, by the external id the partner gave it. Without it, a partner key acts in its own workspace. Any other key that sends it is refused with 403, and an unknown workspace is 404.
- `Idempotency-Key` (string, optional, 8–255 characters) — Makes a retry safe: a repeat with the same key and body within 24 hours gets the first response again, with `Idempotency-Replay: true`. The same key with another request is 409 `IDEMPOTENCY_KEY_REUSED`; while the first is still running, 409 `IDEMPOTENCY_KEY_IN_USE`; after an `unsafe` first request failed or never returned, 409 `IDEMPOTENCY_KEY_OUTCOME_UNKNOWN`. Use a new key, such as a UUID, for each new request.

## Returns

Returns `200 OK` with an `OutboundMessage` in `data`. It can also return `409 Conflict`; see the statuses below. Otherwise returns an error; [Errors](https://kirora.io/docs/api/reference/errors) lists every code and which to retry.

- `id` (string, required, uuid)
- `messageType` (string, required, one of `marketing`, `transactional`) — `marketing` (from `campaign` or `journey`) or `transactional` (neither).
- `channel` (string, required, one of `email`, `push`)
- `status` (string, required, one of `queued`, `sending`, `skipped`, `sent`, `failed`, `delivered`, `bounced`, `complained`)
- `campaign` (object | null, required) — The campaign it came from; null for any other message.
  - `id` (string, required, uuid)
  - `name` (string, required) — The campaign's current name.
- `journey` (object | null, required) — The journey it came from, deleted or not; null for any other message.
  - `id` (string | null, required, uuid) — Null once the journey is deleted.
  - `name` (string | null, required) — The journey's current name; null once it is deleted.
  - `deleted` (boolean, required) — The journey was deleted after sending this. Its messages and their counts stay; nothing names the journey any more.
  - `enrollmentId` (string, required, uuid) — The enrollment that sent it.
  - `stepId` (string, required) — The id of the step that sent it.
- `template` (object | null, required) — The stored template and published version it was sent from; null for inline content.
  - `id` (string, required, uuid)
  - `version` (integer, required, min > 0)
- `copy` (object | null, required) — Set on a `cc` or `bcc` copy; null on the message itself.
  - `kind` (string, required, one of `cc`, `bcc`)
  - `of` (string, required, uuid) — The message it is a copy of.
- `contact` (object | null, required) — Who it was for; null when the contact was deleted, or the message passed the retention period: it no longer names anyone.
  - `id` (string, required, uuid)
  - `externalId` (string | null, required)
  - `email` (string | null, required) — The contact's email now, which may differ from `recipientEmail`.
  - `firstName` (string | null, required) — The contact's first name now.
  - `lastName` (string | null, required) — The contact's last name now.
- `recipientEmail` (string | null, required) — The email address the message went to, frozen at the first attempt; null until then, and for push.
- `simulated` (boolean, required) — Sent to a reserved test address (`example.com` and the like): it passed every check a real send does but was never handed to the provider. Any outcome was simulated from the address, as `bounce@example.com` simulates a bounce.
- `correlationId` (string | null, required) — The reference a transactional send carried; null otherwise.
- `sendAt` (string | null, required, date-time) — When a timed transactional send is due; null for one sent at once.
- `sentAt` (string | null, required, date-time) — When it was handed to the provider.
- `deliveredAt` (string | null, required, date-time) — When the provider confirmed delivery.
- `openedAt` (string | null, required, date-time) — First open, when tracked: a person's first open or click, or a machine's open while only a machine has opened it.
- `openedByMachine` (boolean, required) — Only a machine has opened it so far: a mail app fetched its images with no reader (Apple Mail Privacy Protection, Gmail's prefetch on delivery) or a mail security scanner checked it. A click or a later open by a person clears it.
- `clickedAt` (string | null, required, date-time) — A person's first click, when tracked.
- `clickedByMachine` (boolean, required) — A machine followed one of its links: a mail security scanner checking it on delivery, a link preview. Such a click sets no `clickedAt` and sends no webhook.
- `complainedAt` (string | null, required, date-time) — When the recipient marked it as spam (the first complaint); null for a simulated one.
- `bounce` (object | null, required) — The hard bounce: set when `status` is `bounced`; null otherwise. A soft bounce is a `failure` with reason `transient_bounce`.
  - `occurredAt` (string, required, date-time) — When it happened.
  - `type` (string, required, one of `mailbox_not_found`, `mailbox_full`, `suppressed`, `general`) — Why: `mailbox_not_found`, `mailbox_full`, `suppressed` (the provider refuses the address), or `general` for any other reason.
  - `detail` (string | null, required) — The receiving server's reply, such as `smtp; 550 5.1.1 user unknown`; null for a simulated bounce.
- `failure` (object | null, required) — Set when `status` is `failed`; null otherwise.
  - `occurredAt` (string, required, date-time) — When it happened.
  - `reason` (string, required) — Why sending broke, such as `provider_rejected` (the provider refused the content) or `transient_bounce`. `not_sent`: a problem on Kirora's side stopped it; nothing in your content, audience or setup caused it, and Kirora is alerted. In rare cases the provider may have delivered it before the problem.
- `skip` (object | null, required) — Set when `status` is `skipped`; null otherwise.
  - `occurredAt` (string, required, date-time) — When it happened.
  - `reason` (string, required) — Which guard stopped it before sending, such as `suppressed`, `consent_missing` or `cancelled`; `fallback_not_needed`: a transactional `fallbackOrder` fallback whose first channel arrived; `plan_limit_reached`: a fallback the plan had no room for when it was due; `sending_review_required`: email from a sending domain Kirora has not approved yet.
  - `suppressionReason` (string | null, required, one of `manual`, `imported`, `unsubscribed`, `bounced`, `complained`, `complained_transactional`) — With `suppressed`: why the address is blocked: `unsubscribed`, `bounced`, `complained`, `complained_transactional`, `manual` or `imported`. Null otherwise.
- `createdAt` (string, required, date-time)

Statuses:

- `200 OK`: The message, cancelled.
- `409 Conflict`: The request conflicts with current state — a duplicate unique value, or a resource whose status does not allow this operation.

Headers on `200 OK`:

- `Idempotency-Replay` (string, optional, one of `true`) — `true` when this is the stored response to an earlier request with the same key.

## Example

```bash
curl -X POST https://api.kirora.io/v1/transactional/messages/018f2a4e-6b1c-7d3e-9f40-37386ae05a6b/cancel \
  -H "Authorization: Bearer $KIRORA_API_KEY"
```

Response `200`:

```json
{
  "data": {
    "copy": null,
    "id": "0192f3b0-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
    "messageType": "transactional",
    "channel": "email",
    "status": "skipped",
    "campaign": null,
    "journey": null,
    "template": null,
    "contact": {
      "id": "0192f3a1-7c2e-7d4b-9a10-3e5f6a7b8c9d",
      "externalId": "customer-4821",
      "email": "amaka@example.com",
      "firstName": "Amaka",
      "lastName": "Obi"
    },
    "recipientEmail": null,
    "simulated": false,
    "correlationId": "class-3315",
    "sendAt": "2026-10-01T10:00:00.000Z",
    "sentAt": null,
    "deliveredAt": null,
    "openedAt": null,
    "openedByMachine": false,
    "clickedAt": null,
    "clickedByMachine": false,
    "complainedAt": null,
    "bounce": null,
    "failure": null,
    "skip": {
      "occurredAt": "2026-10-01T09:45:00.000Z",
      "reason": "cancelled",
      "suppressionReason": null
    },
    "createdAt": "2026-10-01T09:30:00.000Z"
  }
}
```
