# Send a transactional message

`POST /v1/transactional/messages`

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

Takes an `Idempotency-Key` header: a retry with the same key returns the first result instead of acting twice. Derive the key from what you are sending, and reuse it on every retry. [About idempotency](/docs/api/idempotency)

Sends a one-off message to one recipient — a receipt, a password reset, a confirmation — bypassing campaigns. It ignores marketing preferences (it is the critical tier) but respects suppression. Agent keys are refused with 403.

**Channels.** Give `email`, `push`, or both. With both, `deliver: all` (the default) sends each one the contact can receive, and `deliver: first` sends push when the contact has a reachable device and email otherwise. A channel the contact cannot receive is passed over; only when none is left does the send fail, with that channel's reason. The response lists one message per channel sent.

**Email.** Inline (`subject`, `from`, `html`, optional `text`, `previewText`, `replyTo`), or a stored template: `templateId` (its id or `alias`) with `data`, readable as `{{ data.<key> }}`. The template's published version is rendered when the request is accepted and supplies the envelope; each envelope field given overrides it. The result needs a `subject` and a `from` (409 `TEMPLATE_ENVELOPE_INCOMPLETE`), and the `from` domain must be verified. A template never published is 409 `TEMPLATE_NOT_PUBLISHED`. Email is tracked only when `tracking` asks, and never for a contact in the EEA or the UK; `threading` continues a conversation.

**Recipient.** `to` names a `contactId`, an `externalId` or an `email`. With `externalId` or `email`, `firstName`, `lastName` and `attributes` create the contact or update it, as `POST /v1/contacts/identify` does; consent is never changed. A bare `email` that matches no contact creates one for transactional mail only.

**Retries.** An `Idempotency-Key` header is required. A retry with the same key and request returns the first result, with `Idempotency-Replay: true`, and sends nothing again; the same key with a different request is 409 `IDEMPOTENCY_KEY_REUSED`. Set `correlationId` to your own reference: it is echoed on each message's webhooks and its read. `sendAt` times the send, email and push alike.

## Header parameters

- `Idempotency-Key` (string, required, 8–255 characters)

## Request body

- `to` (object, required) — Who receives it.
  - One of: **By contact id**
    - `contactId` (string, required, uuid) — An existing contact.
  - One of: **By external id**
    - `externalId` (string, required, 1–255 characters) — Your own id for the contact. Alone it must match an existing contact; with `email` or profile fields the contact is created or updated, as identify does.
    - `email` (string, optional, email, up to 320 characters) — Sets or updates the contact email.
    - `firstName` (string | null, optional, 1–100 characters)
    - `lastName` (string | null, optional, 1–100 characters)
    - `attributes` (object, optional) — Custom attributes, merged key by key; `null` removes one. At most 50 keys and 16 KB.
  - One of: **By email**
    - `email` (string, required, email, up to 320 characters) — The recipient's address. With profile fields the contact is created or updated, as identify does; alone, an email send creates a transactional-only contact when none has it, and a push-only send needs an existing contact.
    - `firstName` (string | null, optional, 1–100 characters)
    - `lastName` (string | null, optional, 1–100 characters)
    - `attributes` (object, optional) — Custom attributes, merged key by key; `null` removes one. At most 50 keys and 16 KB.
- `sensitive` (boolean, optional) — Set when the message carries a secret (a reset link, a one-time code): its content is never kept, whatever `retainContent` and the workspace's retention say.
- `correlationId` (string, optional, 1–255 characters) — Your own reference for this send, echoed on its webhooks and its read.
- `retainContent` (boolean, optional) — `false` discards what was sent as soon as it is sent.
- `email` (object, optional) — Inline content (`subject`, `from`, `html`), or a stored `templateId`.
  - One of: **Stored template**
    - `templateId` (string, required) — The template's id, or its `alias` (such as `password-reset`).
    - `data` (object, optional) — Values for the template's `{{ data.* }}` merge tags.
    - `subject` (string, optional, 1–998 characters) — The subject line. Merge tags are allowed.
    - `previewText` (string, optional, 1–255 characters) — Shown by inboxes after the subject. Merge tags are allowed; keep it near 100 characters.
    - `from` (string, optional, 1–320 characters) — The sender address, on a verified sending domain.
    - `replyTo` (string, optional, email) — Where replies go.
  - One of: **Inline content**
    - `subject` (string, required, 1–998 characters) — The subject line. Merge tags are allowed.
    - `previewText` (string, optional, 1–255 characters) — Shown by inboxes after the subject. Merge tags are allowed; keep it near 100 characters.
    - `html` (string, required, 1–512000 characters) — The HTML body. Rendered from the design when one is set.
    - `text` (string, optional, 1–512000 characters) — The plain-text alternative.
    - `from` (string, required, 1–320 characters) — The sender address, on a verified sending domain.
    - `replyTo` (string, optional, email) — Where replies go.
