Skip to content
Discord

Quickstart

Five minutes, four requests, one real phone call.

You need: a secret key, a phone number you are allowed to call, and a public HTTPS URL for webhooks (ngrok is fine while you build).

  1. Keys are ops-issued during the pilot. Email sneh@miraiminds.co with your company name and use case. You get back three things:

    Looks likeUsed for
    Secret keysk_live_YOUR_API_KEYevery API request
    Webhook secretwhsec_7d4f1a09c2e58b36a1f0d7c93b2e64a8verifying events we send you
    Starting wallet balance₹500pays for call minutes

    The webhook secret is shown once. Store both in your secret manager, not in git.

    Check it works:

    curl https://api.voice.miraiminds.co/v2/wallet \
      -H "Authorization: Bearer sk_live_YOUR_API_KEY"
    { "balance_inr": 500, "currency": "INR", "updated_at": "2026-07-26T09:12:44Z" }
  2. An agent is the reusable configuration a call runs: prompt, opening line, voice, language, limits. Create it once, call it thousands of times.

    curl -X POST https://api.voice.miraiminds.co/v2/agents \
      -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "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": "priya", "language": "hi-IN" },
        "language": "hi-IN",
        "max_duration_secs": 300
      }'
    201 Created
    {
      "id": "agt_01JZQ8F3K7M2N5P9R4T6V8W0XZ",
      "object": "agent",
      "name": "Order Confirmation",
      "voice": { "voice_id": "priya", "language": "hi-IN" },
      "language": "hi-IN",
      "max_duration_secs": 300,
      "created_at": "2026-07-26T09:14:02Z"
    }

    Keep that id.

  3. variables fill the {{placeholders}} in system_prompt and first_message. webhook_url is where lifecycle events land.

    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"
      }'
    202 Accepted
    { "id": "call_01JZQ9B4M8N3P6R2S5T7V9W1YA", "status": "queued" }

    202 means accepted, not connected — the phone has not rung yet. Poll GET /v2/calls/{id} or, better, wait for the webhook.

  4. We POST a signed JSON event to your webhook_url at each lifecycle transition. Verify the signature before you trust the body.

    import hmac, hashlib, time
    from flask import Flask, request
    
    app = Flask(__name__)
    WHSEC = "whsec_7d4f1a09c2e58b36a1f0d7c93b2e64a8"
    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:
            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 hook():
        if not verify(request.get_data(), request.headers.get("X-Mirai-Signature", ""), WHSEC):
            return "", 401
        event = request.get_json()
        print(event["type"], event["data"]["call"]["status"])
        return "", 200  # 2xx stops our retries

    The final event for a connected call:

    POST https://example.com/mirai/webhook
    {
      "id": "evt_01JZQ9C5N9P4R7S3T6V8W2X4YB",
      "type": "call.completed",
      "created_at": "2026-07-26T09:16:38Z",
      "data": {
        "call": {
          "id": "call_01JZQ9B4M8N3P6R2S5T7V9W1YA",
          "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 }
        }
      }
    }

    96 seconds is billed as 2 minutes — minutes round up.

  • Webhooks — every event type, retries, replay protection.
  • Calls — statuses, ended_reason, abort, listing.
  • Billing & tiers — what a minute costs and what each tier can do.
  • Limits — rate limits, concurrency, India calling-window rules.
SymptomCauseFix
401 unauthorizedMissing Bearer prefix, or a copy-paste with a newlineRe-copy the key; check Authorization: Bearer sk_live_…
402 insufficient_balanceWallet cannot cover one minuteAsk ops for a top-up; check GET /v2/wallet
400 invalid_request on toNumber not E.164Use +919876543210, not 9876543210 or 091 98765 43210
Call ends instantly, status: no_answerNumber unreachable or DND-blockedTry another handset; see compliance
No webhook arrivesURL not publicly reachable, or not HTTPSTest with curl -X POST against your own URL first
Signature never verifiesFramework parsed and re-serialized the bodySign over the raw bytes — see Webhooks