https://sandbox.voice.miraiminds.co.
The campaign object
Lifecycle
Only
draft and paused can be edited or started. stopped and completed
never dial again — and a campaign in either refuses new contacts with 409,
rather than accepting rows that would never ring.
Pause reasons
pause_reason distinguishes “the operator stopped this” from “we stopped it for
them”. A pause you asked for carries no reason (null); the two reasons
below are set by the money gate.
Being outside the calling window is not a pause. The campaign stays
play
and sleeps until the next opening edge — a durable sleep, so a campaign
scheduled for 10:00 tomorrow costs nothing overnight and survives our restarts.
An offline fleet is not a pause either: it is a 503 fleet_offline refusal at
start time.Contact statuses
Every row you upload carries its own status. The first block mirrors the call status vocabulary, so a campaign report reads the same way the call list does; the last three exist only inside campaigns.exhausted is the one to watch: it is the honest count of “we tried everything
you paid for and never reached this person”. A contact that ends no_answer
still had attempts left; one that ends exhausted did not.
Calling window
Three things together decide whether the dialler may ring right now, all in the campaign’s owntimezone:
- today’s local date is between
start_dateandend_date, inclusive; and - the local
HH:MMfalls inside at least oneslotsentry.
- 1 to 4 slots. Fewer is a
400; five is a400. - A slot may cross midnight.
21:00–06:00is open on both sides of it. Legal, and almost never what you want in India — see calling rules. - A zero-length slot is a
400.{"start":"10:00","end":"10:00"}is rejected rather than read as “all day”: guessing wrong there means ringing somebody at 03:00. - Local means local. Times are stored and evaluated in
timezone, not UTC, soAsia/Kolkata’s half-hour offset and every daylight-saving boundary are handled for you. This is the difference between a campaign that respects the window and one that respects it except twice a year.
completed —
even with contacts still pending. A window that never opens again cannot
dial them.
Retries
Only outcomes that never reached a human are retried:
no_answer, busy and
failed. completed, voicemail and timeout all produced live media — the
call happened, and re-dialling somebody who already spoke to us is worse than
not calling at all.
A retry that lands outside the calling window simply waits: eligibility is a
lower bound, not a schedule.
Budget and pacing
max_concurrent (1–50) is this campaign’s own ceiling on live calls. It is
independent of — and additionally bounded by — your workspace
concurrency and queue-depth limits. Two campaigns at
max_concurrent: 20 on a workspace provisioned for 5 concurrent calls will
share those 5.
budget_paise caps the spend of this campaign alone. When spent_paise
reaches it, the campaign pauses with budget_exhausted — contacts keep their
place, so raising the budget and resuming continues where it stood.
The wallet is checked before every dialling tick. If it cannot cover another
minute at the campaign’s tier, the campaign pauses with insufficient_balance.
POST /start also does a wallet estimate up front and
tells you how far your balance goes, in funded_calls.
Create a campaign
Request
201 Created
Contacts are reported row by row, not rejected as a batch
A 900-row upload with two bad numbers is not an all-or-nothing400. The
good rows are stored and the bad ones come back by index:
201 Created — two rows rejected, one duplicate
Two things do fail the whole request: more than 1000 rows
(
400 invalid_request), and a malformed body. Everything else is a row report.
variables follow the same rules as call variables — a
flat string map, 32 keys, 512 characters per value — and fill the same
{{placeholders}} in the agent’s prompt. id is your own key for the row; omit
it and we mint one.
Errors
List campaigns
counters is omitted on list pages — fetch one campaign
to get them.
Get a campaign
200 OK
counters comes from the contact table, not from the running dialler, so a
finished campaign answers exactly like a live one. That is what makes this
endpoint safe to build a dashboard on.
404 not_found for an unknown id and for one in another workspace — the two are
deliberately indistinguishable.
Update a campaign
draft or paused only. Anything else is 409 conflict:
changing the slots, the retry policy or the budget under a live dialler would
mean the campaign’s own report describes settings that were never in force for
half of its calls. Pause it, patch it, resume it.
Patchable: name, slots, start_date, end_date, max_concurrent,
retry_count, re_attempt_period_secs, max_duration_secs, budget_paise,
webhook_url. Not patchable: agent_id, tier, timezone.
200 OK with the campaign object.
Add contacts
200 OK
409 conflict if the campaign is stopped or completed — nothing would ever
dial those rows, and accepting them silently is worse than refusing them.
List contacts
200 OK
last_call_id is an ordinary call_ id: fetch it from
GET /v2/calls/{id} for the full outcome.
Start a campaign
draft and from paused — starting a paused campaign is how you
resume one whose dialler is no longer running (after a top-up, say), because
start re-runs the fleet and wallet preflight that a bare
resume skips.
202 Accepted
202 means the dialler is running — not that a phone is ringing. Outside the
calling window it will sit and wait, correctly, in play.
funded_calls: an underfunded campaign still starts
If your wallet cannot cover every remaining contact, the response carries a
warning rather than a refusal:
202 Accepted — partially funded
pause_reason: "insufficient_balance". Top up and start it again to continue.
funded_calls is a one-minute-per-call estimate at this campaign’s tier — a
floor on how far you get, not a promise.
Errors
Pause a campaign
202 Accepted
POST /v2/calls/{id}/abort if you really need a live
call to end now.
Only a play campaign can be paused; anything else is 409 conflict.
Resume a campaign
202 Accepted
409 conflict if the campaign is not paused — and also if it is paused but
has no running dialler, in which case the message tells you to use
POST /start instead. That is deliberate: a bare resume
would skip the wallet and fleet preflight.
Stop a campaign
202 Accepted
aborted, and the campaign will never dial again. Legal from draft,
play and paused.
If you might want to continue later, pause instead.
Campaign report
JSON — the aggregate
200 OK
connected / dialed is your answer rate. attempts / dialed is whether
retry_count is earning its place.
CSV — per contact
last_call_id against GET /v2/calls for durations and
costs.
Webhooks
Setwebhook_url on the campaign and you receive both its lifecycle events
and every child call’s call.* events, signed with
the same workspace secret and verified by the same code path.
POST https://example.com/mirai/webhook
{id, type, created_at, data} — with
data.campaign where data.call would be. Verification, retries and replay
protection are identical: see Webhooks.
A campaign that behaves
- Scrub first. Push known opt-outs to
POST /v2/dncbefore you upload the list. Suppression is checked at dial time, so late additions still work — but a number dialled at 10:00 cannot be un-dialled at 10:05. - Keep the window legal.
09:00–21:00IST is the outer bound for commercial calls in India, and most enterprise programmes run10:00–19:00. See calling rules. - Budget every campaign.
budget_paiseis the cheapest insurance against a bad CSV there is. - Retry once, wait fifteen minutes.
retry_count: 1,re_attempt_period_secs: 900is the shape that works for most Indian mobile lists. Retrying five times in five minutes annoys people and does not connect. - Start small. Ten contacts, one slot,
max_concurrent: 1. Read the transcripts. Then upload the rest into the same campaign. - Watch
exhaustedandsuppressedin the report. They are the two numbers that tell you about your list rather than about your agent.