- `push` (object, optional) — A push notification to the contact's devices. Give `push`, `email`, or both.
  - `pushProjectId` (string, required, uuid) — Reaches the contact's devices on every app of the project.
  - `content` (object, required)
    - `title` (string, required, 1–100 characters) — The notification title (up to 100 characters).
    - `body` (string, required, 1–500 characters) — The notification text (up to 500 characters).
    - `actionUrl` (string, optional, uri, up to 2048 characters) — The HTTPS page opened when the notification is tapped.
    - `imageUrl` (string, optional, uri, up to 2048 characters) — A large picture: web and Android show it; iOS needs the app's service extension.
    - `iconUrl` (string, optional, uri, up to 2048 characters) — Web only; iOS and Android show the app's own notification icon.
    - `data` (object, optional) — Up to 10 string key-value pairs passed to the app untouched, for deep links and in-app routing.
    - `buttons` (object[], optional, at least 1 item, at most 2 items) — Up to 2 action buttons.
      - `title` (string, required, 1–30 characters) — The button label.
      - `url` (string, required, uri, up to 2048 characters) — The HTTPS page the button opens.
    - `collapseKey` (string, optional) — A newer notification with the same key replaces the older one on the device.
    - `sound` (string, optional, 1–64 characters) — The name of a sound the app ships; the device's default if unset.
    - `badge` (integer, optional, min 0, max 99999) — The number shown on the iOS app icon.
    - `ttlSeconds` (integer, optional, min 0, max 2419200) — Seconds a provider keeps trying an offline device (up to 28 days); defaults to 86400 (one day).
    - `urgency` (string, optional, one of `high`, `normal`) — `high` wakes the device now; `normal` lets it wait for a good moment to save battery. Unset, a transactional push is `high` and any other `normal`.
    - `overrides` (object, optional) — Fields that differ on one platform, each replacing the shared one there (a whole `data` map, not merged key by key). On iOS and Android `actionUrl` may be an app link such as `myapp://orders`; the shared one and the web one must be HTTPS.
      - `ios` (object, optional) — Replaces shared fields on iOS.
        - `title` (string, optional, 1–100 characters) — The notification title (up to 100 characters).
        - `body` (string, optional, 1–500 characters) — The notification text (up to 500 characters).
        - `actionUrl` (string, optional, up to 2048 characters)
        - `imageUrl` (string, optional, uri, up to 2048 characters) — A large picture: web and Android show it; iOS needs the app's service extension.
        - `iconUrl` (string, optional, uri, up to 2048 characters) — Web only; iOS and Android show the app's own notification icon.
        - `data` (object, optional) — Up to 10 string key-value pairs passed to the app untouched, for deep links and in-app routing.
        - `buttons` (object[], optional, at least 1 item, at most 2 items) — Up to 2 action buttons.
        - `collapseKey` (string, optional) — A newer notification with the same key replaces the older one on the device.
        - `sound` (string, optional, 1–64 characters) — The name of a sound the app ships; the device's default if unset.
        - `badge` (integer, optional, min 0, max 99999) — The number shown on the iOS app icon.
        - `ttlSeconds` (integer, optional, min 0, max 2419200) — Seconds a provider keeps trying an offline device (up to 28 days); defaults to 86400 (one day).
        - `urgency` (string, optional, one of `high`, `normal`) — `high` wakes the device now; `normal` lets it wait for a good moment to save battery. Unset, a transactional push is `high` and any other `normal`.
        - `interruptionLevel` (string, optional, one of `passive`, `active`, `time-sensitive`) — How iOS presents it: `passive` arrives silently, `time-sensitive` breaks through Focus and needs the app's Time Sensitive Notifications capability.
        - `threadId` (string, optional) — Notifications with the same thread are grouped together in Notification Center.
      - `android` (object, optional) — Replaces shared fields on Android.
        - `title` (string, optional, 1–100 characters) — The notification title (up to 100 characters).
        - `body` (string, optional, 1–500 characters) — The notification text (up to 500 characters).
        - `actionUrl` (string, optional, up to 2048 characters)
        - `imageUrl` (string, optional, uri, up to 2048 characters) — A large picture: web and Android show it; iOS needs the app's service extension.
        - `iconUrl` (string, optional, uri, up to 2048 characters) — Web only; iOS and Android show the app's own notification icon.
        - `data` (object, optional) — Up to 10 string key-value pairs passed to the app untouched, for deep links and in-app routing.
        - `buttons` (object[], optional, at least 1 item, at most 2 items) — Up to 2 action buttons.
        - `collapseKey` (string, optional) — A newer notification with the same key replaces the older one on the device.
        - `sound` (string, optional, 1–64 characters) — The name of a sound the app ships; the device's default if unset.
        - `badge` (integer, optional, min 0, max 99999) — The number shown on the iOS app icon.
        - `ttlSeconds` (integer, optional, min 0, max 2419200) — Seconds a provider keeps trying an offline device (up to 28 days); defaults to 86400 (one day).
        - `urgency` (string, optional, one of `high`, `normal`) — `high` wakes the device now; `normal` lets it wait for a good moment to save battery. Unset, a transactional push is `high` and any other `normal`.
        - `channelId` (string, optional) — The Android notification channel to post in, which the app creates; it sets the sound and importance a person can tune, e.g. one for offers and one for orders.
      - `web` (object, optional) — Replaces shared fields on web.
        - `title` (string, optional, 1–100 characters) — The notification title (up to 100 characters).
        - `body` (string, optional, 1–500 characters) — The notification text (up to 500 characters).
        - `actionUrl` (string, optional, uri, up to 2048 characters) — The HTTPS page opened when the notification is tapped.
        - `imageUrl` (string, optional, uri, up to 2048 characters) — A large picture: web and Android show it; iOS needs the app's service extension.
        - `iconUrl` (string, optional, uri, up to 2048 characters) — Web only; iOS and Android show the app's own notification icon.
        - `data` (object, optional) — Up to 10 string key-value pairs passed to the app untouched, for deep links and in-app routing.
        - `buttons` (object[], optional, at least 1 item, at most 2 items) — Up to 2 action buttons.
        - `collapseKey` (string, optional) — A newer notification with the same key replaces the older one on the device.
        - `sound` (string, optional, 1–64 characters) — The name of a sound the app ships; the device's default if unset.
        - `badge` (integer, optional, min 0, max 99999) — The number shown on the iOS app icon.
        - `ttlSeconds` (integer, optional, min 0, max 2419200) — Seconds a provider keeps trying an offline device (up to 28 days); defaults to 86400 (one day).
        - `urgency` (string, optional, one of `high`, `normal`) — `high` wakes the device now; `normal` lets it wait for a good moment to save battery. Unset, a transactional push is `high` and any other `normal`.
