# Send a campaign to the people you list

`POST /v1/campaigns/{id}/trigger`

Requires an API key with the `campaigns: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 draft now to the contacts `recipients` names, by `contactId`, `externalId` or `email`, instead of its saved audience; it needs none. Each person may carry their own `data`, read in the content as `{{ data.<key> }}` over the shared `data`: "the item in your basket is now cheaper", to each their item. Up to 5,000 recipients, the request up to 10 MB. A recipient that names no contact is not sent and comes back in `notFound`; one contact listed twice is refused with 400. Each message is checked as any campaign send is (consent, suppression, topic, limits), and a message whose content reads a value its recipient lacks is skipped with `missing_merge_tag`. Data is kept on a message only until it is sent. A campaign whose content reads `data` can go out only this way. A campaign is sent at most once. An `Idempotency-Key` header is required: a retry with the same key and request returns the first answer, `notFound` included, with `Idempotency-Replay: true`; the same key with a different request is 409 `IDEMPOTENCY_KEY_REUSED`, and any other trigger of a campaign already sent is 409 `CONFLICT`: to send again, duplicate it. Agent keys are refused with 403, and a campaign an agent wrote or edited answers 409 `AGENT_CONFIRMATION_REQUIRED` until a person confirms it in the dashboard.

## Path parameters

- `id` (string, required, uuid)

## Header parameters

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

## Request body

- `data` (object, optional) — Values every recipient shares.
- `recipients` (object[], required, at least 1 item, at most 5000 items) — The people to send to, each named by one identifier.
  - One of: **Recipient by contact ID**
    - `contactId` (string, required, uuid) — Kirora's id for the contact.
    - `data` (object, optional) — This person's values; each key overrides the trigger's shared `data`.
  - One of: **Recipient by external ID**
    - `externalId` (string, required, 1–255 characters) — Your own id for the contact, as set with identify.
    - `data` (object, optional) — This person's values; each key overrides the trigger's shared `data`.
  - One of: **Recipient by email**
    - `email` (string, required) — The contact's email address, matched trimmed and without case.
    - `data` (object, optional) — This person's values; each key overrides the trigger's shared `data`.

## Returns

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

- `campaign` (Campaign, required)
  - `id` (string, required, uuid)
  - `name` (string, required)
  - `channels` (string[], required) — The channels it sends on.
  - `groupId` (string | null, required, uuid) — The group it goes to, if any.
  - `segmentId` (string | null, required, uuid) — The segment it goes to, if any.
  - `audienceRules` (object | null, required) — Inline rules choosing the audience, if any. At most one audience is set.
    - One of: **Match all**
      - `all` (object[], required, at least 1 item, at most 20 items) — Every rule or nested group must match.
    - One of: **Match any**
      - `any` (object[], required, at least 1 item, at most 20 items) — At least one rule or nested group must match.
  - `topicId` (string | null, required, uuid) — Sends only to contacts the topic reaches; `null` sends to everyone with consent.
  - `messageLimits` (MessageLimitUse, required)
    - `apply` (boolean, required)
    - `retryHours` (integer, optional, min 0, max 48)
  - `utm` (object | null, required) — UTM parameters added to the links in the email.
    - `enabled` (boolean, required)
    - `source` (string, optional, 1–100 characters)
    - `medium` (string, optional, 1–100 characters)
    - `campaign` (string, optional, 1–100 characters)
    - `content` (string, optional, 1–100 characters)
    - `term` (string, optional, 1–100 characters)
  - `content` (CampaignContent, required)
    - `email` (object, optional) — The email fields; any may be missing on a draft.
      - `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.
      - `html` (string, optional, 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, optional, 1–320 characters) — The sender address, on a verified sending domain.
      - `replyTo` (string, optional, email) — Where replies go.
    - `pushProjectId` (string, optional, uuid) — The push app the notification goes out through.
    - `push` (object, optional) — The notification content.
      - `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.
        - `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.
        - `android` (object, optional) — Replaces shared fields on Android.
        - `web` (object, optional) — Replaces shared fields on web.
    - `templateSnapshot` (object, optional) — The email design, copied into the campaign so later template edits do not change it.
      - `schemaVersion` (integer, required, min > 0, max 1000)
      - `blocks` (EmailBlock[], required, at least 1 item, at most 200 items) — At most 200 blocks across the entire tree and five levels including the root. Footer blocks must remain at root. Known child fields are validated recursively.
        - `id` (string, required, 1–128 characters) — Identifies the block within the template; unique among its blocks.
        - `type` (string, required, 1–64 characters) — The block type: `header`, `heading`, `text`, `image`, `button`, `divider`, `spacer`, `columns`, `columns3`, `imagetext`, `imagegroup`, `boxed`, `quote`, `list`, `product`, `video`, `social`, `menu`, `html`, `section`, `footer`, `repeat`, `shared`. Each type reads its own fields next to `type`; an unknown type is kept but not validated.
        - `style` (EmailBlockStyle, optional) — Visual overrides for this block, such as colours, font and spacing.
        - `visibleIf` (object, optional) — Show the block only to contacts these segment rules match, checked as each message is rendered. Omit to show it to everyone. Not allowed on the footer.
        - `showOn` (string, optional, one of `mobile`, `desktop`) — Show the block only on phones (screens up to 620px) or only on wider screens. Clients that ignore screen size show the desktop version, and so does plain text. Omit to show it everywhere. Not allowed on the footer.
        - `columns` (object[][], optional, at most 3 items) — Nested child block arrays: exactly two for columns, three for columns3. Each child follows this block contract. Omit to retain legacy text columns.
        - `blocks` (object[], optional, at most 200 items) — A section's child blocks, drawn inside its box; or a repeat block's, written out once per item of its `list`, where `{{ item.<field> }}` reads that item. Repeats do not nest.
      - `theme` (EmailTheme, required)
        - `font` (string, optional, up to 200 characters) — Body font stack, such as `Inter, sans-serif`.
        - `hfont` (string, optional, up to 200 characters) — Heading font stack.
        - `bg` (string, optional) — Background of the email card, as hex.
        - `canvas` (string, optional) — Background behind the card, as hex.
        - `text` (string, optional) — Default text colour, as hex.
        - `accent` (string, optional) — Brand colour for buttons and highlights, as hex.
        - `link` (string, optional) — Link colour, as hex.
        - `width` (string, optional, one of `560px`, `600px`, `680px`) — Width of the email card on wide screens.
        - `radius` (string, optional, one of `0px`, `6px`, `12px`, `16px`, `24px`, `999px`) — Corner rounding of the email's card.
        - `btnRadius` (string, optional, one of `0px`, `6px`, `12px`, `16px`, `24px`, `999px`) — Button corners; `radius` rounds the email's card and, when this is unset, buttons.
        - `btn` (string, optional, one of `solid`, `outline`) — Button style.
      - `plainTextOverride` (string | null, optional, up to 512000 characters) — Plain text sent instead of the version generated from the blocks.
  - `status` (string, required, one of `draft`, `scheduled`, `sending`, `sent`, `cancelled`) — Where the campaign is in its lifecycle.
  - `sendAt` (string | null, required, date-time) — When it is scheduled to send, if at all.
  - `localSendAt` (string | null, required) — Set for a local-time send: the wall-clock time each contact receives it at.
  - `archivedAt` (string | null, required, date-time) — When it was archived; archived campaigns are left out of the list by default.
  - `revision` (integer, required, min > 0, max 2147483647) — Raised by every edit. Send it back as `expectedRevision` on the update so a change made since you read it is refused instead of overwritten.
  - `createdAt` (string, required, date-time)
  - `updatedAt` (string, required, date-time)
