Skip to content

Run a campaign

Download .md

Send the same text, WhatsApp template or AI call to a whole audience, with one body for every channel: only content changes. Create a draft, preview it, then start exactly what you previewed.

Every create says start: "draft" saves it for preview, "auto" starts it now. Without start it is 400 start_required, and nothing is created.

You need: a number of your own that can send on the channel (senders; not the shared trial number, and for SMS a Twilio number for now), an audience (lists), and the compliance acknowledgment accepted once. Every field: reference.

Terminal window
curl -X POST https://api.omniocto.com/v1/campaigns \
-H "Authorization: Bearer $OMNI_OCTO_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "Spring sale",
"channel": "sms",
"start": "draft",
"from": "+16285550100",
"content": { "text": "Hi {{first_name}}, 20% off today at {{shop}}. Your size {{shoe_size}} is in stock." },
"audience": { "list": "cl-0000000001" },
"variables": { "shop": "Main Street", "shoe_size": "{{contact.shoe_size|your size}}" }
}'

A draft answers 201 Created and sends nothing:

{ "id": "00000000-0000-4000-8000-000000000007", "short_id": "ca-0000000001", "object": "campaign",
"name": "Spring sale", "channel": "sms", "status": "draft", "paused_reason": null,
"from": "+16285550100",
"content": { "text": "Hi {{first_name}}, 20% off today at {{shop}}. Your size {{shoe_size}} is in stock.",
"media": null, "track_links": false, "show_in_conversations": null },
"audience": { "type": "list", "id": "cl-0000000001" },
"variables": { "first_name": "{{contact.first_name}}", "shop": "Main Street",
"shoe_size": "{{contact.shoe_size|your size}}" },
"schedule": null,
"limits": { "spend_cap_credits": null, "max_concurrent_calls": null }, "retry": null,
"counts": null, "metadata": null,
"sms": { "engagement": null, "skipped_by_reason": null, "cost": null, "errors": null, "rate": null },
"created_at": "2026-10-15T17:02:11.000Z", "started_at": null, "completed_at": null,
"completed_reason": null }

Placeholders like {{shop}} are filled per person from variables: plain text, or the contact’s own field with a fallback. What fills by itself, and who is skipped for a missing value, is in How the channels differ. Set track_links (true, in content) counts clicks. For an MMS, set content.media to the media that POST /v1/media answers. Inline audience.contacts or audience.csv are saved as contacts even for a draft (make a list to try). The list an inline audience is imported into (source: "campaign") is removed when you delete the draft, unless another campaign uses it; the contacts and their opt-outs stay.

Terminal window
curl -X POST https://api.omniocto.com/v1/campaigns/ca-0000000001/previews \
-H "Authorization: Bearer $OMNI_OCTO_API_KEY"
{ "id": "snap_b81c4e2a-6d8f-4a1b-9c3e-5f7a9b1d3e5f_1792083731000_9f3a6c1d2e8b", "object": "campaign_preview",
"campaign_id": "00000000-0000-4000-8000-000000000007", "status": "ready",
"recipients": 2, "skipped": 0, "skipped_by_reason": {},
"cost_credits": 4, "balance_credits": null, "cost_estimate": null,
"samples": [
{ "contact_id": "00000000-0000-4000-8000-0000000000a1", "contact_short_id": "ct-00000000a1",
"name": "Ana Lopez", "to": "+14155550101",
"text": "Hi Ana, 20% off today at Main Street. Your size 8 is in stock.", "segments": 1 },
{ "contact_id": "00000000-0000-4000-8000-0000000000a2", "contact_short_id": "ct-00000000a2",
"name": "Sam Lee", "to": "+14155550102",
"text": "Hi Sam, 20% off today at Main Street. Your size 10 is in stock.", "segments": 1 }
],
"error": null, "freezes_audience": true }
  • id is what you start with. An SMS preview’s is snap_…; a WhatsApp or voice preview’s is rev_…, like rev_678b05f2-121b-45a0-b5df-a0a0ce2b02b5_1792435303068_4511bc52c922. Pass it back whole, as it came.
  • recipients is who will get it; skipped_by_reason says who will not, and why. Opted-out numbers are always skipped.
  • cost_credits is what it will cost (for voice, an estimate; cost_estimate has its range).
  • balance_credits (SMS only) is minus the shortfall, or null when your balance covers it; WhatsApp and voice always answer null.
  • samples shows the message filled in for a few people (WhatsApp: with each button’s link or copy code).
  • Whether a start needs a preview, whether the preview freezes the audience and how long it stays usable depend on the channel: How the channels differ.