- `attachments` (EmailAttachmentInput[], optional, at least 1 item, at most 10 items) — Files the email carries (not push): up to 10, 10 MB in all.
  - `filename` (string, required, 1–255 characters) — Its extension sets the type it is sent as.
  - `content` (string, required, base64) — The file, base64-encoded.
  - `contentId` (string, optional) — Shows an image inside the body instead of attaching it: the HTML refers to it as `<img src="cid:<contentId>">`.
- `threading` (EmailThreadingInput, optional) — Places the email in an existing conversation, as the `In-Reply-To` and `References` headers.
  - `inReplyTo` (string, required) — The `Message-ID` of the email being replied to, angle brackets included.
  - `references` (string[], optional, at least 1 item, at most 20 items) — The `Message-ID`s earlier in the conversation, oldest first (up to 20).
- `tracking` (TransactionalEmailTrackingInput, optional) — Open and click tracking for this email; off unless asked for. Contacts whose `country` is in the EEA or the UK are never tracked.
  - `opens` (boolean, optional, default `false`) — Track opens with a pixel.
  - `clicks` (boolean, optional, default `false`) — Rewrite links to track clicks.
- `sendAt` (string, optional, date-time) — When to send, up to 90 days ahead, with its UTC offset. Suppression and push reachability are checked when the request is accepted and again when it goes; `deliver: first` picks its channel when accepted.
- `deliver` (string, optional, one of `all`, `first`, default `"all"`) — With both `push` and `email`: `all` sends each the contact can receive; `first` sends push when the contact has a reachable device, else email.

## Returns

Returns `202 Accepted` with a `TransactionalSendResult` in `data`. Otherwise returns an error; [Errors](/docs/api/errors) lists every code and which to retry.

- `messages` (TransactionalMessage[], required)
  - `id` (string, required, uuid)
  - `channel` (string, required, one of `email`, `push`)
  - `status` (string, required, one of `queued`, `sending`, `skipped`, `sent`, `failed`, `delivered`, `bounced`, `complained`)
  - `correlationId` (string | null, required) — The `correlationId` given when it was sent, if any.
  - `createdAt` (string, required, date-time)
  - `sendAt` (string | null, required, date-time) — When a timed email goes out; null when it went at once.

