# Send up to 100 transactional messages

`POST /v1/transactional/messages/batch`

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

Each entry takes its own `idempotencyKey`, so retrying the whole batch never acts twice on an entry that already went through. [About idempotency](/docs/api/idempotency)

Each entry is a single send (see `POST /v1/transactional/messages`) with its own `idempotencyKey`, answered on its own: `results` keeps the order of `sends`, each with its `messages` and `replayed`, or the `error` a single send would have returned. One refused entry does not stop the rest. The call counts against the rate limit as one request per entry.

## Request body

- `sends` (object[], required, at least 1 item, at most 100 items)
  - `idempotencyKey` (string, required, 8–255 characters) — As the `Idempotency-Key` header of a single send: one per message you mean.
  - `send` (SendTransactionalInput, required)
    - `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.
        - `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.
    - `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 `TransactionalBatchResult` in `data`. Otherwise returns an error; [Errors](/docs/api/errors) lists every code and which to retry.

- `results` (object[], required)
  - One of: **Accepted**
    - `idempotencyKey` (string, required)
    - `replayed` (boolean, required) — True when this key had already been accepted; nothing new was sent.
    - `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.
  - One of: **Rejected**
    - `idempotencyKey` (string, required)
    - `error` (object, required)
      - `code` (string, required, one of `BAD_REQUEST`, `ALREADY_EXISTS`, `CONFLICT`, `IN_USE`, `FORBIDDEN`, `INTERNAL_ERROR`, `NOT_FOUND`, `RATE_LIMITED`, `REVISION_CONFLICT`, `SERVICE_UNAVAILABLE`, `UNAUTHORIZED`, `VALIDATION_ERROR`, `API_KEY_IP_NOT_ALLOWED`, `AGENT_CONFIRMATION_REQUIRED`, `EMBED_ORIGIN_NOT_ALLOWED`, `WORKSPACE_SUSPENDED`, `WORKSPACE_DELETION_PENDING`, `CONTACT_SEND_IN_PROGRESS`, `EXPORT_UNAVAILABLE`, `NOTHING_TO_EXPORT`, `EVENT_NAME_LIMIT_REACHED`, `PLAN_LIMIT_REACHED`, `PLAN_FEATURE_UNAVAILABLE`, `CAMPAIGN_AUDIENCE_CONFLICT`, `EMPTY_AUDIENCE`, `INVALID_CAMPAIGN_CONTENT`, `DRAFT_INCOMPLETE`, `INVALID_SCHEDULE`, `UNSUPPORTED_CHANNEL`, `POSTAL_ADDRESS_MISSING`, `CONSENT_MISSING`, `SUPPRESSED`, `SIGNUP_BLOCKED`, `INVALID_SENDER_ADDRESS`, `SENDING_DOMAIN_DMARC_MISSING`, `SENDING_DOMAIN_IDENTITY_CONFLICT`, `SENDING_DOMAIN_UNVERIFIED`, `IDEMPOTENCY_KEY_REUSED`, `CONTENT_NOT_RETAINED`, `RECIPIENT_EMAIL_MISSING`, `TEMPLATE_NOT_PUBLISHED`, `TEMPLATE_ENVELOPE_INCOMPLETE`, `PUSH_APP_NOT_READY`, `PUSH_DESTINATION_MISSING`)
      - `message` (string, required)
      - `details` (object, optional)
      - `requestId` (string, optional)

## Example: Two receipts in one request

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

Response `202`:

```json
{
  "data": {
    "results": [
      {
        "idempotencyKey": "order-1001-receipt",
        "replayed": false,
        "messages": [
          {
            "id": "0192f3b0-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
            "channel": "email",
            "status": "queued",
            "correlationId": "order-1001",
            "createdAt": "2030-03-14T09:30:00.000Z",
            "sendAt": null
          }
        ]
      },
      {
        "idempotencyKey": "order-1002-receipt",
        "replayed": false,
        "messages": [
          {
            "id": "0192f3b0-3c4d-7e5f-8a61-7c8d9e0f1a2b",
            "channel": "email",
            "status": "queued",
            "correlationId": "order-1002",
            "createdAt": "2030-03-14T09:30:00.000Z",
            "sendAt": null
          }
        ]
      }
    ]
  }
}
```

## Example: One send fails, the other goes

```bash
curl -X POST https://api.kirora.io/v1/transactional/messages/batch \
  -H "Authorization: Bearer $KIRORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sends": [
      {
        "idempotencyKey": "order-1003-receipt",
        "send": {
          "to": {
            "externalId": "customer-4821"
          },
          "correlationId": "order-1003",
          "email": {
            "templateId": "order-receipt",
            "data": {
              "orderNumber": "1003",
              "total": "$24.00",
              "items": 2
            }
          }
        }
      },
      {
        "idempotencyKey": "order-1004-receipt",
        "send": {
          "to": {
            "externalId": "customer-0000"
          },
          "correlationId": "order-1004",
          "email": {
            "templateId": "order-receipt",
            "data": {
              "orderNumber": "1004",
              "total": "$7.25",
              "items": 1
            }
          }
        }
      }
    ]
  }'
```

Response `202`:

```json
{
  "data": {
    "results": [
      {
        "idempotencyKey": "order-1003-receipt",
        "replayed": false,
        "messages": [
          {
            "id": "0192f3b0-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
            "channel": "email",
            "status": "queued",
            "correlationId": "order-1003",
            "createdAt": "2030-03-14T09:30:00.000Z",
            "sendAt": null
          }
        ]
      },
      {
        "idempotencyKey": "order-1004-receipt",
        "error": {
          "code": "NOT_FOUND",
          "message": "No contact matches `to`",
          "details": {
            "field": "to"
          }
        }
      }
    ]
  }
}
```
