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#
- You create a campaign: a channel, an audience and a message. It is a
draft. - You launch it. The audience is counted, the funds are checked, and a snapshot of the recipients is taken in the background (
queued). - 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 producesmessage.*events. - 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
}'{
"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.type | Recipients |
|---|---|
segment | Contacts that match the segment when the campaign starts. |
list | Members of the list when the campaign starts. |
tags | Contacts carrying any (or, with match: "all", all) of the tags. |
adhoc | Up 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_inis 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 tag | Value |
|---|---|
{{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_atstarts the campaign later;schedule.timezoneis the zone the times are read in.schedule.send_window, such as{ "start": "09:00", "end": "20:00" }, keeps sending inside those hours. Withrecipient_timezonethe 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.
| Event | Sent when |
|---|---|
campaign.started | Sending began, or was resumed. |
campaign.paused | Paused by you, by our staff, because the funds ran out or because the channel disconnected. |
campaign.completed | Every recipient has an outcome. Delivery receipts can still arrive afterwards. |
campaign.failed | The 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}/pausestops sending within seconds. Messages already handed to the channel still go out.POST /v1/campaigns/{id}/resumecontinues where the campaign stopped.POST /v1/campaigns/{id}/cancelstops for good. Recipients not messaged yet are markedcancelled.- An action that does not fit the current status answers
409 campaign_state_invalid.