Guide · Start building

Make an AI phone call from Python with the Mirai API.

In ten minutes your Python script will place a real call in Hindi and get the transcript back. You create an agent once, place the call, and catch the result on a small Flask webhook.

By Frequency · Published 16 October 2026.

AI agents from ₹1 a minute.¹

Read the API docs

You need a key, a number and a webhook URL.

  • A secret key. In the sandbox, open Developers and press Create key. Confirm your work email and the wallet gets ₹200 of credit. You also get a whsec_ webhook secret.
  • A number you’re allowed to call, in E.164 form: +919876543210.
  • A public HTTPS URL for webhooks. ngrok http 3000 is fine for now.
  • pip install "httpx==0.28.1" "flask==3.1.2".

Calls cost ₹1 a minute on our volume plan, ₹5 pay as you go. Until your company finishes Go live, every call bills at ₹5. The pricing guide has the detail.

export MIRAI_API_KEY="sk_live_..."
export MIRAI_WEBHOOK_SECRET="whsec_..."

First, create the agent once.

The agent holds the prompt, the opening line, the voice, the language and a time limit. Make it once.

import os

import httpx

API = "https://sandbox.voice.miraiminds.co"
auth = {"Authorization": f"Bearer {os.environ['MIRAI_API_KEY']}"}

agent = httpx.post(
    f"{API}/v2/agents",
    headers=auth,
    json={
        "name": "Order Confirmation",
        "system_prompt": (
            "You are Priya from Acme. Confirm order {{order_id}} with "
            "{{customer_name}} and ask whether the delivery address is "
            "unchanged. Keep replies to one or two short sentences. When "
            "the customer is done, thank them and end the call."
        ),
        "first_message": "नमस्ते {{customer_name}}, मैं Acme से Priya बोल रही हूँ।",
        "voice": {"voice_id": "neha", "language": "hi-IN"},
        "language": "hi-IN",
        "max_duration_secs": 300,
    },
    timeout=30,
).raise_for_status().json()

print(agent["id"])  # agt_...

Voices are ashu, neha, shruti and sameer. Keep voice.language equal to language; the voice follows the top-level one. Each call’s variables fill the {{placeholders}}. Before you scale up, read the prompting guide on Hinglish and ending a call.

Then place the call with one request.

call_resp = httpx.post(
    f"{API}/v2/calls",
    headers={**auth, "Idempotency-Key": "order-8842-confirm-1"},
    json={
        "agent_id": agent["id"],
        "to": "+919876543210",
        "variables": {"customer_name": "Rahul", "order_id": "8842"},
        "metadata": {"order_id": "8842"},
        "webhook_url": "https://example.com/mirai/webhook",
        "final_results": True,
    },
    timeout=30,
)
if call_resp.status_code == 402:
    raise RuntimeError(call_resp.json()["error"]["message"])  # top up the wallet
call = call_resp.raise_for_status().json()
print(call["id"], call["status"])  # call_... queued

202 Accepted means queued, not ringing. Save call["id"] against your order at once, because a webhook can land before this request returns.

Build the Idempotency-Key from your own record. Resend a timed-out request with the same key and you get the first response back, not a second call. Keys last 24 hours.

variables only fill the prompt. metadata (up to 16 string keys) comes back on every webhook, so your IDs go there. final_results: true sends the transcript in one call.processed event at the end.

Last, catch the result on a signed webhook.

import hashlib
import hmac
import os
import time

from flask import Flask, request

app = Flask(__name__)
WEBHOOK_SECRET = os.environ["MIRAI_WEBHOOK_SECRET"]
TOLERANCE_SECS = 300


def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1 or not t.isdigit():
        return False
    if abs(time.time() - int(t)) > TOLERANCE_SECS:
        return False
    expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)


