Skip to main content

Bulk Messaging / Campaigns

← Back to Documentation Index

Overview

The campaigns feature sends a single plaintext message to many recipients while protecting the SMSC from overload. You upload a named recipient list once, then run one or more campaigns against it. Each campaign drips messages into the normal message queue at a configurable rate, and reports live progress and delivery statistics.

Two reusable objects:

ObjectWhat it is
Campaign listA named, reusable set of recipient MSISDNs, each with up to four optional variables. Upload once (JSON array or CSV), reuse across campaigns.
CampaignA send run: a list (or all active subscribers) + a message template, dripped into the queue.

Everything is exposed over the HTTP API. All state is stored in Mnesia alongside the rest of the SMSC.

Targeting: lists and the active-subscriber default

A campaign's recipients come from one of two sources:

  • A recipient list (list_id) — the MSISDNs you uploaded.
  • No list at all — if you create a campaign without a list_id, it targets all currently active (registered) subscribers — every MSISDN with an unexpired registration in the location store at the moment the campaign starts. This is the easiest way to reach "everyone on the network right now".

Per-recipient variables and templating

Each list recipient can carry up to four variables — var1, var2, var3, var4 — for things like first name, last name, or plan name. The campaign message is a template: {{ var1 }}{{ var4 }} (and {{ msisdn }}) are spliced in for each recipient when the message is sent. Whitespace inside the braces is optional, and any placeholder with no value renders as an empty string.

The templating is deliberately minimal — variable substitution only (no conditionals or filters), so there is no extra dependency and no surprises in what subscribers receive.

Uploading variables via CSV — the MSISDN column is the one named msisdn/destination_msisdn/number/phone (or the first column if none is named); every other column becomes var1var4 in column order:

msisdn,first,last,plan
12025550101,Alice,Smith,Gold
12025550102,Bob,Jones,Silver

Uploading variables via JSON — recipient objects may set the vars directly:

{ "name": "VIPs", "recipients": [
{ "msisdn": "12025550101", "var1": "Alice", "var2": "Gold" }
] }

Example template:

Hi {{ var1 }}, your {{ var2 }} plan renews soon. Reply STOP to opt out.

For recipient 12025550101 above (var1=Alice, var2=Gold) this sends: Hi Alice, your Gold plan renews soon. Reply STOP to opt out.

Audience filter

When a campaign uses a list, the audience flag decides who actually gets a message:

AudienceBehaviour
active_only (default)Only list members that are registered (an unexpired location) at the moment the campaign starts. Everyone else is recorded as skipped_inactive and never sent to.
allEvery member of the list, regardless of registration.

The registration check is a snapshot taken at start time — subscribers who register later are not picked up. When a campaign has no list, the recipient set is already the active-subscriber base, so audience is implicitly active_only and any supplied value is ignored.

Lifecycle

  1. draft — created, no targets yet.
  2. start — the campaign snapshots its targets (from its list or the active subscriber base), then moves to running. If there is nothing to send it goes straight to completed.
  3. running — the drip worker submits messages into the queue.
  4. pause / resume — temporarily stop / continue dripping.
  5. completed — every target has been dripped into the queue (delivery then continues asynchronously, and delivery stats keep updating).
  6. cancelled — stopped permanently.

Deleting a campaign

Deleting a campaign stops it but keeps the record — it is cancelled rather than purged, so its history and final statistics stay visible. Stopped and completed campaigns are removed automatically once they age past the retention TTL (campaign_retention_hours, default 7 days).

Drip rate (overload protection)

Each campaign has a drip_tps (messages submitted into the queue per second, default from config). A background worker wakes on a fixed interval and submits at most drip_tps × interval messages per campaign per tick — so a one-million recipient campaign never floods the queue. Rate is per campaign; multiple running campaigns each drip at their own rate.

Progress and delivery statistics

Every campaign object includes a live stats block (poll GET /api/campaigns/:id):

FieldMeaning
totalTargets snapshotted at start
pendingNot yet dripped into the queue
queuedSubmitted, awaiting a delivery report
deliveredConfirmed delivered
failedDelivery failed / dead-lettered
skipped_inactiveSkipped because not registered at start
dispatchedqueued + delivered + failed
progress_percentPercent of targets no longer pending
delivery_percentPercent of dispatched messages delivered

Delivery and failure counts update automatically as delivery reports flow back through the normal SMSC delivery path. Per-recipient detail (and filtering by state, e.g. everyone that failed) is available via GET /api/campaign_targets/:id?state=failed.

Configuration

config :sms_c, :campaigns,
# Start the drip worker (set false to pause ALL campaign delivery)
enabled: true,
# Interval between drip ticks in milliseconds
drip_tick_ms: 1000,
# Per-campaign messages/second when a campaign does not set its own drip_tps
default_drip_tps: 50,
# Default message validity window when a campaign sets no validity_hours
message_validity_hours: 72,
# How long a stopped/completed campaign persists before it is purged (hours)
campaign_retention_hours: 168

Validity and scheduled delivery

A campaign can set, at creation time:

  • validity_hours — the message validity period. Each submitted message is stamped with an expires so undelivered messages lapse instead of lingering in the queue. Defaults to message_validity_hours from config.
  • deliver_after — an optional ISO8601 time (must be in the future) for a scheduled send.

How scheduled delivery interacts with the drip. The campaign still starts dripping immediately at drip_tps, but each message's clock is shifted forward by a fixed offset delta = deliver_after − started_at. So a message dripped at real time t is stamped send_time/deliver_after = t + delta and expires = t + delta + validity_hours. Because messages are inserted at drip_tps in real time, their per-message deliver_after values come out staggered at drip_tps starting at deliver_after — delivery is rate-limited rather than a thundering herd at the scheduled instant. With no deliver_after, delta = 0 and messages are dripped and delivered immediately.

Typical workflow

# 1. Upload a named recipient list with per-recipient variables (CSV or JSON)
curl -k -X POST https://smsc:8443/api/campaign_lists \
-H 'Content-Type: application/json' \
-d '{"name":"VIP customers",
"csv":"msisdn,first,plan\n12025550101,Alice,Gold\n12025550102,Bob,Silver\n"}'
# => {"id": 1, "recipient_count": 2, ...}

# 2. Create a campaign against that list, templating in the variables
curl -k -X POST https://smsc:8443/api/campaigns \
-H 'Content-Type: application/json' \
-d '{"name":"Renewal notice","message":"Hi {{ var1 }}, your {{ var2 }} plan renews soon.",
"source_msisdn":"12345","source_smsc":"IMS_SMSC",
"list_id":1,"audience":"active_only","drip_tps":50}'
# => {"id": 7, "status": "draft", ...}

# 3. Start it
curl -k -X POST https://smsc:8443/api/campaigns/control \
-H 'Content-Type: application/json' \
-d '{"campaign_id":7,"action":"start"}'

# 4. Poll progress / delivery stats
curl -k https://smsc:8443/api/campaigns/7
# => {... "stats": {"total":..,"delivered":..,"failed":..,"progress_percent":..}}

To message all active subscribers, omit list_id in step 2 (and skip step 1 entirely).

See also