> ## Documentation Index
> Fetch the complete documentation index at: https://docs.miraiminds.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Start building

> Go from a secret key to a running campaign in ten minutes, with every request ready to copy and paste.

This page is the partner on-ramp, and it takes ten minutes from start to
finish. You check your key, make one agent, ring one phone, run one campaign
and read the report. You can paste every request below as it is; the only
things to change are the key and the phone number.

**You need:** your `sk_live_` key, a phone number you are allowed to call, and
(for step 5) a public HTTPS URL.

```bash theme={null}
https://sandbox.voice.miraiminds.co
```

<Note>
  Your Mirai contact sends your key in the same message as this link. You can
  also create one yourself in the [console](https://sandbox.voice.miraiminds.co)
  under **Developers → Create key**. It looks like `sk_live_` followed by 32 hex
  characters, and it is shown only **once**. We store only its SHA-256, so we
  cannot recover a lost key; you rotate it instead. Put it in your secret manager
  now, before you go further.

  You also get a webhook signing secret, `whsec_…`. It belongs to the workspace
  rather than the key, so rotating a key does not change it and your receiver
  keeps verifying without an outage.
</Note>

<Steps>
  <Step title="Check the key (30 seconds)">
    ```bash theme={null}
    curl https://sandbox.voice.miraiminds.co/v2/wallet \
      -H "Authorization: Bearer sk_live_YOUR_API_KEY"
    ```

    ```json title="200 OK" theme={null}
    { "balance_inr": 500, "currency": "INR", "updated_at": "2026-08-10T09:12:44Z" }
    ```

    A `401` here is almost always a trailing newline from `cat`-ing a key file.
    Paste the key; do not pipe it.

    That balance is real money, and it pays for the calls below. We bill
    **in 10-second blocks, rounded up, with a one-block minimum**, at your plan's
    [tier](/general/tiers). The examples below ran on T3, which costs ₹3 a minute
    or ₹0.50 a block.
  </Step>

  <Step title="Set up Postman (2 minutes, optional but worth it)">
    The easiest way is from your console: **Developers → Postman** gives you the
    collection and an environment that already has your API address (and your key,
    if you just made one).

    Or download them here:

    | File | What it is |
    | :- | :- |
    | [mirai-voice.postman\_collection.json](https://console.voice.miraiminds.co/static/postman/mirai-voice.postman_collection.json) | The collection: every v2 endpoint, each with worked example responses. |
    | [sandbox.postman\_environment.json](https://console.voice.miraiminds.co/static/postman/sandbox.postman_environment.json) | The environment for trial workspaces (`https://sandbox.voice.miraiminds.co`). |
    | [production.postman\_environment.json](https://console.voice.miraiminds.co/static/postman/production.postman_environment.json) | The environment for workspaces that have gone live (`https://prod.voice.miraiminds.co`). |

    The collection is built from the API itself and ships with every release, so
    these links always give you the current one. Download it again whenever you
    want the latest.

    In Postman, click **Import** and drop in the collection and one environment.
    Select the environment (**Mirai Voice — sandbox** or **Mirai Voice — production**)
    and fill in `api_key` (your `sk_live_…`) and `test_number` (an E.164 number
    **you own**). Then open **Agents → Create agent** and work down through
    **Calls**. Requests that create something save its id for the next request,
    so you don't have to copy and paste anything.

    You can skip this entirely and use the `curl` below. The collection is the
    faster path if more than one person on your side will touch the API.
  </Step>

  <Step title="Create an agent (2 minutes)">
    An agent is the reusable configuration a call runs: its prompt, opening line,
    voice, language and limits. You make it once and use it for thousands of calls.

    ```bash theme={null}
    curl -X POST https://sandbox.voice.miraiminds.co/v2/agents \
      -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Order confirmations",
        "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": "ashu", "language": "hi-IN" },
        "language": "hi-IN",
        "max_duration_secs": 300
      }'
    ```

    ```json title="201 Created" theme={null}
    {
      "id": "agt_01K7Q9F3K7M2N5P9R4T6V8W0XZ",
      "object": "agent",
      "name": "Order confirmations",
      "voice": { "voice_id": "ashu", "language": "hi-IN" },
      "language": "hi-IN",
      "max_duration_secs": 300,
      "created_at": "2026-08-10T09:14:02Z"
    }
    ```

    Keep that `id`.

    `{{order_id}}` and `{{customer_name}}` are **agent placeholders**, which each
    call fills from its `variables`. They are not shell or Postman variables, so
    leave them exactly as they are.

    <Tip>
      `voice_id` is `ashu`, `neha`, `shruti` or `sameer`. The catalogue is the same
      on every tier. See [Voices](/v2/voices).
    </Tip>
  </Step>

  <Step title="Place one real call (1 minute)">
    Put your own number in `to` for this one. It will actually ring.

    ```bash theme={null}
    curl -X POST https://sandbox.voice.miraiminds.co/v2/calls \
      -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: first-call-1" \
      -d '{
        "agent_id": "agt_01K7Q9F3K7M2N5P9R4T6V8W0XZ",
        "to": "+919876543210",
        "variables": { "customer_name": "Rohit", "order_id": "AC-88213" },
        "webhook_url": "https://example.com/mirai/webhook"
      }'
    ```

    ```json title="202 Accepted" theme={null}
    { "id": "call_01K7QB4M8N3P6R2S5T7V9W1YA", "status": "queued" }
    ```

    `202` means the call is **accepted but not yet connected**, so the phone has
    not rung. Read the outcome when the call ends:

    ```bash theme={null}
    curl https://sandbox.voice.miraiminds.co/v2/calls/call_01K7QB4M8N3P6R2S5T7V9W1YA \
      -H "Authorization: Bearer sk_live_YOUR_API_KEY"
    ```

    ```json title="200 OK" theme={null}
    {
      "id": "call_01K7QB4M8N3P6R2S5T7V9W1YA",
      "object": "call",
      "status": "completed",
      "ended_reason": "assistant-ended-call",
      "duration_secs": 96,
      "tier": "t5",
      "cost_inr": 8.34
    }
    ```

    96 seconds rounds up to 10 blocks of 10 seconds, which costs ₹8.34 on T5. See
    [what a minute costs](/general/tiers#what-a-minute-costs).

    <Warning>
      **Always send `Idempotency-Key` on `POST /v2/calls`.** A network timeout tells
      you nothing about whether the phone rang. With the key, a retry replays the
      original response and places no second call. Without it, your retry rings a real
      person twice.
    </Warning>
  </Step>

  <Step title="Receive the webhook (3 minutes)">
    You can poll for the outcome, but subscribe to webhooks if you can. We `POST` a
    signed JSON event at each lifecycle transition. **Verify the signature before
    you trust the body.**

    ```
    X-Mirai-Signature: t=1785057398,v1=5f3c…
    ```

    `v1` is `HMAC-SHA256(whsec, "{t}.{raw_body}")`, hex-encoded. Sign the **raw**
    bytes. If your framework parsed the JSON and serialized it again, the string is
    different and the signature will never verify.

    ```python theme={null}
    import hmac, hashlib, time
    from flask import Flask, request

    app = Flask(__name__)
    WHSEC = "whsec_YOUR_WEBHOOK_SECRET"

    def verify(raw: bytes, header: 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 abs(time.time() - int(t)) > 300:
            return False
        expected = hmac.new(WHSEC.encode(), t.encode() + b"." + raw, 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", "")):
            return "", 401
        event = request.get_json()
        print(event["type"], event["data"])
        return "", 200        # any 2xx stops our retries
    ```

    The events are `call.started`, `call.completed`, `call.voicemail`, `call.failed`
    and `call.aborted`, plus `campaign.started` / `paused` / `resumed` /
    `completed` / `stopped`. Dedupe on `id`, because a retried delivery repeats it.
    [Webhooks](/v2/webhooks) has the full reference and a test vector.
  </Step>

  <Step title="Run a campaign (2 minutes)">
    One call proves that the plumbing works. Campaigns are the main product: you
    upload a list, set a window and a budget, and the platform dials the list,
    handling pacing, retries and do-not-call suppression.

    ```bash theme={null}
    curl -X POST https://sandbox.voice.miraiminds.co/v2/campaigns \
      -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "First campaign",
        "agent_id": "agt_01K7Q9F3K7M2N5P9R4T6V8W0XZ",
        "timezone": "Asia/Kolkata",
        "start_date": "2026-08-11",
        "end_date": "2026-08-13",
        "slots": [{ "start": "10:00", "end": "19:00" }],
        "max_concurrent": 1,
        "retry_count": 1,
        "re_attempt_period_secs": 900,
        "budget_paise": 30000,
        "webhook_url": "https://example.com/mirai/webhook",
        "contacts": [
          { "id": "cust-1001", "phone": "+919876543210",
            "variables": { "customer_name": "Rohit", "order_id": "AC-88213" } },
          { "id": "cust-1002", "phone": "+919876543211",
            "variables": { "customer_name": "Anita", "order_id": "AC-88219" } }
        ]
      }'
    ```

    ```json title="201 Created" theme={null}
    {
      "id": "cmp_01K7QAC5N9P4R7S3T6V8W2X4YB",
      "object": "campaign",
      "status": "draft",
      "tier": "t3",
      "contacts_accepted": 2,
      "contacts_duplicate": 0,
      "contacts_rejected": []
    }
    ```

    The response is the campaign object itself, with the upload result as top-level
    keys. Bad rows come back by index, and they do not fail the rest of the upload.

    Start it:

    ```bash theme={null}
    curl -X POST https://sandbox.voice.miraiminds.co/v2/campaigns/cmp_01K7QAC5N9P4R7S3T6V8W2X4YB/start \
      -H "Authorization: Bearer sk_live_YOUR_API_KEY"
    ```

    ```json title="202 Accepted" theme={null}
    { "id": "cmp_01K7QAC5N9P4R7S3T6V8W2X4YB", "status": "play" }
    ```

    `play` means the dialler is running. Outside the `10:00` to `19:00` window it
    waits in `play` until the window opens. That is expected, and the campaign is
    not stuck. [Campaigns](/v2/campaigns) has the full semantics.
  </Step>

  <Step title="Read the report (1 minute)">
    ```bash theme={null}
    curl https://sandbox.voice.miraiminds.co/v2/campaigns/cmp_01K7QAC5N9P4R7S3T6V8W2X4YB/report \
      -H "Authorization: Bearer sk_live_YOUR_API_KEY"
    ```

    ```json title="200 OK" theme={null}
    {
      "campaign_id": "cmp_01K7QAC5N9P4R7S3T6V8W2X4YB",
      "status": "completed",
      "counters": { "completed": 1, "no_answer": 1 },
      "contacts": 2,
      "dialed": 2,
      "attempts": 3,
      "connected": 1,
      "spent_paise": 600,
      "spent_inr": 6,
      "budget_paise": 30000
    }
    ```

    Per contact, as CSV:

    ```bash theme={null}
    curl -G https://sandbox.voice.miraiminds.co/v2/campaigns/cmp_01K7QAC5N9P4R7S3T6V8W2X4YB/report \
      -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
      --data-urlencode "format=csv" -o campaign.csv
    ```

    You have now run the whole loop. The rest of this page covers scale and
    hygiene.
  </Step>
</Steps>

## Before you go live

<AccordionGroup>
  <Accordion title="Verify webhook signatures — and reject what fails">
    Verify `X-Mirai-Signature` over the **raw** body, enforce the 5-minute timestamp
    window, and return `401` when it does not match. Dedupe on the event `id`,
    since retries repeat it. Don't order your state machine by arrival order,
    because a retried `call.started` can land after `call.completed`. Order by
    `data.call.status` instead.
    [Reference](/v2/webhooks#signature-verification).
  </Accordion>

  <Accordion title="Wire the do-not-call list to your opt-outs">
    Every "stop calling me", whether it comes on a call, over WhatsApp or by email,
    should become a `POST /v2/dnc` in the same minute. The request is idempotent, so
    you don't need a read-modify-write. We check suppression at dial time, so it
    also takes effect on campaigns that are already running. [Reference](/v2/dnc).
  </Accordion>

  <Accordion title="Cap every campaign with budget_paise">
    `budget_paise` limits the cost of a mistake in the *list*, such as a duplicated
    CSV or a column shifted by one. Code review cannot catch that kind of failure.
    When the campaign hits the cap, it pauses with
    `pause_reason: "budget_exhausted"`, keeps its place, and continues when you raise
    the cap. Alert on `campaign.paused`.
  </Accordion>

  <Accordion title="Alert on wallet balance, hourly">
    Check `GET /v2/wallet` and alert at a threshold that covers a day of traffic.
    A `402` in the middle of a campaign is an expensive way to learn that your
    wallet is empty. An underfunded campaign still starts, dials what it can, and
    then pauses itself on `insufficient_balance`.
    Once your company is live, top up from the **Billing** page of the console.
  </Accordion>

  <Accordion title="Know the three limits that will actually bite you">
    | Limit | Default | Over it |
    | :- | :- | :- |
    | Request rate | 10 req/s, burst 20 | `429 rate_limited`, honour `Retry-After` |
    | Concurrent live calls | 5 | nothing fails; calls wait in `queued` |
    | Queue depth | 500 accepted-and-not-yet-ended | `429 rate_limited`, `Retry-After: 30` |

    The concurrency limit paces your calls: extra calls wait instead of failing.
    Raising a ceiling is a provisioning change and needs no code change, so ask for
    it before the campaign starts. See [Limits](/v2/limits).
  </Accordion>

  <Accordion title="Handle these errors specifically">
    | `error.code` | Status | Do |
    | :- | :- | :- |
    | `insufficient_balance` | `402` | Stop dialling, alert, top up. No call was placed and nothing was charged. |
    | `rate_limited` | `429` | Back off and honour `Retry-After`. `message` says which ceiling you hit. |
    | `duplicate_call` | `409` | This is not an error. The same `Idempotency-Key` is still in flight, and we place no second call. |
    | `at_capacity` | `429` | Every line is busy. Retry in 30 seconds. |
    | `fleet_offline` | `503` | Speech capacity is down. Retry in a few minutes; nothing was charged. |
    | `contract_unavailable` | `403` | The call's `tier` is not your plan's, or the company has not finished going live. Leave `tier` out. |
    | `campaigns_unavailable` | `503` | Campaigns are not enabled on your workspace. Ask us, because retrying will not clear it. |

    Branch on status first, then on `error.code`. [Errors](/v2/errors) has the full
    table and a retry wrapper in two languages.
  </Accordion>

  <Accordion title="Get the compliance basics right">
    The business placing the calls is responsible for the calling window, consent
    records, national DND scrubbing and telling people they are speaking to an AI.
    Campaigns enforce the window and the suppression list that you set, but they do
    not make your calls compliant. Put the AI disclosure in `first_message`. It
    costs one clause, and it reduces early hang-ups.
    [India calling rules](/v2/limits#india-calling-rules).
  </Accordion>
</AccordionGroup>

## Where to go next

<CardGroup cols={2}>
  <Card title="Campaigns" icon="list-check">
    Windows, retries, budgets, per-contact reporting. [Reference](/v2/campaigns).
  </Card>

  <Card title="Agents" icon="robot">
    Prompts, voices, voicemail behaviour, duration caps. [Reference](/v2/agents).
  </Card>

  <Card title="Webhooks" icon="circle-check">
    Every event, the signature scheme, retries. [Guide](/v2/webhooks).
  </Card>

  <Card title="Billing & tiers" icon="indian-rupee-sign">
    The price ladder and what a minute costs. [Reference](/general/tiers).
  </Card>
</CardGroup>

If you are stuck on something this page does not cover, email
[help@miraiminds.co](mailto:help@miraiminds.co) with the `call_id` or
`campaign_id`, and we can see exactly what happened.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.