The POST waits up to 5 seconds for an SMS preview to build. A larger audience answers "status": "building":

{ "id": "snap_…", "object": "campaign_preview", "campaign_id": "…", "status": "building",
"recipients": null, "skipped": null, "skipped_by_reason": null, "cost_credits": null,
"balance_credits": null, "cost_estimate": null, "samples": null, "error": null,
"freezes_audience": true, "poll_after_ms": 2000, "progress": { "done": 3000, "total": 12000 } }

Read it again with GET /v1/campaigns/{id}/previews/{preview_id} after poll_after_ms (or Retry-After, in seconds) until it is ready or failed. A build stalled for 2 minutes fails with interrupted: preview again.

A preview runs every check the start makes: the compliance acknowledgment, balance, billing, calling minutes, sender, agent, content and audience. A preview that cannot start the campaign answers "status": "failed", with the code the start would answer in error and every other problem in error.also, each with a hint saying what to do. Fix them, then preview again. An edit, or a change to something the preview checked (the sender, the template, the agent, the compliance acknowledgment, the list), makes a preview stale; a PATCH that changes nothing is not an edit. How long each channel’s preview stays usable: How the channels differ.

Terminal window
curl -X POST https://api.omniocto.com/v1/campaigns/ca-0000000001/start \
-H "Authorization: Bearer $OMNI_OCTO_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "preview_id": "snap_b81c4e2a-6d8f-4a1b-9c3e-5f7a9b1d3e5f_1792083731000_9f3a6c1d2e8b" }'

It answers 200 with the campaign, status: "queued"; it moves to running as sending starts.

  • An SMS draft needs preview_id; WhatsApp and voice may start without one.
  • A stale or failed preview is 409 preview_stale: preview again.
  • Send an Idempotency-Key (why). A retry with it never starts twice.
  • A draft missing something a start needs is 422 launch_fields_missing, and missing lists the fields to set.

Each field takes its own kind of id, or that thing’s UUID (ids):

fieldlist it withid
fromGET /v1/sendersE.164, pn-…, or a channel’s ch-…
audience.listGET /v1/listscl-…
audience.segmentGET /v1/segmentssg-…
content.templateGET /v1/templateswt-…
content.agentGET /v1/agentsag-…
content.text (SMS)GET /v1/sms-templates, its texttp-…
Terminal window
curl "https://api.omniocto.com/v1/lists?limit=20" -H "Authorization: Bearer $OMNI_OCTO_API_KEY"
{ "object": "list", "has_more": false, "next_cursor": null,
"data": [ { "id": "cl-0000000001", "object": "contact_list", "uuid": "9f3c…",
"name": "VIP customers", "source": "dashboard", "contacts": 412,
"created_at": "2026-10-01T16:20:00.000Z" } ] }

POST /v1/lists makes a list and imports its contacts, the same rows as audience.contacts. A number a contact already has reuses that contact. Each phone is stored for SMS, calls and WhatsApp alike, so one list serves a campaign on any channel. Storing a number is not consent: opt-outs stay on the number.

Terminal window
curl -X POST https://api.omniocto.com/v1/lists \
-H "Authorization: Bearer $OMNI_OCTO_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "name": "Open house", "contacts": [ { "phone": "+14155550100", "name": "Ana" } ] }'

