Skip to content
Discord

Calls

A call is one outbound dial attempt driven by an agent. Creating a call is asynchronous: you get 202 Accepted immediately and the outcome arrives by webhook (or by polling GET /v2/calls/{id}).

Base URL https://api.voice.miraiminds.co.

{
  "id": "call_01JZQ9B4M8N3P6R2S5T7V9W1YA",
  "object": "call",
  "agent_id": "agt_01JZQ8F3K7M2N5P9R4T6V8W0XZ",
  "to": "+919876543210",
  "status": "completed",
  "ended_reason": "assistant-ended-call",
  "started_at": "2026-07-26T09:15:02Z",
  "ended_at": "2026-07-26T09:16:38Z",
  "duration_secs": 96,
  "cost": { "amount_inr": 2, "per_min_inr": 1 }
}
FieldTypeDescription
idstringcall_<ulid>. Also the idempotency anchor and the workflow ID.
agent_idstringThe agent this call ran.
tostringE.164 destination.
statusenumSee statuses.
ended_reasonstring | nullWhy it ended. See ended reasons. null while running.
started_atstring | nullRFC 3339, UTC. Set when media goes live, not when we dialled. null if the call never connected.
ended_atstring | nullRFC 3339, UTC.
duration_secsintegerBillable seconds of live media. 0 for calls that never connected.
costobjectamount_inr charged, and the per_min_inr rate applied. Zero until the call ends.
StatusTerminalMeaning
queuedAccepted, waiting on fleet capacity.
dialingSIP invite sent, phone is ringing.
in_progressMedia live, conversation running.
completedConnected and finished normally. Billable.
voicemailAn answering machine picked up. See voicemail. Billable.
no_answerRang out, nobody picked up. Free.
busyCallee’s line was busy. Free.
failedCould not connect, or the pipeline errored. Free.
timeoutHit max_duration_secs. Billable.
abortedYou cancelled it with POST /abort — from the queue, mid-ring, or mid-conversation. Free either way.

Statuses only move forward. A terminal status never changes.

stateDiagram-v2
    [*] --> queued: POST /v2/calls
    queued --> dialing: capacity acquired
    queued --> aborted: POST /abort
    dialing --> aborted: POST /abort
    in_progress --> aborted: POST /abort
    dialing --> in_progress: media live
    dialing --> no_answer
    dialing --> busy
    dialing --> failed
    dialing --> voicemail: machine detected
    in_progress --> completed
    in_progress --> voicemail
    in_progress --> timeout: max_duration_secs
    in_progress --> failed
    completed --> [*]
    voicemail --> [*]
    no_answer --> [*]
    busy --> [*]
    failed --> [*]
    timeout --> [*]
    aborted --> [*]

ended_reason explains why a terminal status was reached. Match on status for control flow; use ended_reason for analytics and support.

ended_reasonUsual statusMeaning
assistant-ended-callcompletedThe agent called end_call — the job finished.
customer-ended-callcompletedThe callee hung up.
exceeded-max-durationtimeoutHit max_duration_secs.
customer-did-not-answerno_answerRang out.
customer-busybusyLine busy.
voicemailvoicemailAn answering machine answered. Detecting it is what sets the status, so the two always travel together.
silence-timed-outcompletedMedia was live but nobody ever spoke. Billable — the agent ran.
idle-timed-outcompletedThe agent re-prompted its configured number of times and the caller never came back. Billable.
no-mediafailedThe call was placed but audio never flowed. See the note below. Not billed.
aborted-by-apiabortedYou called POST /abort. The only reason an abort produces.
assistant-errorfailedPipeline failure on our side. Not billed.
transferredcompletedReserved for a completed human handoff on t3/t5. Not wired — no call emits this yet. See Node graphs.
balance-exhaustedcompletedReserved for a call ended when the wallet emptied mid-call. Not wired — no call emits this yet.

POST /v2/calls
FieldTypeRequiredDescription
agent_idstringyesThe agent to run.
tostringyesE.164, e.g. +919876543210.
variablesobjectnoFlat string map substituted into the agent’s prompt. See variables.
webhook_urlstringnoHTTPS URL for lifecycle events.
max_duration_secsintegernoOverrides the agent’s cap for this call only.
tierstringnot1 | t3 | t5. Defaults to your key’s tier. See tiers.
curl -X POST https://api.voice.miraiminds.co/v2/calls \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-8842-confirm-1" \
  -d '{
    "agent_id": "agt_01JZQ8F3K7M2N5P9R4T6V8W0XZ",
    "to": "+919876543210",
    "variables": { "customer_name": "Rahul", "order_id": "8842" },
    "webhook_url": "https://example.com/mirai/webhook",
    "max_duration_secs": 240
  }'
