always_on it keeps
running after its list is done, and you add numbers whenever your system has
them.
Base URL https://sandbox.voice.miraiminds.co.
The campaign object
Lifecycle
Only
draft and paused campaigns can be edited or started. stopped and
completed campaigns never dial again, and they refuse new contacts with 409
because those rows would never ring.
Pause reasons
pause_reason tells you whether the operator paused the campaign or we paused
it for them. A pause you asked for has no reason (null), or the reason you
sent with it. The money gate sets insufficient_balance and
budget_exhausted, your connected-call limit sets connected_limit_reached,
and a number or pool that cannot call sets calls_from_unavailable.
Every pause, and every start, resume, stop and change to the connected-call
limit, is recorded in the campaign’s history, with who did
it.
Being outside the calling window is not a pause. The campaign stays
play
and sleeps until the window next opens. The sleep is durable, 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 has its own status. The first block mirrors the call status vocabulary, so a campaign report reads the same way the call list does. The rest exist only inside campaigns.
Watch
exhausted. It counts the people we tried with every attempt you paid
for and never reached. A contact that ends no_answer still had attempts left;
one that ends exhausted did not.
A contact waiting for its next retry is pending again, with attempts above
zero and eligible_at set to when the retry may ring. To stop that retry, for
example because the customer already did what the call was for, cancel the
contact.
Calling window
Two checks decide whether the dialler may ring right now, both 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:00to06:00is open on both sides of it. The API accepts this, but it is almost never what you want in India. See calling rules. - A zero-length slot is a
400. We reject{"start":"10:00","end":"10:00"}instead of reading it as “all day”, because guessing wrong there means ringing somebody at 03:00. - Times are local. We store and evaluate them in
timezone, not UTC, soAsia/Kolkata’s half-hour offset and every daylight-saving boundary are handled for you.
completed,
even if some contacts are still pending, because no window remains to dial
them in.
A campaign whose window has already closed for good cannot be started.
POST /start and POST /resume
refuse it with 409 window_closed, instead of accepting it and finishing it
as completed a second later with no calls.The usual cause is start_date and end_date both set to today, with a slot
that has already ended. Extend end_date, or add a slot that is still ahead,
with PATCH. Then start it again.A campaign started outside its window that opens again later is still
accepted. It stays play and sleeps until the window opens.Retries
Only outcomes that never reached a human are retried:
no_answer, busy and
failed. completed, voicemail and timeout all produced live media, which
means the call happened. Re-dialling somebody who already spoke to us is worse
than not calling at all.
A retry that comes due outside the calling window waits for the window to open.
The retry wait is a minimum, and the actual retry can come later.
Budget and pacing
max_concurrent (1 to 50) is this campaign’s own ceiling on live calls. It
is separate from your workspace
concurrency and queue-depth limits, which also apply. 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 if you raise the budget and resume, the campaign continues where it
stopped.
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.
Pause after connected calls
Setpause_after_connected and the campaign pauses itself once that many calls
have connected. Resume it when you want the next batch. Use it to keep a long
campaign in step with the people who act on its calls: run a list for a month,
pause after every 50 conversations, and resume when your team has followed up.
What counts as connected. A contact whose call ended completed or
timeout, which means a person answered and talked. voicemail, no_answer,
busy, failed and skipped don’t count. This is narrower than the report’s
connected, which also counts voicemail.
When the count starts. Counting begins when you turn the limit on, from
null to a number. It starts again from zero each time you resume or start the
campaign after the limit paused it. Other pauses (yours, the wallet, the budget)
keep the count where it was. Changing the number keeps the count, so with 30
counted, moving the limit from 50 to 80 means 50 more calls. Setting it back to
null removes the limit and its count.
How it lands on the number. Near the limit, the campaign rings only as many
people as it still needs, so it stops on the number you set. Calls already in
progress always finish, because a pause never hangs up on anyone. If you lower
the limit below the current count, the campaign pauses at its next check.
When it pauses, status is paused, pause_reason is
connected_limit_reached, and you get the usual
campaign.paused webhook. The pause is in the campaign’s
history, with the limit and the count.
Example: pause a running campaign every 50 conversations
- cURL
- Python
- Node.js
200 OK — the PATCH answer, trimmed
GET /v2/campaigns/{id} shows pause_after_connected_count climbing. To act
the moment a batch is done, listen for campaign.paused with
data.campaign.pause_reason set to connected_limit_reached.
Always-on campaigns
An ordinary campaign completes once every contact has reached a final state, and from then on it refuses new contacts. Setalways_on: true and the
campaign stays open instead. When it has called everyone on its list it stays
play and waits. Contacts you add later are called under the same rules as
the first ones: inside the calling window, within max_concurrent, and retried
up to retry_count times.
Use it to run a queue. Keep one open campaign for an agent and add each number
as your system produces it, such as a new order or a new lead, instead of
placing single calls. Every number then gets calling hours, pacing and retries,
which POST /v2/calls does not do.
You can set always_on:
- when you create the campaign, or on a copy made with
POST /v2/campaigns/{id}/duplicate, which keeps the source’s value unless you send another; - with
PATCHwhile the campaign isdraftorpaused. On a running campaign the change is refused with409 conflict, so pause it, patch it and resume it.
data.campaign.always_on on every
campaign webhook.
An always-on campaign ends when you stop it, or when its
end_date is behind it. Then it completes and sends campaign.completed as
usual. Set end_date as far ahead as you want the campaign to stay open. To
move it later, pause the campaign, PATCH the new end_date and resume.
Running out of contacts never completes it and sends no event.
The money rules are the same as for any campaign. The wallet check pauses it
with insufficient_balance, and budget_paise pauses it with
budget_exhausted. On an open campaign the budget counts everything it has
ever spent, so treat it as a ceiling you raise from time to time, or leave it
unset and rely on the wallet.
A contact added while the campaign is waiting is called straight away when the
calling window is open, and when the window next opens otherwise. If that
wake-up is ever lost, the campaign looks at its list again within five minutes.
Example: one open campaign, numbers added as they come
Create the campaign once and start it:- cURL
- Python
- Node.js
200 OK
contacts_accepted: 1 means the number is in line to be called, unless it is
on your do-not-call list when its turn comes. Each call sends the
ordinary call.* webhooks to the campaign’s webhook_url with order_id in
data.call.metadata, and GET /v2/campaigns/{id}/contacts
shows where every number is, attempts included.
Create a campaign
Request
201 Created
Contacts are reported row by row, not rejected as a batch
If a 900-row upload has two bad numbers, we store the good rows and return the bad ones by index instead of failing the whole upload with400:
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. Every other problem is reported
per row.
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. If
you omit it, we mint one.
Your own fields: metadata
Most lists carry more than the agent needs: a CRM lead id, a branch, where the
lead came from. Put those in metadata, not variables:
- The agent never sees it. It is not a variable and is not in the prompt.
- It stays with the contact: List contacts returns it.
- Every call to that contact carries it as the call’s
metadata, sodata.call.metadatain eachcall.*webhook tells you which of your records the call was for. - It follows the same rules as call metadata: up to 16 keys, string values of up to 256 characters, 4 KB in total. A row that breaks them is rejected by index like any other bad row.
variables for the agent and metadata for your webhooks. In the
console, a CSV column set to Extra info is sent as metadata, and Both
sends it in each. See
Campaigns in the console.
If the agent declares its variables (
input_schema), a variable it does not
declare rejects the row. Send those extra columns as metadata instead.Voice and language per contact
Give a contact its owncall_settings, and its calls use that voice and those
languages in place of the agent’s. People on one list can hear a different
voice, or be understood in a different language.
- The keys and rules are the same as on
POST /v2/calls. A key you leave out keeps the agent’s value. - A contact with a bad value is rejected on its own row in
contacts_rejected, with the reason, and the rest of the list goes in. - We check the values again when the contact is dialled. If a key no longer passes, for example because a voice left the catalogue after you uploaded the list, that call keeps the agent’s value for it.
- A pre-call tool on the agent can still change them call by call. Each call’s
call_settings.sourcessayscampaignfor a value that came from the contact. - List contacts returns them, and a duplicated campaign keeps them.
call_settings.voice_id, call_settings.language
and call_settings.secondary_languages set the same keys. Separate secondary
languages with commas, for example hi,en. An existing language column stays a
variable.
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 and you can safely build a
dashboard on this endpoint.
404 not_found for an unknown id and for one in another workspace. The two
responses are identical on purpose.
Update a campaign
draft or paused you can change: name, slots,
start_date, end_date, max_concurrent, retry_count,
re_attempt_period_secs, max_duration_secs, budget_paise, webhook_url,
skip_called_before, final_results, always_on,
pause_after_connected and keyterms. Not patchable: agent_id, tier,
timezone.
While it is running, only the pacing, the agent’s version, the connected-call
limit and the keyterms can change: max_concurrent, slots, agent_revision,
pause_after_connected and keyterms. They take effect straight away. Anything else
returns 409 conflict, because changing the retry policy or the budget under a
live dialler would leave the campaign’s own report describing settings that
were never in force for half of its calls. Pause it, patch it, then resume it.
A finished campaign can’t be edited.
200 OK with the campaign object. A change to pause_after_connected
is recorded in the campaign’s history.
Switch to the agent’s latest version
A campaign keeps the version of its agent it was created with. If you change the agent later (a new voice, a new prompt), the campaign’s calls don’t pick that up on their own. The campaign object shows the version it uses inagent_revision, and GET /v2/agents/{id} shows the agent’s current
revision.
To move the campaign onto the new version, send that revision. This works
while the campaign is running, so you don’t need to pause it:
call_settings
name a voice keeps that voice. You can also send an older revision to go back.
In the console, the campaign page shows Use the latest version when the
agent has changed.
Words to catch
keyterms are the names your contacts may say that a recogniser tends to
mishear, such as a product, an offer or a city. A campaign’s calls listen for
its own keyterms first, then its agent’s, at most 200 in all. You can change
them while the campaign is running, without pausing it:
- The list you send replaces the saved one.
[]ornullclears it, and aPATCHwithoutkeytermsleaves it alone. - The next call uses the new list. Calls already in progress keep theirs.
- Up to 100 terms, each 1 to 50 characters and at most 5 words, with no comma.
A list that breaks a rule is refused with
400 invalid_request, and the message names the term. GET /v2/campaigns/{id}also returnskeyterm_spellings: the same terms in the script of the agent’s languages, added for you a short while after you save.
Add contacts
metadata and
call_settings included. A
draft, play or paused campaign accepts new contacts. When a running
campaign had nothing left to dial, the add wakes it, so the new contacts are
called straight away if the calling window is open.
200 OK
409 conflict if the campaign is stopped or completed. Nothing would ever
dial those rows, so we refuse them instead of accepting them silently.
The status check runs in the same database transaction as the insert, under a
lock the campaign also takes when it completes. A campaign that completes at
the moment you add contacts therefore does one of two things: it counts your
new contacts and calls them, or it completes first and your request gets
409 conflict with nothing stored. A 200 never leaves contacts on a campaign
that has finished.
List contacts
200 OK
last_call_id is an ordinary call_ id: fetch it from
GET /v2/calls/{id} for the full outcome. A contact
without metadata has no metadata field, and
one without call_settings has no
call_settings field.
Call a contact again
completed, voicemail, no_answer, busy, timeout, failed,
exhausted or cancelled. It becomes pending, eligible now, and keeps
attempts, last_call_id and last_status. attempts goes up by one when
the new call is placed. On a running campaign it rings as soon as the calling
window and pacing allow, and the call spends credit.
Answers 200 with the contact, in the same shape as
List contacts. Allowed while the campaign is draft,
paused or play.
Remove a contact
pending, attempts 0, not being called. Answers 204 with no body, and the
campaign’s counters no longer include it. The number can be added again.
Once a contact has a call it is part of the campaign’s record and stays. To
stop its next retry, cancel it. To stop every future call
to a number, add it to the do-not-call list.
Cancel a contact
cancelled. It is not called again, and the campaign no
longer waits for it, so a campaign that is not
always-on can complete without it. Its attempts,
last_call_id and last_status stay, and it stays in
List contacts and the report. Allowed
while the campaign is draft, paused or play.
A call that is already ringing or in conversation is never cut off. The
cancel answers 409 contact_in_call. Send it again once the call ends: if the
call reached nobody and a retry is booked, the retry is cancelled. To end a
live call, use POST /v2/calls/{id}/abort with that
call’s id (data.call.id in its webhooks).
- cURL
- Python
- Node.js
200 OK
cancelled answers 200 with it again,
so a repeated request (a webhook delivered twice, a retried job) is safe. To
call a cancelled contact after all, call it again.
Percent-encode
contact_id in the path when your ids contain /, spaces or
other reserved characters.
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, for example),
because start re-runs the fleet and wallet preflight that a bare
resume skips. Starting a campaign that the
connected-call limit paused starts its count
again from zero, as a resume does.
202 Accepted
202 means the dialler is running. It does not mean a phone is ringing yet:
outside the calling window, the campaign waits in play, which is expected.
If the window can never open again, you get
409 window_closed instead.
funded_calls: an underfunded campaign still starts
If your wallet cannot cover every remaining contact, the campaign still starts
and the response carries a warning:
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. Treat
it as a floor on how far you get, though not a guaranteed one.
Errors
Here is
window_closed for a campaign whose start_date and end_date are
both today, started after its only slot ended at 17:00:
409 Conflict
end_date out and
start it again:
Pause a campaign
202 Accepted
POST /v2/calls/{id}/abort.
Only a play campaign can be paused; anything else is 409 conflict.
The body is optional. Send reason and by to say why and who, and the
campaign’s pause_reason reads <by>: <reason> (at most 300 characters
together):
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 that case the message tells you to use
POST /start instead, because a bare resume would skip the
wallet and fleet preflight.
409 window_closed if the calling window can never open
again, exactly as on start. The campaign stays paused. Extend end_date or
widen slots with PATCH, then resume.
Resuming a campaign paused with connected_limit_reached starts the
connected-call count again from zero, so it
calls the next batch and pauses again. To call more before the next pause,
raise pause_after_connected first. To stop pausing, set it to null.
Stop a campaign
202 Accepted
aborted, and the campaign will never dial again. Allowed
from draft, play and paused.
If you might want to continue later, pause instead.
Campaign history
200 OK
The history starts on the day this endpoint shipped. Older campaigns have no
entries from before it.
404 not_found for an unknown id and for one in another
workspace.
Who did it: Mirai-Actor
Add a Mirai-Actor header to any campaign write (create, PATCH, start,
pause, resume, stop) to name the person or system behind it. It is up to 120
characters and appears as by in the history.
by is the by you sent in a pause body,
then your API key’s label, then API. Changes made in the console carry your
workspace’s name.
Campaign report
JSON — the aggregate
200 OK
connected / dialed is your answer rate. attempts / dialed tells you whether
retry_count is paying off.
CSV — per contact
last_call_id against GET /v2/calls for durations and
costs.
Webhooks
If you setwebhook_url on the campaign, you receive both its lifecycle events
and every child call’s call.* events. Both are
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. data.campaign.always_on is on every campaign event.
Every call.* event of a campaign call names the campaign and the contact in
data.call.campaign_id and data.call.contact_id, and carries the contact’s
metadata in data.call.metadata. With
final_results on (the default), each call also ends with one
call.processed that has the transcript and the
after-call results, so you don’t need to fetch anything. An agent with
emit_processed_webhook: false sends no call.processed, whatever the
campaign says. Verification, retries and replay protection are identical; see
Webhooks.
Follow a campaign call live
Each campaign call has a live event stream, like any other call: the agent’s state and each turn while the call runs. A campaign call has no create response, so it has noevents_url. Open its stream from your backend
with your API key. Take the call id from data.call.id on its call.started
webhook, or from GET /v2/calls.
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:00to21:00IST is the outer bound for commercial calls in India, and most enterprise programmes run10:00to19:00. See calling rules. - Budget every campaign.
budget_paiseis the cheapest insurance there is against a bad CSV. - Retry once, wait fifteen minutes.
retry_count: 1withre_attempt_period_secs: 900works for most Indian mobile lists. Retrying five times in five minutes annoys people and does not connect. - Start small. Begin with ten contacts, one slot and
max_concurrent: 1, and 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 your agent.