The answer is the list (source: "api") with import, what the import did. Pass its id as audience.list. POST /v1/lists/{id}/contacts adds more, GET /v1/lists/{id}/contacts pages through them, DELETE /v1/lists/{id}/contacts/{contact} takes one out (by ct-… id, or by number when only one contact in the list has it), and DELETE /v1/lists/{id} deletes the list, refused with 409 list_in_use while a campaign that has not finished uses it. A deleted list can no longer be an audience.list. Every field: Contact lists reference.

/v1/contacts keeps the people themselves: create and find them, give them several numbers and emails, tag them, opt them out, and add the ones you have to a list by id. See Keep contacts.

fieldwhat it isrules
contentthe text, the WhatsApp template, or the agent and missionChannel fields
audienceone of list, segment, contacts, csvAudience
variableshow each placeholder is filled per personVariables
schedulewhen it may send or callSchedule
limits, retrya spend cap (SMS and AI calls), calls at once and retries (voice)Limits and retries
call_settingsvoice: phone menus, recording, hold limit, voicemail, knowledge baseCall settings

"phone_menu": "never" turns phone-menu handling off (fields). Running campaign:

Terminal window
curl -X POST https://api.omniocto.com/v1/campaigns/ca-0000000001/pause -H "Authorization: Bearer $OMNI_OCTO_API_KEY"
curl -X PATCH https://api.omniocto.com/v1/campaigns/ca-0000000001 \
-H "Authorization: Bearer $OMNI_OCTO_API_KEY" -H "Content-Type: application/json" \
-d '{ "call_settings": { "phone_menu": "never" } }'
curl -X POST https://api.omniocto.com/v1/campaigns/ca-0000000001/resume -H "Authorization: Bearer $OMNI_OCTO_API_KEY" -H "Idempotency-Key: $(uuidgen)"
Terminal window
curl https://api.omniocto.com/v1/campaigns/ca-0000000001 -H "Authorization: Bearer $OMNI_OCTO_API_KEY"

status ends completed, failed or canceled. counts has recipients, pending, sent, delivered, failed, skipped, replied and opted_out (null where a channel does not count them: voice has neither of the last two, WhatsApp no opted_out). Rather than poll, subscribe to the campaign events. What each count means: Reading a campaign. Cost:

Terminal window
curl https://api.omniocto.com/v1/campaigns/ca-0000000001/usage -H "Authorization: Bearer $OMNI_OCTO_API_KEY"
Terminal window
curl -X POST https://api.omniocto.com/v1/campaigns/ca-0000000001/pause -H "Authorization: Bearer $OMNI_OCTO_API_KEY"
curl -X POST https://api.omniocto.com/v1/campaigns/ca-0000000001/resume -H "Authorization: Bearer $OMNI_OCTO_API_KEY" -H "Idempotency-Key: $(uuidgen)"
curl -X POST https://api.omniocto.com/v1/campaigns/ca-0000000001/cancel -H "Authorization: Bearer $OMNI_OCTO_API_KEY"

Each answers the campaign with its new status. Pause and cancel are safe to repeat. A resume takes an Idempotency-Key on every channel (send one). A campaign also pauses by itself, when credits run out or it reaches its spend cap: paused_reason says why. DELETE /v1/campaigns/{id} removes a draft, or a campaign canceled before it sent anything; to stop a draft, delete it. A campaign that has sent keeps its history: cancel it instead.

PATCH /v1/campaigns/{id} edits a draft or a paused campaign; pause a running one first. content, variables, limits, retry and call_settings merge; schedule and metadata are replaced; a name in variables set to null is removed. A null on another channel’s field ("retry": null on SMS) is ignored. Preview again after any edit. On a voice campaign that has started, call_settings reach the calls placed after a resume, but a new content.mission does not: its calls keep their first mission. What each field does.

An SMS campaign takes a new from only as a draft. An uncleared number is accepted; preview and start refuse it (sender_not_cleared).