202 Accepted
{ "id": "call_01JZQ9B4M8N3P6R2S5T7V9W1YA", "status": "queued" }

202 means the call is admitted to the queue — the phone has not rung yet. The full call object is available from GET /v2/calls/{id}.

Statuserror.codeCause
400invalid_requestMissing agent_id/to, to not E.164, max_duration_secs out of range
401unauthorizedBad or missing key
402insufficient_balanceWallet cannot cover the first minute at your tier. Checked before we dial — no partial charge. See Wallet.
404not_foundagent_id does not exist in your workspace — including one that exists in someone else’s
409duplicate_callA request with this Idempotency-Key is still being processed. See idempotency.
429rate_limitedRequest rate exceeded, or your queue is full (500 calls accepted and not yet ended). Concurrency does not 429 — over-cap calls queue. error.message says which. Honour Retry-After. See Limits.
501tier_unavailabletier is t3 or t5, which are not live yet

variables is a flat map of string keys to string values. Each key replaces the matching {{key}} placeholder in the agent’s system_prompt and first_message.

{ "variables": { "customer_name": "Rahul", "order_id": "8842" } }
  • Keys are matched literally and case-sensitively.
  • A placeholder with no matching variable is substituted with an empty string — it does not error, and it does not leave {{braces}} for the agent to read aloud. Validate on your side.
  • Keep values short. They are part of the prompt, and the prompt is on the latency path.
  • Variables are static per call. There is no way to change them mid-call.

Send Idempotency-Key with a value derived from your own domain object:

-H "Idempotency-Key: order-8842-confirm-1"

If we have seen that key in the last 24 hours, we replay the original response byte for byte and place no second call. This is the safe way to retry a request that timed out — you cannot tell from a network timeout whether the call was placed, and without the key a retry means the customer’s phone rings twice.

The key is scoped to your workspace. Reusing a key with a different body still replays the original response — pick keys that are unique per intended call.

There is one window where a reused key does not replay: while the first request with that key is still being processed, there is no stored response to replay yet, so the second request gets 409 duplicate_call. It means “already in flight, no second call placed” — retry a moment later and you will get the original response, or wait for the webhook.


GET /v2/calls/{id}
curl https://api.voice.miraiminds.co/v2/calls/call_01JZQ9B4M8N3P6R2S5T7V9W1YA \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"

200 OK — the call object. 404 not_found for an unknown ID, and for an ID that belongs to another workspace — the two are deliberately indistinguishable.


POST /v2/calls/{id}/abort

Cancels a call that has not ended yet. Accepted while the call is queued, dialing or in_progress — a live call is hung up on the SIP server mid-conversation, not merely flagged. Only a call that has already reached a terminal status is refused: there is nothing left to stop.

curl -X POST https://api.voice.miraiminds.co/v2/calls/call_01JZQ9B4M8N3P6R2S5T7V9W1YA/abort \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
202 Accepted
{ "id": "call_01JZQ9B4M8N3P6R2S5T7V9W1YA", "status": "aborted" }

202 means the cancel was signalled. The call reaches aborted shortly after with ended_reason: "aborted-by-api", and a call.aborted event fires. An aborted call is never billed — including one aborted after media went live.

Statuserror.codeCause
404not_foundUnknown call ID, or a call in another workspace
409conflictThe call has already ended — its status is terminal, so there is nothing to abort

GET /v2/calls?agent_id=&status=&from=&to=&limit=&cursor=
Query paramDescription
agent_idOnly calls run by this agent.
statusFilter by one status.
fromRFC 3339 lower bound on created_at, inclusive.
toRFC 3339 upper bound on created_at, exclusive.
limit1–100, default 20.
cursorFrom next_cursor of the previous page.
curl -G https://api.voice.miraiminds.co/v2/calls \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  --data-urlencode "agent_id=agt_01JZQ8F3K7M2N5P9R4T6V8W0XZ" \
  --data-urlencode "status=completed" \
  --data-urlencode "from=2026-07-01T00:00:00Z" \
  --data-urlencode "limit=100"
200 OK
{
  "data": [
    {
      "id": "call_01JZQ9B4M8N3P6R2S5T7V9W1YA",
      "object": "call",
      "agent_id": "agt_01JZQ8F3K7M2N5P9R4T6V8W0XZ",
      "to": "+919876543210",
      "status": "completed",
      "ended_reason": "assistant-ended-call",
      "started_at": "2026-07-26T09:15:02Z",
      "ended_at": "2026-07-26T09:16:38Z",
      "duration_secs": 96,
      "cost": { "amount_inr": 2, "per_min_inr": 1 }
    }
  ],
  "has_more": true,
  "next_cursor": "call_01JZQ9B4M8N3P6R2S5T7V9W1YB"
}

Results are newest first. See pagination.