Skip to main content
A campaign is a contact list plus the rules for dialling it: which agent runs, what hours it may ring people, how many lines it may use at once, how often to retry, and how much it is allowed to spend. You upload the list once and start it. After that the platform handles the dialling. It paces itself, waits through the night, retries calls that did not connect, skips numbers on your do-not-call list, pauses itself when the money runs out or when it has connected as many calls as you asked for, and gives you a per-contact report at the end. A campaign can also stay open. With 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.
A campaign is a dialler on top of the call API, so its calls are ordinary calls. They appear in GET /v2/calls, they emit the same call.* webhooks, and they debit the same wallet at the same per-minute rate.

The campaign object

Campaign money is in paise, call money is in rupees.budget_paise and spent_paise are integer paise (100 paise = ₹1), so a ₹500 cap is "budget_paise": 50000. The call object and the wallet use rupee fields (amount_inr, balance_inr). No object mixes the two units.

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 own timezone:
  1. today’s local date is between start_date and end_date, inclusive; and
  2. the local HH:MM falls inside at least one slots entry.
  • 1 to 4 slots. Fewer is a 400; five is a 400.
  • A slot may cross midnight. 21:00 to 06:00 is 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, so Asia/Kolkata’s half-hour offset and every daylight-saving boundary are handled for you.
When the date range runs out entirely, the campaign finishes as 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.
Set budget_paise on every campaign, even when you trust the list. It is the one control that limits the cost of a mistake in the list itself, such as a duplicated CSV or a column shifted by one.

Pause after connected calls

Set pause_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

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. Set always_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 PATCH while the campaign is draft or paused. On a running campaign the change is refused with 409 conflict, so pause it, patch it and resume it.
It is returned on the campaign object, on list pages, in the add-contacts response and as 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.
Give each contact its own id in an always-on campaign. A contact you add with an id, such as an order or cart id, is unique by that id: the same number comes back next week under a new id and is called again, and the same id sent twice counts in contacts_duplicate. A contact sent without an id is unique by phone for the whole life of the campaign, and so is every contact of a campaign that is not always-on. Two contacts on one number are never dialled at the same time. To call a contact again with the details it already has, use POST /v2/campaigns/{id}/contacts/{contact_id}/retry.

Example: one open campaign, numbers added as they come

Create the campaign once and start it:
Then add each new order as it is placed:
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
The response is the campaign object itself, with three extra top-level keys. It is not wrapped as {"campaign": …, "contacts": …}. Read the id at .id, and the upload result at .contacts_accepted / .contacts_duplicate / .contacts_rejected.

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 with 400:
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, so data.call.metadata in each call.* 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.
A value the agent needs and your system matches on, like a loan number, can go in both: 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 own call_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.sources says campaign for a value that came from the contact.
  • List contacts returns them, and a duplicated campaign keeps them.
In a CSV upload, the columns 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

Returns the standard paginated envelope of campaign objects, newest first. 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

While the campaign is 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.
Returns 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 in agent_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:
The next calls use the new version. Calls already in progress finish as they started. A contact whose own 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. [] or null clears it, and a PATCH without keyterms leaves 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 returns keyterm_spellings: the same terms in the script of the agent’s languages, added for you a short while after you save.
The agent’s own keyterms come from the revision the campaign uses. In the console, the list is Words to catch on the campaign form. Words to catch covers what to put on the list.

Add contacts

Up to 1000 rows per request. For a larger list, call it repeatedly. Each row takes the same fields as on create, 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
The response has the same shape as the create response, so one parser in your client handles both routes that add contacts. 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

Puts one contact back in line, even one that has used all its retries: 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

Takes a contact off the list before its first call, for example a number added by mistake. Only a contact that has never been dialled can be removed: status 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

Stops calling one contact that is waiting: one not called yet, or one waiting for its next retry after a call nobody picked up. Use it when the reason for the call goes away between attempts. A shopper completes the order you were going to remind them about, a customer pays the bill, a lead books on your site. The contact becomes 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).
200 OK
The answer is the contact, in the same shape as List contacts. Cancelling a contact that is already 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

Allowed from 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
The campaign starts, dials what the balance covers, and then pauses itself with 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
Nothing was dialled, and the campaign keeps its status. Move end_date out and start it again:

Pause a campaign

202 Accepted
Takes effect immediately, including while the campaign is asleep waiting for tomorrow’s window. Calls already live are not hung up. Pause stops new calls and lets current conversations finish. If you need a live call to end now, use 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
Stopping is terminal and cannot be undone. Live calls are hung up, pending contacts are marked 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

The campaign’s log: every start, pause, resume, stop and completion, and every change to its connected-call limit, with when it happened, who did it and why. It is how you tell a pause someone pressed from one the campaign made on its own.
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.
Without the header, 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

The CSV is streamed rather than buffered, so it is safe on a six-figure campaign. Join last_call_id against GET /v2/calls for durations and costs.

Webhooks

If you set webhook_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
Campaign events use the same envelope as call events ({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.
Alert on campaign.paused with a pause_reason. That event means a campaign has stopped spending money for a reason you can fix in one API call.

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 no events_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

  1. Scrub first. Push known opt-outs to POST /v2/dnc before 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.
  2. Keep the window legal. 09:00 to 21:00 IST is the outer bound for commercial calls in India, and most enterprise programmes run 10:00 to 19:00. See calling rules.
  3. Budget every campaign. budget_paise is the cheapest insurance there is against a bad CSV.
  4. Retry once, wait fifteen minutes. retry_count: 1 with re_attempt_period_secs: 900 works for most Indian mobile lists. Retrying five times in five minutes annoys people and does not connect.
  5. Start small. Begin with ten contacts, one slot and max_concurrent: 1, and read the transcripts. Then upload the rest into the same campaign.
  6. Watch exhausted and suppressed in the report. They are the two numbers that tell you about your list rather than your agent.