> ## 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.

# Limits & compliance

> Rate limits, concurrency, payload ceilings, and the India calling-window and DNC rules you are responsible for.

Two kinds of limit apply to you: ours (technical, we enforce them) and India's
(regulatory, **you** are responsible for them).

## Rate limits

Limits are per **secret key**.

| Limit                 | Pilot default                | Applies to                          | Over the limit      |
| :-------------------- | :--------------------------- | :---------------------------------- | :------------------ |
| Request rate          | 10 requests/second, burst 20 | all `/v2/*` endpoints               | `429 rate_limited`  |
| Concurrent live calls | 5                            | calls in `dialing` or `in_progress` | the call **queues** |
| Queue depth           | 500                          | calls accepted and not yet ended    | `429 rate_limited`  |

<Warning>
  **Pilot keys are provisioned individually**

  The numbers above are the defaults a new pilot key is issued with. Yours may
  differ — confirm your actual ceilings with your account contact before you size
  a campaign. Higher concurrency is a provisioning change, not a code change: ask.
</Warning>

### Request rate

Exceeding the **request rate** returns `429` with `error.code: rate_limited` and
a `Retry-After` header in seconds. It is about how fast you call the API, not
about how many phones are ringing.

```json title="429 Too Many Requests" theme={null}
{
  "error": {
    "code": "rate_limited",
    "message": "rate limit exceeded, retry in 0.4s"
  }
}
```

### Concurrency

Concurrency does **not** produce a `429`. A `POST /v2/calls` beyond your
concurrent-call ceiling is still accepted with `202` and sits in `queued` until a
slot frees, then dials — nothing is rejected and nothing is lost. `429` is
request-rate only.

That makes the ceiling a **pacer**, not a gate. Two consequences:

* A queued call's phone rings later than you asked. If a call is only useful
  inside a window, check `status` before you assume it went out — or do not
  submit it until you have the capacity.
* Submitting a burst does not fail; it builds a queue that drains at your
  concurrency.

### Queue depth

The queue itself **is** bounded. Once you have **500 calls accepted and not yet
ended**, further `POST /v2/calls` return `429 rate_limited` with a `Retry-After`
header until calls drain. The message names the ceiling, so you can tell it apart
from a request-rate `429`:

```json title="429 Too Many Requests" theme={null}
{
  "error": {
    "code": "rate_limited",
    "message": "queue depth limit reached for this workspace (500 calls accepted and not yet ended, limit 500); wait for calls to drain or ask ops to raise it"
  }
}
```

Both limits share the `rate_limited` code on purpose: the correct client
behaviour is identical — honour `Retry-After` and retry. Read `error.message` if
you want to log which one you hit.

The count is calls **accepted and not yet ended** — `queued` plus the handful
actually `dialing` or `in_progress` — not strictly `queued`. With a concurrency
of 5 the difference is at most 5 calls.

Queue depth is provisioned per workspace, like concurrency. If a campaign
genuinely needs to submit more than 500 rows in one batch, ask your Mirai contact before the
campaign, not during it.

Keeping the queue short is still the better shape, because a queue you own is
one you can reorder, cancel and re-prioritise. The simplest correct pattern:

```python theme={null}
# keep N calls in flight; refill as terminal webhooks arrive
in_flight = 0
MAX_IN_FLIGHT = 5   # your provisioned concurrency

def on_terminal_webhook(event):
    global in_flight
    in_flight -= 1
    pump()

def pump():
    global in_flight
    while in_flight < MAX_IN_FLIGHT and queue:
        place_call(queue.pop())
        in_flight += 1
```

Do not drive pacing off a fixed `sleep()`. Answer rates vary by hour, and a
fixed delay either wastes capacity or hammers the limit.

## Payload and duration ceilings

| Thing               | Limit                             |
| :------------------ | :-------------------------------- |
| Request body        | 1 MB                              |
| `system_prompt`     | 8,000 characters                  |
| `first_message`     | 500 characters                    |
| `variables`         | 32 keys, 512 characters per value |
| `max_duration_secs` | 30–1800, default 300              |
| `webhook_url`       | HTTPS only, 2,048 characters      |
| Idempotency key     | 255 characters, 24-hour window    |

## Campaign ceilings

[Campaigns](/v2/campaigns) carry their own bounds, all enforced at the API edge
with `400 invalid_request`.

| Thing                    | Limit                        | Notes                                                                                                         |
| :----------------------- | :--------------------------- | :------------------------------------------------------------------------------------------------------------ |
| `contacts` per request   | **1000 rows**                | Create and `POST /contacts` both. Call it repeatedly for a longer list — a running campaign accepts new rows. |
| `slots`                  | 1–4 per campaign             | `HH:MM` local. A zero-length slot is rejected, not read as "all day".                                         |
| `max_concurrent`         | 1–50                         | Bounded again by your workspace concurrency — two campaigns share one ceiling.                                |
| `retry_count`            | 0–5                          | Retries after the first attempt.                                                                              |
| `re_attempt_period_secs` | 60–86,400                    | 1 minute to 24 hours.                                                                                         |
| `max_duration_secs`      | 30–1800                      | Defaults to the agent's.                                                                                      |
| `budget_paise`           | positive integer             | Paise, not rupees. Omit for no cap.                                                                           |
| `name`                   | 120 characters               |                                                                                                               |
| Contact `variables`      | 32 keys, 512 chars per value | Same rule as call variables.                                                                                  |

Bad contact rows do **not** fail the batch: they come back by index in
`contacts_rejected`. Only exceeding 1000 rows fails the whole request.

Long prompts are a latency problem before they are a limit problem. The
transactional tier is tuned for prompts in the hundreds of tokens, not
thousands.

## Timeouts