## Example: Email from a stored template

```bash
curl -X POST https://api.kirora.io/v1/transactional/messages \
  -H "Authorization: Bearer $KIRORA_API_KEY" \
  -H "Idempotency-Key: order-1001-receipt" \
  -H "Content-Type: application/json" \
  -d '{
    "to": {
      "externalId": "customer-4821"
    },
    "correlationId": "order-1001",
    "email": {
      "templateId": "order-receipt",
      "data": {
        "orderNumber": "1001",
        "total": "$45.00",
        "items": 3
      }
    }
  }'
```

Response `202`:

```json
{
  "data": {
    "messages": [
      {
        "id": "0192f3b0-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
        "channel": "email",
        "status": "queued",
        "correlationId": "order-1001",
        "createdAt": "2030-03-14T09:30:00.000Z",
        "sendAt": null
      }
    ]
  }
}
```

## Example: Email written in the request

```bash
curl -X POST https://api.kirora.io/v1/transactional/messages \
  -H "Authorization: Bearer $KIRORA_API_KEY" \
  -H "Idempotency-Key: signin-code-8f3a2c" \
  -H "Content-Type: application/json" \
  -d '{
    "to": {
      "email": "amaka@example.com"
    },
    "sensitive": true,
    "email": {
      "subject": "Your sign-in code",
      "from": "Juniper Kitchen <security@juniperkitchen.com>",
      "html": "<p>Your code is <strong>482913</strong>. It expires in 10 minutes.</p>",
      "text": "Your code is 482913. It expires in 10 minutes."
    }
  }'
```

Response `202`:

```json
{
  "data": {
    "messages": [
      {
        "id": "0192f3b0-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
        "channel": "email",
        "status": "queued",
        "correlationId": null,
        "createdAt": "2030-03-14T09:30:00.000Z",
        "sendAt": null
      }
    ]
  }
}
```

## Example: Push and email together

```bash
curl -X POST https://api.kirora.io/v1/transactional/messages \
  -H "Authorization: Bearer $KIRORA_API_KEY" \
  -H "Idempotency-Key: delivery-7731-shipped" \
  -H "Content-Type: application/json" \
  -d '{
    "to": {
      "contactId": "0192f3a1-7c2e-7d4b-9a10-3e5f6a7b8c9d"
    },
    "correlationId": "delivery-7731",
    "push": {
      "pushProjectId": "0192f3a1-91d4-7b2a-8c3e-4d5e6f7a8b9c",
      "content": {
        "title": "Your order is on its way",
        "body": "Arriving Thursday."
      }
    },
    "email": {
      "templateId": "order-shipped",
      "data": {
        "orderNumber": "7731",
        "eta": "Thursday"
      }
    },
    "deliver": "all"
  }'
```

Response `202`:

```json
{
  "data": {
    "messages": [
      {
        "id": "0192f3b0-2b3c-7d4e-9f50-6b7c8d9e0f1a",
        "channel": "push",
        "status": "queued",
        "correlationId": "delivery-7731",
        "createdAt": "2030-03-14T09:30:00.000Z",
        "sendAt": null
      },
      {
        "id": "0192f3b0-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
        "channel": "email",
        "status": "queued",
        "correlationId": "delivery-7731",
        "createdAt": "2030-03-14T09:30:00.000Z",
        "sendAt": null
      }
    ]
  }
}
```

## Example: Sent later

```bash
curl -X POST https://api.kirora.io/v1/transactional/messages \
  -H "Authorization: Bearer $KIRORA_API_KEY" \
  -H "Idempotency-Key: class-reminder-3315" \
  -H "Content-Type: application/json" \
  -d '{
    "to": {
      "contactId": "0192f3a1-7c2e-7d4b-9a10-3e5f6a7b8c9d"
    },
    "email": {
      "subject": "Your class starts in an hour",
      "from": "Juniper Kitchen <hello@juniperkitchen.com>",
      "html": "<p>See you at 10:00 in the Shoreditch studio.</p>"
    },
    "sendAt": "2030-03-15T09:00:00+01:00"
  }'
```

Response `202`:

```json
{
  "data": {
    "messages": [
      {
        "id": "0192f3b0-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
        "channel": "email",
        "status": "queued",
        "correlationId": null,
        "createdAt": "2030-03-14T09:30:00.000Z",
        "sendAt": "2030-03-15T08:00:00.000Z"
      }
    ]
  }
}
```