- `notFound` (object[], required) — Recipients that name no contact in the workspace, as listed; not sent.
  - One of: **Contact ID**
    - `contactId` (string, required, uuid) — Kirora's id for the contact.
  - One of: **External ID**
    - `externalId` (string, required, 1–255 characters) — Your own id for the contact, as set with identify.
  - One of: **Email**
    - `email` (string, required, email, up to 320 characters) — The contact's email address, matched trimmed and without case.

## Example: Each person with their own item

```bash
curl -X POST https://api.kirora.io/v1/campaigns/018f2a4e-6b1c-7d3e-9f40-37386ae05a6b/trigger \
  -H "Authorization: Bearer $KIRORA_API_KEY" \
  -H "Idempotency-Key: price-drop-2030-03-14" \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "store": "Juniper Kitchen"
    },
    "recipients": [
      {
        "externalId": "customer-4821",
        "data": {
          "item": "Cast iron skillet",
          "price": "$39.00"
        }
      },
      {
        "email": "sam@example.com",
        "data": {
          "item": "Chef knife",
          "price": "$54.00"
        }
      },
      {
        "externalId": "customer-9999",
        "data": {
          "item": "Dutch oven",
          "price": "$89.00"
        }
      }
    ]
  }'
```

Response `202`:

```json
{
  "data": {
    "campaign": {
      "id": "0192f3d0-4c5d-7e6f-8a90-1b2c3d4e5f60",
      "name": "Price drop on saved items",
      "channels": [
        "email"
      ],
      "status": "sent",
      "groupId": null,
      "segmentId": null,
      "audienceRules": null,
      "topicId": null,
      "content": {
        "email": {
          "subject": "Now cheaper: {{ data.item }}",
          "from": "Juniper Kitchen <hello@juniperkitchen.com>"
        },
        "templateSnapshot": {
          "schemaVersion": 1,
          "blocks": [
            {
              "id": "heading",
              "type": "heading",
              "text": "Now cheaper: {{ data.item }}"
            },
            {
              "id": "body",
              "type": "text",
              "text": "It is {{ data.price }} today at {{ data.store }}."
            },
            {
              "id": "footer",
              "type": "footer",
              "text": "1 Example Street, London",
              "locked": true
            }
          ],
          "theme": {}
        }
      },
      "messageLimits": {
        "apply": false,
        "retryHours": 0
      },
      "utm": null,
      "sendAt": "2030-03-14T09:30:00.000Z",
      "localSendAt": null,
      "archivedAt": null,
      "revision": 3,
      "createdAt": "2030-03-14T09:00:00.000Z",
      "updatedAt": "2030-03-14T09:30:00.000Z"
    },
    "notFound": [
      {
        "externalId": "customer-9999"
      }
    ]
  }
}
```