Terminal window
curl "https://api.omniocto.com/v1/campaigns/ca-0000000001/recipients?limit=500" \
-H "Authorization: Bearer $OMNI_OCTO_API_KEY"

One row per person: to, status, error, attempts, and its message (message_id) or call (outcome, call_id). Filter with ?status=, page with limit and after; read one by id. Every status.

With "start": "auto", one request creates the campaign, runs every check a start runs, and starts it. It sends for real, with no review step.

Terminal window
curl -X POST https://api.omniocto.com/v1/campaigns \
-H "Authorization: Bearer $OMNI_OCTO_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "Spring sale",
"channel": "sms",
"start": "auto",
"from": "+16285550100",
"content": { "text": "Hi {{first_name}}, 20% off today at {{shop}}." },
"audience": { "contacts": [
{ "name": "Ana Lopez", "phone": "+14155550201" },
{ "name": "Ben Ito", "phone": "+14155550202" }
] },
"variables": { "shop": "Main Street" }
}'

It answers 202 Accepted with the campaign: queued, running, or preparing while a large SMS audience is checked. An empty audience is 400 empty_audience_input, and nothing is created. ?status=preparing lists the ones still being prepared. When everybody is skipped it is 422 no_recipients (or all_opted_out for SMS), and the campaign is kept as a draft on every channel: skipped_by_reason says why each person was left out, and error.campaign_id names the draft. Fix the audience, then start the draft. A retry with the same Idempotency-Key answers the same refusal for the same draft and creates no second campaign. A start of a WhatsApp or voice draft whose audience reaches nobody is 422 no_recipients too.

Accept the outbound compliance acknowledgment

Section titled “Accept the outbound compliance acknowledgment”

A workspace’s first campaign needs an owner or admin to accept the outbound compliance acknowledgment once. Until then a start is 409 outbound_compliance_required; drafts are never gated. Read it, and show attestation to the person accepting it:

Terminal window
curl https://api.omniocto.com/v1/compliance \
-H "Authorization: Bearer $OMNI_OCTO_API_KEY"
{ "object": "outbound_compliance", "accepted": false, "version": null,
"current_version": "2026-09-01", "attestation": "…", "accepted_at": null }

Then accept the current_version you read:

Terminal window
curl -X POST https://api.omniocto.com/v1/compliance/accept \
-H "Authorization: Bearer $OMNI_OCTO_API_KEY" -H "Content-Type: application/json" \
-d '{ "version": "2026-09-01" }'

It answers the object with accepted: true, accepted as the key’s owner. When we change the text, current_version moves on and accepted is false again. Accepting twice keeps the first acceptance. A stale version is 409 version_mismatch; a key whose owner is not an owner or admin gets 403 insufficient_role. You can also accept it in the dashboard under Settings → Compliance → Outbound. Every field: Account.

Campaigns announce campaign.started, campaign.paused, campaign.resumed, campaign.completed and campaign.canceled as webhooks and on the event stream. SMS campaigns add campaign.waiting and campaign.window_opened around their sending hours, and voice campaigns’ calls add the call events. Per-text and per-call updates reach only endpoints that name them. See Campaign events.

To wait for one campaign, poll:

Terminal window
curl -s "https://api.omniocto.com/v1/events?wait=25&api_version=v2&campaign=ca-0000000001&types=campaign.completed,campaign.canceled" \
-H "Authorization: Bearer $OMNI_OCTO_API_KEY"

See Poll instead of stream.

Common codes (how errors work):

statuscodewhat to do
409preview_required, preview_stalepreview, then start with that preview_id
409outbound_compliance_requiredaccept the acknowledgment above (POST /v1/compliance/accept)
422unmapped_variablefill every placeholder fields lists
400unknown_variableremove or rename the names fields lists (422 on a WhatsApp or voice start or resume)
402insufficient_creditsadd credits; short_credits says how many, GET /v1/balance what you have
409campaign_not_editablepause it before a PATCH

Every campaign code, with its fix: Errors → Campaigns.