Skip to content

Audience

Campaigns

Send one message to many recipients at a controlled speed, with per-recipient results, pause and resume, and the same billing as single messages.

How a campaign works#

  1. You create a campaign: a channel, an audience and a message. It is a draft.
  2. You launch it. The audience is counted, the funds are checked, and a snapshot of the recipients is taken in the background (queued).
  3. Messages go out at the speed you set (sending). Each one is an ordinary message: it is charged when it is sent, appears in the message log and produces message.* events.
  4. When every recipient has an outcome the campaign is completed. You can pause, resume or cancel at any time before that.

Keys need the scopes campaigns:read and campaigns:write. A campaign belongs to the mode of the key: with a test key it runs against a sandbox channel, delivers nothing and costs nothing.

Create and launch in one request#

For automations, a template plus a list of recipients is enough. launch: true creates the campaign and launches it; if the launch is refused, nothing is created.

curl https://api.omnimessage.co/v1/campaigns \
  -H "Authorization: Bearer om_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Back in stock",
    "channel": "ch_7Hq2mN5vB8cX1zL0pK3j",
    "audience": {
      "type": "adhoc",
      "recipients": [
        {
          "to": "+971503456789",
          "first_name": "Noor"
        }
      ]
    },
    "message": {
      "type": "template",
      "template": {
        "name": "order_update",
        "language": {
          "code": "en"
        },
        "components": [
          {
            "type": "body",
            "parameters": [
              {
                "type": "text",
                "text": "{{first_name}}"
              },
              {
                "type": "text",
                "text": "back in stock"
              }
            ]
          }
        ]
      }
    },
    "merge_fallbacks": {
      "first_name": "there"
    },
    "consent_declared": true,
    "launch": true
  }'
Response
{
  "id": "cmp_5Cf8hK1mP4rT7vY0aD3g",
  "object": "campaign",
  "mode": "live",
  "name": "Back in stock",
  "channel_id": "ch_7Hq2mN5vB8cX1zL0pK3j",
  "channel_type": "whatsapp",
  "status": "queued",
  "pause_reason": null,
  "failure_reason": null,
  "audience": {
    "type": "adhoc",
    "count": 1,
    "save_as_contacts": false
  },
  "message": {
    "type": "template",
    "template": {
      "name": "order_update",
      "language": {
        "code": "en"
      },
      "components": [
        {
          "type": "body",
          "parameters": [
            {
              "type": "text",
              "text": "{{first_name}}"
            },
            {
              "type": "text",
              "text": "back in stock"
            }
          ]
        }
      ]
    }
  },
  "merge_fallbacks": {
    "first_name": "there"
  },
  "require_opt_in": false,
  "consent_declared": true,
  "schedule": {
    "send_at": null,
    "timezone": "Asia/Dubai",
    "send_window": null,
    "recipient_timezone": false
  },
  "throttle_per_second": 20,
  "counters": {
    "total": 0,
    "queued": 0,
    "sent": 0,
    "delivered": 0,
    "read": 0,
    "failed": 0,
    "skipped": 0,
    "cancelled": 0,
    "skip_reasons": {}
  },
  "cost": {
    "currency": "USD",
    "estimated_wallet_micros": 0,
    "estimated_credits": 1,
    "wallet_micros": 0,
    "package_credits": 0
  },
  "created_at": "2026-10-05T09:00:00.000Z",
  "launched_at": "2026-10-05T09:05:00.000Z",
  "started_at": null,
  "paused_at": null,
  "completed_at": null,
  "cancelled_at": null
}

Without launch the campaign stays a draft until POST /v1/campaigns/{id}/launch. Drafts can also be prepared in the console and launched from code.

Audience#

audience.typeRecipients
segmentContacts that match the segment when the campaign starts.
listMembers of the list when the campaign starts.
tagsContacts carrying any (or, with match: "all", all) of the tags.
adhocUp to 10,000 recipients in the request. save_as_contacts: true stores them as contacts.

Some contacts of the audience are left out, and counted in counters.skip_reasons:

  • no_identifier: no phone number (or channel ID) for this channel.
  • invalid_number: a pasted number that is not a phone number.
  • unsubscribed: opted out on this channel type, or blocked. Checked again just before sending, so a STOP that arrives mid-campaign is honoured.
  • no_consent: require_opt_in is set and the contact did not opt in.
  • duplicate: the same recipient more than once.

Message and merge tags#

message is the body of POST /v1/messages without channel and to. Any string in it may contain merge tags, which are replaced for each recipient:

Merge tagValue
{{first_name}}, {{last_name}}, {{full_name}}Name of the contact.
{{phone}}, {{email}}, {{locale}}Fields of the contact.
{{attributes.<key>}}A custom attribute you defined.

An unknown tag is rejected when the campaign is saved, not while it is being sent. merge_fallbacks supplies the value for recipients that have none, for example { "first_name": "there" }. For pasted recipients, variables on the recipient wins over both.

Schedule and speed#

  • schedule.send_at starts the campaign later; schedule.timezone is the zone the times are read in.
  • schedule.send_window, such as { "start": "09:00", "end": "20:00" }, keeps sending inside those hours. With recipient_timezone the window is applied in each contact’s own time zone when it is known.
  • throttle_per_second (1 to 100) is the speed of this campaign. All campaigns on one channel share that channel’s limit, and campaigns of one account take turns.

Cost and funds#

Each message costs what a single message costs: one package credit, or the per-message price from the wallet. Failed messages are refunded. Launch estimates the total and answers 402 insufficient_balance when credits and wallet do not cover every recipient.

Pass allow_insufficient_funds: true to start anyway. When the funds run out the campaign pauses with pause_reason: "insufficient_balance", balance.low is sent, and POST /v1/campaigns/{id}/resume continues after a top-up. Nobody is messaged or charged twice.

Following progress#

GET /v1/campaigns/{id} returns the counters: queued, sent (accepted and not failed), delivered, read, failed, skipped. GET /v1/campaigns/{id}/recipients lists every recipient with its status, the message ID and the error if it failed; filter with status=failed.

EventSent when
campaign.startedSending began, or was resumed.
campaign.pausedPaused by you, by our staff, because the funds ran out or because the channel disconnected.
campaign.completedEvery recipient has an outcome. Delivery receipts can still arrive afterwards.
campaign.failedThe campaign cannot continue, for example because its channel was deleted.

Subscribe a webhook endpoint to these events instead of polling.

Pause, resume, cancel#

  • POST /v1/campaigns/{id}/pause stops sending within seconds. Messages already handed to the channel still go out.
  • POST /v1/campaigns/{id}/resume continues where the campaign stopped.
  • POST /v1/campaigns/{id}/cancel stops for good. Recipients not messaged yet are marked cancelled.
  • An action that does not fit the current status answers 409 campaign_state_invalid.

    Loading