Skip to main content

Campaigns

A campaign is the run control for an outbound flow. The flow holds the configuration; these three endpoints start it, stop it, and report what it is doing.

PUBLIC_BASE_URL is required

Starting a campaign fails immediately without it — the provider has to be able to reach your answer and hangup webhooks, and every call would otherwise connect to silence.

{ "detail": "PUBLIC_BASE_URL is not set, so telephony cannot reach this server to run a campaign" }

Start a campaign​

POST /api/voice/flows/{uuid}/campaign/start

Marks the flow active and starts the runner in the background.

202 Accepted:

{ "status": "running" }
StatusCause
400PUBLIC_BASE_URL is not set
404No outbound flow with this UUID in your organization
409{"error": "this campaign is already running"}

The 409 is deliberate: two runners over one contact list would call half of it twice, and the customer has no way to tell it was a bug.

Confirm the runner started

The 202 acknowledges that the flow was marked active and the runner was launched. Telephony credentials are validated by the runner itself, so check campaign status afterwards — status: "active" together with running: true confirms it is dialling. A connected Plivo integration with a valid E.164 number is required.

Stop a campaign​

POST /api/voice/flows/{uuid}/campaign/stop

{ "status": "paused", "was_running": true }

was_running is false when no runner was live in this process — after a restart, for instance. The flow is marked paused either way, which is what makes a campaign stoppable after the process that started it has gone.

Cancellation is immediate: a runner waiting for its next dialling slot stops now rather than when the timer next fires.

Campaign status​

GET /api/voice/flows/{uuid}/campaign/status

{
"status": "active",
"running": true,
"contact_tags": ["winback", "north"],
"time_window": { "from": "09:00", "to": "18:00" },
"max_retries": 2,
"calls_per_minute": 10
}
FieldMeaning
statusThe flow's stored status
runningWhether a runner is alive in this process

The two can disagree. A process restart loses in-memory runners while the flow stays active — shown as active + running: false, which is a campaign that needs restarting rather than one quietly making progress.

How the runner works​

For each contact, in id order:

  1. Stop if the flow is no longer active — checked every contact, so a pause takes effect within a call or two rather than at the end of the list
  2. Stop if outside the calling window
  3. Skip if already handled — see the rules below
  4. Write the call row, then dial
  5. Wait 60 / calls_per_minute seconds

The call row is written before the provider is called, so a call that fails at dial time still leaves a record. A campaign with no record of its failures cannot be diagnosed.

Who gets skipped​

ConditionReason
No phone numberNot callable
Last outcome is not a no-answerAlready reached — whatever they said
Attempts exceed max_retriesEnough
Last call is within retry_interval_minutesToo soon; same number twice in a minute reads as a robocall

Only these outcomes are treated as "did not pick up" and therefore retryable: no_answer, busy, failed, customer_no_answer.

A contact whose last call carries any other outcome — including the initiated value a campaign call is created with — is treated as already reached and is not dialled again. The server log names the reason for each skip:

[campaign 3] skipping Priya Kumar: already reached (initiated)

Call history​

The runner loads every prior outcome for the flow before dialling anything. If that query fails it aborts rather than continuing — calling everyone who was already reached is the one mistake a campaign must never make.

Calling windows​

{ "from": "09:00", "to": "18:00" }

24-hour local time. A window where from is later than to wraps midnight and is treated as two ranges. An unparseable window allows calling rather than blocking it — a campaign silently dialling nobody because of a typo is worse than one that runs when it should not.

Outside the window the run stops rather than sleeping until the window reopens. Start it again when the window is open.

Set TZ on the service

The window is evaluated against the server's local time; there is no per-organization timezone. On a UTC server a 09:00–18:00 window means 14:30–23:30 IST. Set TZ on the process to the timezone your windows are written in.

Run lifecycle​

  • A start makes one pass. The runner works to the bottom of the contact list and finishes. Contacts skipped because their retry interval had not elapsed are picked up the next time the campaign is started.
  • start_date and end_date are stored on the flow and returned by the API for your own scheduling; starting a campaign dials immediately regardless of them.
  • Runners live in the process that started them. After a restart, a campaign shows status: "active" with running: false and should be started again.