@app.post("/mirai/webhook")
def mirai_webhook():
    if not verify(request.get_data(), request.headers.get("X-Mirai-Signature", ""), WEBHOOK_SECRET):
        return "", 401
    event = request.get_json()
    call = event["data"].get("call", {})
    if event["type"] in ("call.completed", "call.voicemail", "call.failed", "call.aborted"):
        print(event["type"], call["status"], call.get("ended_reason"), call.get("metadata"))
    elif event["type"] == "call.processed":
        transcript = event["data"].get("transcript", {})
        for turn in transcript.get("turns", []):
            print(f"{turn['role']}: {turn['text']}")
    return "", 200  # any 2xx stops retries


if __name__ == "__main__":
    app.run(port=3000)

Point your tunnel at port 3000 and use its URL as webhook_url in step 2. In production, store each event before you answer 200 and dedupe on event["id"], as the webhook guide shows.

We tested verify against the docs’ test vector. The other two requests match the quickstart field for field; we didn’t run them, as they place a real call.

Each call ends with an event, then a transcript.

Each call ends with one of call.completed, call.voicemail, call.failed or call.aborted, carrying data.call.status and ended_reason. A completed call also has duration_secs, the billed cost and data.wallet.balance_inr.

Usually within two minutes, call.processed brings the transcript. Each turn has role, text, start_ms and end_ms, in the language it was spoken in, Hinglish included. Over 512 KB it arrives as fetch_required, so read GET /v2/calls/{id}/transcript. GET /v2/calls/{id}/recording gives a 15-minute signed link. We keep both for 90 days.

No public URL? Poll GET /v2/calls/{id}, every 5 seconds at most. It counts against your rate limit.

These are the errors you are most likely to hit.

  • 401 unauthorized: missing Bearer prefix, or a stray newline in the key.
  • 402 insufficient_balance: check GET /v2/wallet and top up under Billing.
  • 400 invalid_request on to or voice_id: send +919876543210, not 9876543210, and one of the four voices.
  • 409 duplicate_call: the first request with that key is still running. Retry shortly.
  • 403 contract_unavailable: you sent a tier that isn’t your plan’s. Leave it out.
  • 429 rate_limited: too many requests, or 500 calls already queued. Wait for Retry-After.
  • no_answer, or failed with no-media: it didn’t connect. Some carriers report a ring-out as no-media. Neither is billed.
  • No webhook: the URL isn’t public HTTPS. curl -X POST it yourself.
  • Signature never matches: your framework re-serialised the body. Sign the raw bytes.
  • A burst rings late: calls over your concurrency limit (5 on pilot keys) wait in the queue.
  • Single calls skip your do-not-call list. Check it yourself.

Here is what to do after your first call.

After go-live, send the same requests to https://prod.voice.miraiminds.co. For a whole list, use campaigns. If the agent goes off script, check GET /v2/agents/{id}/logs and the prompt repair guide.

Sources

These are the references this guide is built on.

References reviewed on . Measurement dates and experimental limits are stated in the article. Check current API and runtime documentation when integrating.

docs.miraiminds.co
Voice-agent API quickstart
docs.miraiminds.co
Agents
docs.miraiminds.co
Billing and tiers

Questions

These are the questions developers ask us most.

What does one AI phone call cost?

Calls bill in 10-second blocks, rounded up, so a 96-second call bills as 100 seconds: ₹8.34 pay as you go. Missed, busy and failed calls are free.

Is there an official Python SDK?

Yes, mirai-voice, from our own package index, not PyPI. We use plain httpx here so you can see every request.

Can I test without placing a real phone call?

Partly. There's no test key, so every phone call is real. But you can talk to the agent in the console's web phone, and check your webhook against the docs' test vector.

Will the API stop me calling at night?

Not for single calls. Keep to 09:00 to 21:00 IST and honour opt-outs in your own code. Campaigns keep to the window you set.

Can the agent transfer the call to a person?

Not yet, though it's on the roadmap. For now, have the agent promise a callback.

Still deciding? Ask us.

Fifteen minutes with the team that builds it. Bring your script, or your hardest call.

Book 15 minutes

Book a call

15 minutes with Sneh Mehta

Finding a time for us…

  1. AI agents from ₹1 a minute: on our volume plan, ₹1,00,000 a month. Pay as you go is ₹5 a minute. Prices exclude GST.