# Quickstart

Source: https://docs.usetone.ai/quickstart

> Build a voice agent, give it a phone number, and place a call with a known outcome — in five minutes, for free.

By the end of this page you'll have placed a call through the Tone API and read back its outcome.
It costs nothing, no phone rings, and you don't need KYC, a DLT registration, or a wallet balance —
everything here runs in [test mode](/test-mode).

Everything you build here works unchanged in production. Going live is a key swap.

## 1. Get a test key

In the dashboard, open **Developer → API keys** and create a key with environment **test** and
scope **write**. Copy it — it's shown once.

```bash
export TONE_API=https://apibeta.usetone.ai
export TONE_KEY=tone_test_...
```

Check it works:

```bash
curl -H "Authorization: Bearer $TONE_KEY" "$TONE_API/v1/limits"
```

You should get your request budget and concurrency quotas back. If you get a `401`, the key is
wrong or revoked; if you get `insufficient_scope` later on, it was minted `read`-only.

## 2. Create an agent

An agent is the thing that talks: a prompt, a voice, and a language.

```bash
curl "$TONE_API/v1/agents" \
  -X POST \
  -H "Authorization: Bearer $TONE_KEY" \
  -H 'content-type: application/json' \
  -d '{
    "name": "Order confirmation",
    "purpose": "transactional",
    "systemPrompt": "You call customers to confirm a cash-on-delivery order. Confirm the order, ask which day suits them for delivery, then thank them and end the call. Be brief and polite.",
    "voice": {
      "ttsModel": "bulbul:v3",
      "ttsVoice": "simran",
      "sttModel": "saaras:v3",
      "languages": ["hi-IN", "en-IN"]
    }
  }'
```

```json
{ "data": { "id": "a4f21c8e-...", "name": "Order confirmation", "status": "draft", "...": "..." } }
```

Save the `id` as `$AGENT`.

Two fields deserve a moment:

**`purpose`** is your call's regulatory classification — `transactional`, `service`,
`promotional`, or `collections` — and it is not cosmetic. India's TCCCPR rules restrict when
promotional and collections calls may be placed, and Tone's compliance gate enforces that on every
dial using this field. There is deliberately no way to override it per call; if there were, the
audit trail would be worthless. Classify honestly.

**`voice.ttsVoice` must be valid for `ttsModel`.** Voices aren't portable across model versions:
`simran` is a `bulbul:v3` voice and is rejected on `bulbul:v2`. A bad pairing is a `400` at write
time rather than a failure mid-call. Browse what's available:

```bash
curl -H "Authorization: Bearer $TONE_KEY" "$TONE_API/v1/catalog/voice"
```

### Publish it

Agents are created as drafts:

```bash
curl "$TONE_API/v1/agents/$AGENT" \
  -X PATCH \
  -H "Authorization: Bearer $TONE_KEY" \
  -H 'content-type: application/json' \
  -d '{"status":"live"}'
```

You can dial with a draft agent — outbound calls run whatever you point them at, which is what
makes iterating quick. Publishing matters for **inbound**: a number whose agent is still a draft
refuses incoming calls, on the grounds that a half-written prompt shouldn't answer a real customer.

## 3. Get a phone number

In test mode, numbers are free and allocated on request:

```bash
curl "$TONE_API/v1/numbers" \
  -X POST \
  -H "Authorization: Bearer $TONE_KEY" \
  -H 'content-type: application/json' \
  -d '{"series":"regular","agentId":"'"$AGENT"'"}'
```

```json
{ "data": { "id": "b91e...", "e164": "+918041000123", "status": "active", "environment": "test", "agentId": "a4f21c8e-...", "...": "..." } }
```

Save the `id` as `$NUMBER`. Binding `agentId` here is what makes this agent answer calls *to* this
number; for placing calls, you'll name the agent explicitly.

In production you'd search real carrier inventory and buy — see [Going live](#7-going-live).

## 4. Place a call

```bash
curl "$TONE_API/v1/calls" \
  -X POST \
  -H "Authorization: Bearer $TONE_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H 'content-type: application/json' \
  -d '{
    "agentId": "'"$AGENT"'",
    "numberId": "'"$NUMBER"'",
    "toE164": "+915555000001",
    "variables": { "order_id": "SO-4417", "customer_name": "Priya" }
  }'
```

```json
{ "data": { "id": "3b7e...", "status": "queued", "direction": "outbound", "environment": "test", "...": "..." } }
```

Three things happened in that request:

- **`+915555000001` is a [magic number](/test-mode#magic-numbers)** — in test mode it always
  answers, for 45 seconds, with a canned summary. `...0002` is always busy, `...0003` never
  answers. Deterministic outcomes are what let you write real tests.
- **`Idempotency-Key`** makes the request safe to retry. If the connection drops and you send it
  again with the same key, you get the original call back rather than dialling twice.
  [More on idempotency](/idempotency).
- **`variables`** fill the `{{placeholders}}` in your agent's prompt, per call. They're stored on
  the call record and returned by reads.

### If you get a `403`

```json
{ "error": { "type": "compliance", "code": "blocked_dnd", "message": "That number is on your Do-Not-Call list." } }
```

That's the compliance gate, and it runs in test mode exactly as it does live. Handle it as a normal
outcome, not an exception: some fraction of any real recipient list is going to come back blocked.
See [the compliance guide](/quickstart-compliance).

## 5. Read the outcome

The sandbox rings, then settles, within a few seconds:

```bash
curl -H "Authorization: Bearer $TONE_KEY" "$TONE_API/v1/calls/3b7e..."
```

```json
{
  "data": {
    "id": "3b7e...",
    "status": "ended",
    "disposition": "answered",
    "durationSeconds": 45,
    "billedPaise": 0,
    "summary": "Sandbox call: the customer confirmed the order and asked for delivery on Thursday.",
    "outputs": { "confirmed": true, "preferred_day": "thursday", "sandbox": true },
    "variables": { "order_id": "SO-4417", "customer_name": "Priya" }
  }
}
```

`status` moves `queued` → `in_progress` → `ended`. Only when it reaches `ended` is `disposition`
meaningful — `answered`, `no_answer`, `busy`, `voicemail`, `failed`, or `unknown`. Treat that list
as open: new dispositions can appear without being a breaking change.

`outputs` is structured data extracted from the conversation, shaped by the output variables you
declare on the agent. That's usually the field your application actually cares about — the
transcript is for humans, `outputs` is for your database.

## 6. Stop polling — use webhooks

Polling works for a quickstart. For anything real, subscribe:

```bash
curl "$TONE_API/v1/integrations/webhooks" \
  -X POST \
  -H "Authorization: Bearer $TONE_ADMIN_KEY" \
  -H 'content-type: application/json' \
  -d '{
    "url": "https://your-app.example.com/hooks/tone",
    "events": ["call.completed", "call.failed"]
  }'
```

The response contains a `whsec_...` signing secret, shown **once**. Every delivery is signed with
it, and verifying that signature is not optional — see [Webhooks](/webhooks) for the full
catalog, the verification snippet, and the retry behaviour.

Registering an endpoint needs an `admin` key, because the URL decides where your call data flows.

## 7. Going live

```bash
export TONE_KEY=tone_live_...
```

That is genuinely the code change. What has to be true around it:

1. **Complete KYC and DLT registration** in the dashboard. Live keys unlock at verification, and
   the compliance gate blocks promotional traffic without an approved DLT PE ID.
2. **Fund your wallet.** Live calls debit per minute; live numbers charge a setup fee and a monthly
   rental. Subscribe to `wallet.balance.low` so you hear about it before a campaign pauses itself.
3. **Buy a real number** with an `admin` key. Search inventory, then purchase — and note the
   mandatory `Idempotency-Key`, because a duplicate purchase is a second monthly rental:

   ```bash
   curl -H "Authorization: Bearer $TONE_LIVE_ADMIN_KEY" \
     "$TONE_API/v1/numbers/available?number_type=mobile&region=KA"

   curl "$TONE_API/v1/numbers/purchase" \
     -X POST \
     -H "Authorization: Bearer $TONE_LIVE_ADMIN_KEY" \
     -H "Idempotency-Key: $(uuidgen)" \
     -H 'content-type: application/json' \
     -d '{"e164":"+918041234567","label":"orders line"}'
   ```

4. **Register a live webhook endpoint.** Endpoints and their secrets are per-endpoint, not shared
   between environments — point test and live at different URLs so a sandbox event can never be
   mistaken for a real one.
5. **Re-check your error handling** against the [error catalog](/errors), especially
   `insufficient_funds`, `concurrent_call_limit_reached`, and the `blocked_*` family. In test mode
   you had no wallet and no real capacity limits; in production both bite.

## Where to go next

- **[Calling many people](/campaigns)** — create a campaign, add recipients, launch it.
  Campaigns handle pacing, concurrency, retries, and the calling-window rules for you.
- **[Bring your own voice stack](/quickstart-byo)** — keep Tone's numbers and compliance, run
  your own AI.
- **[Compliance as an API](/quickstart-compliance)** — verdicts, consent, and suppression, with
  or without placing calls through us.
- **Knowledge bases** — upload documents your agent can answer from, and attach them with
  `knowledgeBaseIds` on the agent.
