# Campaigns

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

A campaign is one marketing send to an audience, a list or a segment, at once or at a scheduled time. A campaign sends at most once; a journey is for messages that follow each contact over time. It reaches only contacts with consent on its channel, so check eligibility before sending. See [Campaigns and journeys](https://kirora.io/docs/api/campaigns-and-journeys).

## Endpoints

- [List campaigns](https://kirora.io/docs/api/reference/listCampaigns): `GET /v1/campaigns`
- [Create a campaign](https://kirora.io/docs/api/reference/createCampaign): `POST /v1/campaigns`
- [Retrieve a campaign](https://kirora.io/docs/api/reference/getCampaign): `GET /v1/campaigns/{id}`
- [Update a campaign](https://kirora.io/docs/api/reference/updateCampaign): `PATCH /v1/campaigns/{id}`
- [Delete a draft campaign](https://kirora.io/docs/api/reference/deleteCampaign): `DELETE /v1/campaigns/{id}`
- [Retrieve a campaign's eligibility](https://kirora.io/docs/api/reference/getCampaignEligibility): `GET /v1/campaigns/{id}/eligibility`
- [Preview campaign content](https://kirora.io/docs/api/reference/previewCampaign): `POST /v1/campaigns/{id}/preview`
- [Schedule a campaign](https://kirora.io/docs/api/reference/scheduleCampaign): `POST /v1/campaigns/{id}/schedule`
- [Unschedule a campaign](https://kirora.io/docs/api/reference/unscheduleCampaign): `POST /v1/campaigns/{id}/unschedule`
- [Send a campaign](https://kirora.io/docs/api/reference/sendCampaign): `POST /v1/campaigns/{id}/send`
- [Trigger a campaign](https://kirora.io/docs/api/reference/triggerCampaign): `POST /v1/campaigns/{id}/trigger`
- [Choose an A/B test's winner](https://kirora.io/docs/api/reference/chooseAbTestWinner): `POST /v1/campaigns/{id}/ab-test/winner`
- [Cancel a campaign that is still sending](https://kirora.io/docs/api/reference/cancelCampaign): `POST /v1/campaigns/{id}/cancel`
- [Archive a campaign](https://kirora.io/docs/api/reference/archiveCampaign): `POST /v1/campaigns/{id}/archive`
- [Unarchive a campaign](https://kirora.io/docs/api/reference/unarchiveCampaign): `POST /v1/campaigns/{id}/unarchive`
- [Duplicate a campaign](https://kirora.io/docs/api/reference/duplicateCampaign): `POST /v1/campaigns/{id}/duplicate`
- [Retrieve a campaign's metrics](https://kirora.io/docs/api/reference/getCampaignMetrics): `GET /v1/campaigns/{id}/metrics`
- [Retrieve a campaign's email results by domain, device or country](https://kirora.io/docs/api/reference/getCampaignBreakdown): `GET /v1/campaigns/{id}/breakdown`
- [Retrieve a campaign's metrics over time](https://kirora.io/docs/api/reference/getCampaignSeries): `GET /v1/campaigns/{id}/series`
- [List a campaign's links and their clicks](https://kirora.io/docs/api/reference/listCampaignLinks): `GET /v1/campaigns/{id}/links`

## The Campaign object

```json
{
  "id": "0192f3d3-5e6f-7071-8c83-9e0f1a2b3c4d",
  "name": "October newsletter",
  "channels": [
    "email"
  ],
  "allContacts": false,
  "listIds": [
    "0192f3a2-1b3c-7e5d-8a21-4f6a7b8c9d0e"
  ],
  "segmentId": null,
  "audienceName": null,
  "audienceRules": null,
  "topicId": null,
  "messageLimits": {
    "apply": true,
    "retryFor": {
      "value": 24,
      "unit": "hours"
    }
  },
  "utm": null,
  "content": {
    "email": {
      "subject": "New this month at Acme Shop",
      "previewText": "Autumn arrivals are in",
      "from": "Acme Shop <hello@mail.acme-shop.com>",
      "replyTo": "support@acme-shop.com",
      "templateSnapshot": {
        "schemaVersion": 1,
        "blocks": [
          {
            "id": "title",
            "type": "heading",
            "text": "Autumn arrivals",
            "size": "28px"
          },
          {
            "id": "intro",
            "type": "text",
            "text": "Hi {{ contact.firstName }}, the new range is in."
          },
          {
            "id": "shop",
            "type": "button",
            "text": "Shop now",
            "href": "https://acme-shop.com"
          }
        ],
        "theme": {}
      }
    }
  },
  "abTest": null,
  "status": "draft",
  "schedule": null,
  "sentAt": null,
  "delivery": {
    "rateLimit": null,
    "expiresAfter": null
  },
  "pendingAgentReview": false,
  "sendApprovalRequest": null,
  "exclusions": null,
  "archivedAt": null,
  "tags": [
    "Newsletter"
  ],
  "revision": 1,
  "createdAt": "2026-10-01T09:30:00.000Z",
  "updatedAt": "2026-10-01T09:30:00.000Z"
}
```

- `id` (string, required, uuid)
- `name` (string, required)
- `channels` (string[], required) — The channels it sends on.
- `allContacts` (boolean, required) — Whether it goes to all contacts: everyone its channels reach, with consent and suppressions applied as on any send.
- `listIds` (string[] | null, required) — The lists it goes to, if any: a member of any of them receives it once.
- `segmentId` (string | null, required, uuid) — The segment it goes to, if any.
- `audienceName` (string | null, required) — The names of the lists or the segment it went to, once one of them has been deleted (the list leaves `listIds`, or `segmentId` becomes `null`). Only a sent or cancelled campaign can lose its audience this way.
- `audienceRules` ([object | null](https://kirora.io/docs/api/models#segment-rules), required) — Inline rules choosing the audience, if any. At most one audience is set.
  - Any of: **Match all**
    - `all` (object[], required, at least 1 item, at most 20 items) — Every rule or nested group must match.
      - Any of: **Attribute**
        - `type` ("attribute", required)
        - `field` (string, required, pattern `^attributes\.[\p{L}\p{N}\p{M}_.-]{1,64}$`) — `attributes.<key>`, e.g. `attributes.plan`.
        - `operator` (string, required, one of `equals`, `notEquals`, `contains`, `notContains`, `startsWith`, `exists`, `notExists`, `before`, `after`, `gt`, `gte`, `lt`, `lte`, `in`, `withinLastDays`, `moreThanDaysAgo`, `inNextDays`, `anniversaryInNextDays`, `anniversaryThisMonth`) — How to compare. Text comparisons (`equals`, `notEquals`, `contains`, `notContains`, `startsWith`, `in`) ignore case and accents ("jose" matches "José"). `notEquals` and `notContains` also match contacts without the value. Date operators (`before`, `after`, `withinLastDays`, `moreThanDaysAgo`) compare as dates and times in UTC: a stored ISO-8601 date is the start of that day, `before` means earlier than the given day and `after` later than it, and a value that is not an ISO-8601 date or date and time never matches. `inNextDays` is a date from today to that many days ahead. `anniversaryInNextDays` (0 = today) and `anniversaryThisMonth` ignore the year, so they suit birthdays, and also read a yearless `--MM-DD`; a 29 February counts on 28 February in other years. Days are UTC days; these three are for attributes and dates, not contact fields.
        - `value` (string, optional, up to 255 characters) — What to compare with, always as text: `"pro"`, `"3"`, `"true"`. Equality compares the stored value as text, so a stored 5 matches `"5"`. An ISO date for `before` and `after`; a number for `gt`, `gte`, `lt` and `lte`; a whole number of days (1 to 3650) for `withinLastDays`, `moreThanDaysAgo` and `inNextDays`, and 0 to 365 for `anniversaryInNextDays`. Omit for `in`, `exists`, `notExists` and `anniversaryThisMonth`.
        - `values` (string[], optional, at least 1 item, at most 100 items) — For `in` only: up to 100 values, each as text, any of which matches.
      - Any of: **Contact field**
        - `type` ("contact", required)
        - `field` (string, required, one of `email`, `firstName`, `lastName`, `phone`, `timezone`, `externalId`, `consent.email`, `consent.push`, `country`, `locale`, `rfmGroup`, `rfmRecency`, `rfmFrequency`, `rfmMonetary`, `purchaseCount`, `totalSpent`, `lastPurchasedAt`, `createdAt`) — The contact column to test. Each takes these operators, and is refused with others: `email`: equals, notEquals, contains, startsWith, exists, notExists, in; `firstName`: equals, notEquals, contains, startsWith, exists, notExists, in; `lastName`: equals, notEquals, contains, startsWith, exists, notExists, in; `phone`: equals, notEquals, contains, startsWith, exists, notExists, in; `timezone`: equals, notEquals, contains, startsWith, exists, notExists, in; `externalId`: equals, notEquals, contains, startsWith, exists, notExists, in; `consent.email`: equals; `consent.push`: equals; `country`: equals, notEquals, in, exists, notExists; `locale`: equals, notEquals, startsWith, in, exists, notExists; `rfmGroup`: equals, notEquals, in, exists, notExists; `rfmRecency`: equals, notEquals, in, exists, notExists, gt, gte, lt, lte; `rfmFrequency`: equals, notEquals, in, exists, notExists, gt, gte, lt, lte; `rfmMonetary`: equals, notEquals, in, exists, notExists, gt, gte, lt, lte; `purchaseCount`: equals, gt, gte, lt, lte; `totalSpent`: equals, gt, gte, lt, lte; `lastPurchasedAt`: before, after, withinLastDays, moreThanDaysAgo, exists, notExists; `createdAt`: before, after, withinLastDays, moreThanDaysAgo. `phone` holds +country numbers: write `equals`, `notEquals` and `in` values with their country code. Consent takes `"true"` or `"false"`. Purchases: `purchaseCount`, `totalSpent` (in the main currency) and `lastPurchasedAt` are a customer's totals of the workspace's purchase event (`purchaseEvent` in workspace settings: an order, a paid invoice, a booking), recomputed daily; the contact's stored `totalSpent` is null when the workspace's purchases carry no amount. In comparisons on `purchaseCount` and `totalSpent` a missing count or spend is 0, so someone who never purchased has 0 purchases and 0 spent. RFM: `rfmGroup` and its scores `rfmRecency`, `rfmFrequency` and `rfmMonetary` (monetary value: how much they spend) come from the same daily pass. A score is a whole number from 1 to 5, written as text (`"5"`; `values` of them for `in`), 5 being the most recent, most frequent or highest spending; any other value is refused with 400. Someone who never purchased has no `rfmGroup` or scores: on those four fields they match only `notExists` and `notEquals`. So `rfmRecency` `notEquals` 5 includes everyone who never purchased; add an `exists` rule on the same field to leave them out (bought, but not recently).
        - `operator` (string, required, one of `equals`, `notEquals`, `contains`, `notContains`, `startsWith`, `exists`, `notExists`, `before`, `after`, `gt`, `gte`, `lt`, `lte`, `in`, `withinLastDays`, `moreThanDaysAgo`, `inNextDays`, `anniversaryInNextDays`, `anniversaryThisMonth`) — How to compare. Text comparisons (`equals`, `notEquals`, `contains`, `notContains`, `startsWith`, `in`) ignore case and accents ("jose" matches "José"). `notEquals` and `notContains` also match contacts without the value. Date operators (`before`, `after`, `withinLastDays`, `moreThanDaysAgo`) compare as dates and times in UTC: a stored ISO-8601 date is the start of that day, `before` means earlier than the given day and `after` later than it, and a value that is not an ISO-8601 date or date and time never matches. `inNextDays` is a date from today to that many days ahead. `anniversaryInNextDays` (0 = today) and `anniversaryThisMonth` ignore the year, so they suit birthdays, and also read a yearless `--MM-DD`; a 29 February counts on 28 February in other years. Days are UTC days; these three are for attributes and dates, not contact fields.
        - `value` (string, optional, up to 255 characters) — What to compare with, always as text: `"pro"`, `"3"`, `"true"`. Equality compares the stored value as text, so a stored 5 matches `"5"`. An ISO date for `before` and `after`; a number for `gt`, `gte`, `lt` and `lte`; a whole number of days (1 to 3650) for `withinLastDays`, `moreThanDaysAgo` and `inNextDays`, and 0 to 365 for `anniversaryInNextDays`. Omit for `in`, `exists`, `notExists` and `anniversaryThisMonth`.
        - `values` (string[], optional, at least 1 item, at most 100 items) — For `in` only: up to 100 values, each as text, any of which matches.
      - Any of: **Event**
        - `type` ("event", required)
        - `name` (string, required, 1–128 characters) — The event name.
        - `withinDays` (integer, optional, min > 0, max 3650) — Only events from the last `withinDays` days.
        - `notFollowedBy` (string, optional, 1–128 characters) — Only when this event name did not follow it afterwards.
        - `count` (object, optional) — How often; defaults to at least once. `{ operator: equals, value: 0 }` is "has not done".
        - `where` (object[], optional, at least 1 item, at most 5 items) — Property conditions the event must meet, up to 5; `count` counts only events meeting all of them.
      - Any of: **Message activity**
        - `type` ("activity", required)
        - `activity` (string, required, one of `sent`, `notSent`, `opened`, `clicked`, `notOpened`, `notClicked`, `bounced`) — `notOpened` and `notClicked`: sent, and not opened or clicked. Opens include machine opens (see `openedByMachine`).
        - `count` (object, optional) — How many messages had this activity; defaults to at least one. Not with `notSent`.
        - `link` (string, optional, 1–500 characters) — With `clicked` or `notClicked`: only clicks on a link whose address contains this text, ignoring case.
        - `channel` (string, optional, one of `email`, `push`) — Only messages on this channel (opened a push, not an email); any when left out.
        - `campaignId` (string, optional, uuid) — Only messages of this campaign.
        - `tagId` (string, optional, uuid) — Only messages of campaigns or journeys carrying this tag, as they are tagged when the rule is evaluated.
        - `journeyStepId` (string, optional, pattern `^[a-z0-9][a-z0-9_-]{0,63}$`) — Inside a journey only: the message sent by the step with this id in the contact's current enrollment. Matches nobody outside a journey.
        - `withinDays` (integer, optional, min > 0, max 3650) — Only activity from the last `withinDays` days. For `notOpened` and `notClicked` it is a waiting time instead: only messages sent at least this many days ago count, so each has had that long to be opened or clicked.
      - Any of: **Push installation**
        - `type` ("installation", required)
        - `has` (string, required, one of `active`, `none`) — `active`: reachable by push (granted, active, not invalidated); `none`: not.
        - `pushProjectId` (string, optional, uuid) — Only devices of this push project; any when omitted.
        - `platform` (string, optional, one of `ios`, `android`, `web`) — Only devices on this platform; any when omitted.
      - Any of: **List membership**
        - `type` ("list", required)
        - `listIds` (string[], required, at least 1 item, at most 50 items) — One list or several: with `member: true` in any of them, with `false` in none of them. Each must be one of the workspace's lists when the rules are saved.
        - `member` (boolean, required) — True for in the lists, false for not in them.
      - Any of: **Segment membership**
        - `type` ("segment", required)
        - `segmentId` (string, required, uuid)
        - `member` (boolean, required) — True for in the segment, false for not in it.
      - Any of: **Topic**
        - `type` ("topic", required)
        - `topicId` (string, required, uuid) — One of the workspace's topics when the rules are saved.
        - `channel` (string, optional, one of `email`, `push`) — Reads the contact's choice for this channel, which overrides their topic-wide choice. Without it, the rule reads the topic-wide choice, which a contact's `subscribed` shows.
        - `subscribed` (boolean, required) — True for contacts the topic reaches, false for those it does not.
      - Any of: **Match all**
        - `all` (object[], required, at least 1 item, at most 20 items) — Every rule must match.
      - Any of: **Match any**
        - `any` (object[], required, at least 1 item, at most 20 items) — At least one rule must match.
  - Any of: **Match any**
    - `any` (object[], required, at least 1 item, at most 20 items) — At least one rule or nested group must match.
      - Any of: **Attribute**
        - `type` ("attribute", required)
        - `field` (string, required, pattern `^attributes\.[\p{L}\p{N}\p{M}_.-]{1,64}$`) — `attributes.<key>`, e.g. `attributes.plan`.
        - `operator` (string, required, one of `equals`, `notEquals`, `contains`, `notContains`, `startsWith`, `exists`, `notExists`, `before`, `after`, `gt`, `gte`, `lt`, `lte`, `in`, `withinLastDays`, `moreThanDaysAgo`, `inNextDays`, `anniversaryInNextDays`, `anniversaryThisMonth`) — How to compare. Text comparisons (`equals`, `notEquals`, `contains`, `notContains`, `startsWith`, `in`) ignore case and accents ("jose" matches "José"). `notEquals` and `notContains` also match contacts without the value. Date operators (`before`, `after`, `withinLastDays`, `moreThanDaysAgo`) compare as dates and times in UTC: a stored ISO-8601 date is the start of that day, `before` means earlier than the given day and `after` later than it, and a value that is not an ISO-8601 date or date and time never matches. `inNextDays` is a date from today to that many days ahead. `anniversaryInNextDays` (0 = today) and `anniversaryThisMonth` ignore the year, so they suit birthdays, and also read a yearless `--MM-DD`; a 29 February counts on 28 February in other years. Days are UTC days; these three are for attributes and dates, not contact fields.
        - `value` (string, optional, up to 255 characters) — What to compare with, always as text: `"pro"`, `"3"`, `"true"`. Equality compares the stored value as text, so a stored 5 matches `"5"`. An ISO date for `before` and `after`; a number for `gt`, `gte`, `lt` and `lte`; a whole number of days (1 to 3650) for `withinLastDays`, `moreThanDaysAgo` and `inNextDays`, and 0 to 365 for `anniversaryInNextDays`. Omit for `in`, `exists`, `notExists` and `anniversaryThisMonth`.
        - `values` (string[], optional, at least 1 item, at most 100 items) — For `in` only: up to 100 values, each as text, any of which matches.
      - Any of: **Contact field**
        - `type` ("contact", required)
        - `field` (string, required, one of `email`, `firstName`, `lastName`, `phone`, `timezone`, `externalId`, `consent.email`, `consent.push`, `country`, `locale`, `rfmGroup`, `rfmRecency`, `rfmFrequency`, `rfmMonetary`, `purchaseCount`, `totalSpent`, `lastPurchasedAt`, `createdAt`) — The contact column to test. Each takes these operators, and is refused with others: `email`: equals, notEquals, contains, startsWith, exists, notExists, in; `firstName`: equals, notEquals, contains, startsWith, exists, notExists, in; `lastName`: equals, notEquals, contains, startsWith, exists, notExists, in; `phone`: equals, notEquals, contains, startsWith, exists, notExists, in; `timezone`: equals, notEquals, contains, startsWith, exists, notExists, in; `externalId`: equals, notEquals, contains, startsWith, exists, notExists, in; `consent.email`: equals; `consent.push`: equals; `country`: equals, notEquals, in, exists, notExists; `locale`: equals, notEquals, startsWith, in, exists, notExists; `rfmGroup`: equals, notEquals, in, exists, notExists; `rfmRecency`: equals, notEquals, in, exists, notExists, gt, gte, lt, lte; `rfmFrequency`: equals, notEquals, in, exists, notExists, gt, gte, lt, lte; `rfmMonetary`: equals, notEquals, in, exists, notExists, gt, gte, lt, lte; `purchaseCount`: equals, gt, gte, lt, lte; `totalSpent`: equals, gt, gte, lt, lte; `lastPurchasedAt`: before, after, withinLastDays, moreThanDaysAgo, exists, notExists; `createdAt`: before, after, withinLastDays, moreThanDaysAgo. `phone` holds +country numbers: write `equals`, `notEquals` and `in` values with their country code. Consent takes `"true"` or `"false"`. Purchases: `purchaseCount`, `totalSpent` (in the main currency) and `lastPurchasedAt` are a customer's totals of the workspace's purchase event (`purchaseEvent` in workspace settings: an order, a paid invoice, a booking), recomputed daily; the contact's stored `totalSpent` is null when the workspace's purchases carry no amount. In comparisons on `purchaseCount` and `totalSpent` a missing count or spend is 0, so someone who never purchased has 0 purchases and 0 spent. RFM: `rfmGroup` and its scores `rfmRecency`, `rfmFrequency` and `rfmMonetary` (monetary value: how much they spend) come from the same daily pass. A score is a whole number from 1 to 5, written as text (`"5"`; `values` of them for `in`), 5 being the most recent, most frequent or highest spending; any other value is refused with 400. Someone who never purchased has no `rfmGroup` or scores: on those four fields they match only `notExists` and `notEquals`. So `rfmRecency` `notEquals` 5 includes everyone who never purchased; add an `exists` rule on the same field to leave them out (bought, but not recently).
        - `operator` (string, required, one of `equals`, `notEquals`, `contains`, `notContains`, `startsWith`, `exists`, `notExists`, `before`, `after`, `gt`, `gte`, `lt`, `lte`, `in`, `withinLastDays`, `moreThanDaysAgo`, `inNextDays`, `anniversaryInNextDays`, `anniversaryThisMonth`) — How to compare. Text comparisons (`equals`, `notEquals`, `contains`, `notContains`, `startsWith`, `in`) ignore case and accents ("jose" matches "José"). `notEquals` and `notContains` also match contacts without the value. Date operators (`before`, `after`, `withinLastDays`, `moreThanDaysAgo`) compare as dates and times in UTC: a stored ISO-8601 date is the start of that day, `before` means earlier than the given day and `after` later than it, and a value that is not an ISO-8601 date or date and time never matches. `inNextDays` is a date from today to that many days ahead. `anniversaryInNextDays` (0 = today) and `anniversaryThisMonth` ignore the year, so they suit birthdays, and also read a yearless `--MM-DD`; a 29 February counts on 28 February in other years. Days are UTC days; these three are for attributes and dates, not contact fields.
        - `value` (string, optional, up to 255 characters) — What to compare with, always as text: `"pro"`, `"3"`, `"true"`. Equality compares the stored value as text, so a stored 5 matches `"5"`. An ISO date for `before` and `after`; a number for `gt`, `gte`, `lt` and `lte`; a whole number of days (1 to 3650) for `withinLastDays`, `moreThanDaysAgo` and `inNextDays`, and 0 to 365 for `anniversaryInNextDays`. Omit for `in`, `exists`, `notExists` and `anniversaryThisMonth`.
        - `values` (string[], optional, at least 1 item, at most 100 items) — For `in` only: up to 100 values, each as text, any of which matches.
      - Any of: **Event**
        - `type` ("event", required)
        - `name` (string, required, 1–128 characters) — The event name.
        - `withinDays` (integer, optional, min > 0, max 3650) — Only events from the last `withinDays` days.
        - `notFollowedBy` (string, optional, 1–128 characters) — Only when this event name did not follow it afterwards.
        - `count` (object, optional) — How often; defaults to at least once. `{ operator: equals, value: 0 }` is "has not done".
        - `where` (object[], optional, at least 1 item, at most 5 items) — Property conditions the event must meet, up to 5; `count` counts only events meeting all of them.
      - Any of: **Message activity**
        - `type` ("activity", required)
        - `activity` (string, required, one of `sent`, `notSent`, `opened`, `clicked`, `notOpened`, `notClicked`, `bounced`) — `notOpened` and `notClicked`: sent, and not opened or clicked. Opens include machine opens (see `openedByMachine`).
        - `count` (object, optional) — How many messages had this activity; defaults to at least one. Not with `notSent`.
        - `link` (string, optional, 1–500 characters) — With `clicked` or `notClicked`: only clicks on a link whose address contains this text, ignoring case.
        - `channel` (string, optional, one of `email`, `push`) — Only messages on this channel (opened a push, not an email); any when left out.
        - `campaignId` (string, optional, uuid) — Only messages of this campaign.
        - `tagId` (string, optional, uuid) — Only messages of campaigns or journeys carrying this tag, as they are tagged when the rule is evaluated.
        - `journeyStepId` (string, optional, pattern `^[a-z0-9][a-z0-9_-]{0,63}$`) — Inside a journey only: the message sent by the step with this id in the contact's current enrollment. Matches nobody outside a journey.
        - `withinDays` (integer, optional, min > 0, max 3650) — Only activity from the last `withinDays` days. For `notOpened` and `notClicked` it is a waiting time instead: only messages sent at least this many days ago count, so each has had that long to be opened or clicked.
      - Any of: **Push installation**
        - `type` ("installation", required)
        - `has` (string, required, one of `active`, `none`) — `active`: reachable by push (granted, active, not invalidated); `none`: not.
        - `pushProjectId` (string, optional, uuid) — Only devices of this push project; any when omitted.
        - `platform` (string, optional, one of `ios`, `android`, `web`) — Only devices on this platform; any when omitted.
      - Any of: **List membership**
        - `type` ("list", required)
        - `listIds` (string[], required, at least 1 item, at most 50 items) — One list or several: with `member: true` in any of them, with `false` in none of them. Each must be one of the workspace's lists when the rules are saved.
        - `member` (boolean, required) — True for in the lists, false for not in them.
      - Any of: **Segment membership**
        - `type` ("segment", required)
        - `segmentId` (string, required, uuid)
        - `member` (boolean, required) — True for in the segment, false for not in it.
      - Any of: **Topic**
        - `type` ("topic", required)
        - `topicId` (string, required, uuid) — One of the workspace's topics when the rules are saved.
        - `channel` (string, optional, one of `email`, `push`) — Reads the contact's choice for this channel, which overrides their topic-wide choice. Without it, the rule reads the topic-wide choice, which a contact's `subscribed` shows.
        - `subscribed` (boolean, required) — True for contacts the topic reaches, false for those it does not.
      - Any of: **Match all**
        - `all` (object[], required, at least 1 item, at most 20 items) — Every rule must match.
      - Any of: **Match any**
        - `any` (object[], required, at least 1 item, at most 20 items) — At least one rule must match.
- `exclusions` (object | null, required) — Lists and segments left out of its audience ("don't send to"); `null` for none.
  - `listIds` (string[], required, at most 20 items, default `[]`) — Lists whose members are left out.
  - `segmentIds` (string[], required, at most 10 items, default `[]`) — Segments whose members are left out, by the segment's rules at send.
- `topicId` (string | null, required, uuid) — Sends only to contacts the topic reaches; `null` sends to everyone with consent.
- `messageLimits` (MessageLimitUse, required)
  - `apply` (boolean, required) — Whether its messages count toward the workspace's message limits and are held by them.
  - `retryFor` (Duration, optional) — How long a message over a limit keeps being retried before it is skipped with reason `message_limit`: whole hours, at most 48. Unset, it is skipped at once. Absent when `apply` is false.
    - `value` (integer, required, min > 0)
    - `unit` (string, required, one of `seconds`, `minutes`, `hours`, `days`)
- `utm` (object | null, required) — UTM parameters added to the links in the email.
  - `enabled` (boolean, required)
  - `source` (string, optional, pattern `^[\p{L}\p{N}\p{M} ._\-+/]+$`, 1–100 characters)
  - `medium` (string, optional, pattern `^[\p{L}\p{N}\p{M} ._\-+/]+$`, 1–100 characters)
  - `campaign` (string, optional, pattern `^[\p{L}\p{N}\p{M} ._\-+/]+$`, 1–100 characters)
  - `content` (string, optional, pattern `^[\p{L}\p{N}\p{M} ._\-+/]+$`, 1–100 characters)
  - `term` (string, optional, pattern `^[\p{L}\p{N}\p{M} ._\-+/]+$`, 1–100 characters)
- `content` (CampaignContent, required)
  - `email` (object, optional) — The email fields and design; 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–512,000 characters) — The HTML body. Rendered from the design when one is set.
    - `text` (string, optional, 1–512,000 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; the part before the @ must be ASCII.
    - `templateSnapshot` (object, optional) — The email design or HTML, copied into the campaign so later template edits do not change it.
      - One of: **With schemaVersion, blocks and theme**
        - `kind` ("design", optional)
        - `schemaVersion` (integer, required, min > 0, max 1000)
        - `blocks` (object[], required, at least 1 item, at most 200 items) — The blocks, rendered top to bottom: at most 200 across the whole tree and five levels including the root. The footer stays at the root.
        - `theme` (EmailTheme, required)
        - `language` (string, optional) — The language the email is written in, as a BCP 47 tag ("en", "fr-BE", "ar"). It sets the HTML `lang` and the reading direction: Arabic, Hebrew, Persian, Urdu and other right-to-left languages render right to left. Defaults to "en".
        - `plainTextOverride` (string | null, optional, up to 512,000 characters) — Plain text sent instead of the version generated from the design or HTML.
      - One of: **With kind and html**
        - `kind` ("html", required)
        - `html` (string, required, up to 512,000 characters)
        - `plainTextOverride` (string | null, optional, up to 512,000 characters) — Plain text sent instead of the version generated from the design or HTML.
  - `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 2,048 characters) — The HTTPS page opened when the notification is tapped.
    - `imageUrl` (string, optional, uri, up to 2,048 characters) — A large picture: web and Android show it; iOS needs the app's service extension.
    - `iconUrl` (string, optional, uri, up to 2,048 characters) — Web only; iOS and Android show the app's own notification icon.
    - `data` (map<string, string>, optional, at most 10 keys, keys: pattern `^[A-Za-z0-9_.-]{1,64}$`, values: up to 1,024 characters) — 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 2,048 characters) — The HTTPS page the button opens.
    - `collapseKey` (string, optional, pattern `^[A-Za-z0-9_-]{1,64}$`) — 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.
    - `ttl` (DurationOrZero, optional) — How long a provider keeps trying an offline device, up to 28 days; zero delivers now or never. Defaults to one day.
      - `value` (integer, required, min 0)
      - `unit` (string, required, one of `seconds`, `minutes`, `hours`, `days`)
    - `urgency` (string, optional, one of `high`, `normal`) — `high` asks the push service to deliver promptly, even to a sleeping device; `normal` lets it wait for a good moment to save battery. Device settings and the app still decide whether and how it is shown. 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, pattern `^[a-z][a-z0-9+.-]*:\S+$`, up to 2,048 characters)
        - `imageUrl` (string, optional, uri, up to 2,048 characters) — A large picture: web and Android show it; iOS needs the app's service extension.
        - `iconUrl` (string, optional, uri, up to 2,048 characters) — Web only; iOS and Android show the app's own notification icon.
        - `data` (map<string, string>, optional, at most 10 keys, keys: pattern `^[A-Za-z0-9_.-]{1,64}$`, values: up to 1,024 characters) — 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, pattern `^[A-Za-z0-9_-]{1,64}$`) — 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.
        - `ttl` (DurationOrZero, optional) — How long a provider keeps trying an offline device, up to 28 days; zero delivers now or never. Defaults to one day.
        - `urgency` (string, optional, one of `high`, `normal`) — `high` asks the push service to deliver promptly, even to a sleeping device; `normal` lets it wait for a good moment to save battery. Device settings and the app still decide whether and how it is shown. 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` can break through Focus when the user allows it, and needs the app's Time Sensitive Notifications capability.
        - `threadId` (string, optional, pattern `^[A-Za-z0-9_.-]{1,64}$`) — 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, pattern `^[a-z][a-z0-9+.-]*:\S+$`, up to 2,048 characters)
        - `imageUrl` (string, optional, uri, up to 2,048 characters) — A large picture: web and Android show it; iOS needs the app's service extension.
        - `iconUrl` (string, optional, uri, up to 2,048 characters) — Web only; iOS and Android show the app's own notification icon.
        - `data` (map<string, string>, optional, at most 10 keys, keys: pattern `^[A-Za-z0-9_.-]{1,64}$`, values: up to 1,024 characters) — 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, pattern `^[A-Za-z0-9_-]{1,64}$`) — 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.
        - `ttl` (DurationOrZero, optional) — How long a provider keeps trying an offline device, up to 28 days; zero delivers now or never. Defaults to one day.
        - `urgency` (string, optional, one of `high`, `normal`) — `high` asks the push service to deliver promptly, even to a sleeping device; `normal` lets it wait for a good moment to save battery. Device settings and the app still decide whether and how it is shown. Unset, a transactional push is `high` and any other `normal`.
        - `channelId` (string, optional, pattern `^[A-Za-z0-9_.-]{1,64}$`) — 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 2,048 characters) — The HTTPS page opened when the notification is tapped.
        - `imageUrl` (string, optional, uri, up to 2,048 characters) — A large picture: web and Android show it; iOS needs the app's service extension.
        - `iconUrl` (string, optional, uri, up to 2,048 characters) — Web only; iOS and Android show the app's own notification icon.
        - `data` (map<string, string>, optional, at most 10 keys, keys: pattern `^[A-Za-z0-9_.-]{1,64}$`, values: up to 1,024 characters) — 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, pattern `^[A-Za-z0-9_-]{1,64}$`) — 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.
        - `ttl` (DurationOrZero, optional) — How long a provider keeps trying an offline device, up to 28 days; zero delivers now or never. Defaults to one day.
        - `urgency` (string, optional, one of `high`, `normal`) — `high` asks the push service to deliver promptly, even to a sleeping device; `normal` lets it wait for a good moment to save battery. Device settings and the app still decide whether and how it is shown. Unset, a transactional push is `high` and any other `normal`.
    - `pushProjectId` (string, optional, uuid) — The push project it goes out through. Needed to send.
- `abTest` (object | null, required) — Its A/B test, if it has one; `null` sends one version to everyone.
  - `versions` (object[], required) — Versions 2 onward; version 1 is the campaign's own content.
    - `email` (object, optional) — This version's email fields and design; any left out are version 1's.
      - `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–512,000 characters) — The HTML body. Rendered from the design when one is set.
      - `text` (string, optional, 1–512,000 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; the part before the @ must be ASCII.
      - `templateSnapshot` (object, optional) — The email design or HTML, copied into the campaign so later template edits do not change it.
        - One of: **With schemaVersion, blocks and theme**
        - One of: **With kind and html**
    - `push` (object, optional) — This version's push title, body and image; any left out are version 1's.
      - `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).
      - `imageUrl` (string, optional, uri, up to 2,048 characters) — A large picture: web and Android show it; iOS needs the app's service extension.
  - `testPercent` (integer, required, min 10, max 50) — The share of the audience, in percent, the versions go to, split evenly between them. The rest get the winner.
  - `winnerBy` (string, required, one of `clicks`, `opens`, `conversions`) — How the winner is picked: the version with the highest click or open rate over its delivered messages (as the campaign's own rates; over sent until any version has a delivery), or the highest share of its sent messages that reached the campaign's conversion goal (`conversionGoalId`). Opens overcount: machine opens (Apple Mail Privacy Protection, Gmail's prefetch, mail scanners) count unread email as opened. A push test is decided by clicks (a tap the app reports with a link to follow, so the push needs an `actionUrl`) or conversions, not opens. A tie goes to the lower version.
  - `conversionGoalId` (string | null, required, uuid) — The conversion goal that decides, when `winnerBy` is `conversions`.
  - `decideAfter` (object | null, required) — How long after the test went out to pick the winner and send it to the rest, from 1 to 72 hours; `null` to pick it by hand.
    - `value` (integer, required, min > 0)
    - `unit` (string, required, one of `seconds`, `minutes`, `hours`, `days`)
  - `winner` (integer | null, required, min 1) — The winning version, once picked.
  - `testSentAt` (string | null, required, date-time) — When every test message was queued.
  - `decidedAt` (string | null, required, date-time) — When the winner was picked.
- `status` (string, required, one of `draft`, `scheduled`, `sending`, `testing`, `sent`, `cancelled`) — Where the campaign is in its lifecycle. `testing`: an A/B test went out and the rest of the audience waits for the winner. `sent`: every message was created and queued; it says nothing about delivery, which `GET /v1/campaigns/{id}/metrics` reports (some may still be queued, skipped or failed).
- `schedule` (object | null, required) — When it sends: `fixed`, everyone at one instant, or `local_time`, each contact at a time on their own clock. Set while it is `scheduled`; a local-time schedule stays set once sending starts, since each contact's time still decides when theirs goes. `null` otherwise.
  - One of: **Fixed time**
    - `type` ("fixed", required)
    - `sendAt` (string, required, date-time) — The instant everyone receives it at.
  - One of: **Local time**
    - `type` ("local_time", required)
    - `localDateTime` (string, required, pattern `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$`) — The wall-clock time (`YYYY-MM-DDTHH:MM`, no timezone) each contact receives it at on their own clock. It may already have passed in some timezones, not in all.
    - `timezoneIfUnknown` (string | null, required) — The IANA timezone for contacts without one; `null`: the workspace's.
    - `ifTimeHasPassed` (string, required, one of `send_now`, `skip`, `next_day`) — Where the time has already gone when it is scheduled: `send_now` sends there at once, `next_day` at the same time the next day, and `skip` leaves those contacts out (skipped with `local_time_passed`).
    - `startsAt` (string, required, date-time) — When sending starts: the earliest timezone reaching `localDateTime`.
- `sentAt` (string | null, required, date-time) — When sending started; set from `sending` on (testing, sent, cancelled), `null` before. A local-time campaign starts in the earliest timezone.
- `delivery` (CampaignDelivery, required) — How fast it goes out, and until when; frozen with its content at send.
  - `rateLimit` (object | null, required) — How fast it goes out; `null` as fast as the plan allows. The plan's own send rate still applies, so a campaign can only slow a send down.
    - `recipients` (integer, required, min 1, max 1000000) — At most this many recipients per `per`, spread evenly across it. Counts people, so one person's email and push go together.
    - `per` (string, required, one of `minute`, `hour`, `day`) — The period the limit counts over.
  - `expiresAfter` (object | null, required) — From when sending starts, from 1 to 720 hours: recipients not yet sent when it passes are skipped with reason `expired`. A rate-limited send always expires, after 720 hours unless set. With an A/B test it must outlast `abTest.decideAfter`. `null`: no expiry.
    - `value` (integer, required, min > 0)
    - `unit` (string, required, one of `seconds`, `minutes`, `hours`, `days`)
- `pendingAgentReview` (boolean, required) — An agent key authored or edited it and it is still a draft, so it waits in the agent review queue: sending needs a person's approval.
- `sendApprovalRequest` (object | null, required) — The latest request, from a member in the dashboard, that an admin send it while the workspace keeps sending to admins; kept after it sends, cleared by any edit before then.
  - `userId` (string, required, uuid) — The member who asked.
  - `requestedAt` (string, required, date-time)
  - `note` (string | null, required)
- `archivedAt` (string | null, required, date-time) — When it was archived; archived campaigns are left out of the list by default.
- `tags` (string[], required) — Tag names, sorted without case.
- `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)