| Stage            | Behaviour                                                                                                                    |
| :--------------- | :--------------------------------------------------------------------------------------------------------------------------- |
| API request      | Respond within 30 seconds or treat as failed and retry with the same `Idempotency-Key`.                                      |
| Ring / answer    | \~60 seconds. No media by then → `no_answer`.                                                                                |
| Call duration    | Ends at `max_duration_secs` with `status: timeout`.                                                                          |
| Webhook delivery | Your endpoint should answer well under a second. Slow responses are treated as failures and [retried](/v2/webhooks#retries). |

## Data retention

| Data                                 | Retained                      |
| :----------------------------------- | :---------------------------- |
| Call record (status, duration, cost) | 90 days                       |
| Call recordings and transcripts      | 90 days, then deleted         |
| Wallet ledger                        | 12 months                     |
| Campaigns and their contact rows     | For the life of the workspace |

Recordings and transcripts are available through the API —
[`GET /v2/calls/{id}/recording`](/v2/calls#get-the-recording) and
[`/transcript`](/v2/calls#get-the-transcript). Pull anything you need to keep
beyond 90 days into your own system, and tell us up front if your use case
requires that we **not** record at all.

***

## India calling rules

You are the sender. Under Indian telecom regulation the obligations for
commercial voice calls sit with the business placing them, not with the platform
carrying them. Reading this page is not legal advice, and it is not a
substitute for your own compliance review.

### Calling window

Commercial and promotional voice calls are restricted to daytime hours. The
window applied in practice is **09:00–21:00 IST**, and several enterprise
programmes tighten it further to 10:00–19:00.

**On a [campaign](/v2/campaigns), the platform enforces the window you declare.**
`timezone`, `start_date`/`end_date` and 1–4 `slots` are evaluated in local time
on every dialling tick: outside the window the campaign sleeps in `play` and
rings nobody. Declaring `10:00`–`19:00 Asia/Kolkata` is enough — you do not need
your own scheduler in front of it.

```json theme={null}
{
  "timezone": "Asia/Kolkata",
  "slots": [{ "start": "10:00", "end": "19:00" }]
}
```

**On a single [`POST /v2/calls`](/v2/calls#create-a-call) there is no window.**
A direct call is an explicit instruction to ring one number now, and it will
dial at 02:00 IST if you ask it to. Gate those in your scheduler:

```python theme={null}
from datetime import datetime
from zoneinfo import ZoneInfo

IST = ZoneInfo("Asia/Kolkata")

def within_calling_window(now=None) -> bool:
    now = now or datetime.now(IST)
    return 9 <= now.hour < 21
```

### Consent and DNC/DND

* **Get consent before you call.** Explicit, recorded, and revocable. Keep the
  record — it is what you produce when a complaint arrives.
* **Push your opt-outs to [`POST /v2/dnc`](/v2/dnc).** That list is enforced by
  us: every campaign checks it at dial time, so a number added mid-campaign is
  suppressed from that moment, marked `suppressed` in the report, and never
  billed. It does not apply to single `POST /v2/calls` — those are explicit.
* **Scrub against the DND registry** (TRAI's Do Not Disturb / NCPR list) as
  well. We do **not** scrub against the national register for you, and our
  suppression list is not a substitute for it. A number can register between two
  campaigns.
* **Honour opt-outs immediately.** If a customer says "stop calling me" during
  a call, that is an opt-out — capture it and suppress the number, on every
  channel it applies to.
* **Transactional vs promotional matters.** A delivery confirmation to an
  existing customer is treated differently from a marketing pitch to a cold
  list. Classify each campaign, and do not let a transactional pretext carry a
  promotional payload.
* **Register as required.** Commercial communication under TCCCPR 2018 runs
  through registered entities, headers and consent templates. Confirm your
  registration status with your telecom provider or counsel.

### Disclosure that it is an AI

Tell people. Put it in the agent's `first_message` or its opening turn:

```json theme={null}
{
  "first_message": "नमस्ते {{customer_name}}, मैं Acme की AI असिस्टेंट Priya बोल रही हूँ।"
}
```

This is the direction regulation is moving in worldwide, it costs you one
clause, and in practice it *reduces* early hang-ups — callers who feel misled
hang up harder than callers who were told.

### Recording

If you record calls, say so in the opening turn and keep the recordings under
the same retention and access controls as the rest of your customer data.

### What we do enforce

| Rule                                     | Enforced by us                                                 |
| :--------------------------------------- | :------------------------------------------------------------- |
| E.164 destination format                 | ✅ `400 invalid_request`                                        |
| Wallet balance before dialling           | ✅ `402 insufficient_balance`                                   |
| Request rate                             | ✅ `429 rate_limited`                                           |
| Concurrency ceiling                      | ✅ over-cap calls wait in `queued`                              |
| Queue depth                              | ✅ `429 rate_limited` past 500 outstanding                      |
| Maximum call duration                    | ✅ `status: timeout`                                            |
| Campaign budget cap                      | ✅ campaign pauses, `pause_reason: budget_exhausted`            |
| Calling window — **campaigns**           | ✅ `slots` + `timezone`, evaluated in local time every tick     |
| Calling window — single `POST /v2/calls` | ❌ your scheduler                                               |
| Your own opt-out list                    | ✅ [`/v2/dnc`](/v2/dnc), checked at dial time on every campaign |
| National DND / NCPR scrubbing            | ❌ your list                                                    |
| Consent records                          | ❌ your records                                                 |
| AI disclosure                            | ❌ your prompt                                                  |

Persistent complaint patterns against a workspace will get its keys suspended.
That is not a regulatory mechanism, it is ours — we would rather suspend one
campaign than lose the numbering range for everyone on the platform.
