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

# Telephony APIs Quick Start

> Make your first call and receive your first call on an Unpod number

Go from zero to a live call: first you call a phone, then a phone calls you.

> [!NOTE]
>
> **Preview.** Call control is in preview. Ask the Unpod team to switch on the Call API for your
> org; until then every call returns `403 call_api_disabled`. Coming from Plivo? See
> [Migrate from Plivo](https://docs.unpod.ai/telephony/apis/migrate-from-plivo.md).

## Make your first call

**1. Credentials**

Export `UNPOD_BASE_URL`, `UNPOD_PLATFORM_TOKEN` and `UNPOD_ORG_HANDLE` as shown in
[Credentials](https://docs.unpod.ai/telephony/introduction.md#credentials).

**2. Install**

Python 3.12+ (`unpod` 0.6.2 requires it). cURL needs nothing.

```bash theme={null}
python3 -m venv .venv
source .venv/bin/activate
pip install "unpod>=0.6.2"
```

**3. Get a number**

Add one from the dashboard ([Get a Number](https://docs.unpod.ai/telephony/apis/numbers.md)) or run this once to
attach an unassigned pool number:

```python Python theme={null}
import httpx
from unpod import Client

with Client() as client:
    tel = client.telephony
    pool = tel.numbers.list()
    number = next((n.number for n in pool if n.status == "not_assigned"), None)
    if number is None:
        raise SystemExit("No unassigned pool number. Add one from the dashboard.")
    try:
        result = tel.numbers.attach(number, attach_type="human_call")
    except httpx.HTTPStatusError as e:  # nothing attached: the body says why
        raise SystemExit(e.response.text)
    r = result.numbers[0]
    print(r.number, r.ok, r.connection_state, r.error)
```

```bash cURL theme={null}
# 1. list: pick a number whose "state" is "NOT_ASSIGNED" and "active" is true
curl -s "https://unpod.ai/api/v2/platform/telephony/numbers/?include_assigned=true" \
  -H "Authorization: Token $UNPOD_PLATFORM_TOKEN" -H "Org-Handle: $UNPOD_ORG_HANDLE"

# 2. attach it
curl -s -X POST "https://unpod.ai/api/v2/platform/telephony/numbers/attach/" \
  -H "Authorization: Token $UNPOD_PLATFORM_TOKEN" -H "Org-Handle: $UNPOD_ORG_HANDLE" \
  -H "Content-Type: application/json" \
  -d '{"numbers": ["+919000000001"], "attach_type": "human_call"}'
```

`attach_type="human_call"` routes the number for call control, with no AI agent. A number
already attached to an AI agent can also be the `from` of an outbound call; `human_call`
matters for inbound calls, where it decides that your app answers instead of the agent.

> [!NOTE]
>
> Dial from a number whose `connection_state` is not `NOT_LINKED` (for example
> `PENDING_VERIFY` or `LINKED`). See it with the overview request from the
> [credentials check](https://docs.unpod.ai/telephony/introduction.md#credentials). A `403 forbidden: from is not
>           a number available to this organization` means the number isn't ready yet; a
> `400 from_not_owned` means it isn't one of your org's numbers.

**4. Call someone and speak**

Set `FROM` to your Unpod number and `TO` to a phone you can answer, both in E.164. Pick up
when it rings.

```python Python theme={null}
import time
from unpod import Client
from unpod.telephony import TelephonyAPIError

FROM = "+919000000001"  # your Unpod number
TO = "+919000000002"    # who to call

with Client() as client:
    calls = client.telephony.calls
    call = calls.create(FROM, TO, ring_timeout=30)
    print("calling", call.call_id)

    state = call.state
    while state not in ("ended", None):  # an ended call comes back with no state
        time.sleep(2)
        try:
            new = calls.get(call.call_id).state
        except TelephonyAPIError as e:  # the call is gone: missed or hung up
            print("ended:", e.status_code, e.message)
            break
        if new == "talking" and state != "talking":
            try:
                calls.speak(call.call_id, "Hello! This is your first call from Unpod.")
            except TelephonyAPIError as e:
                print("speak failed:", e.code, e.message)
        if new != state:
            print("state:", new or "ended")
        state = new
```

```bash cURL theme={null}
# 1. dial -> {"call_id": "...", "state": "initiated", ...}
curl -s -X POST "https://unpod.ai/api/v2/platform/sbc/v1/calls" \
  -H "Authorization: Token $UNPOD_PLATFORM_TOKEN" -H "Org-Handle: $UNPOD_ORG_HANDLE" \
  -H "Idempotency-Key: $(uuidgen)" -H "Content-Type: application/json" \
  -d '{"from": "+919000000001", "to": "+919000000002", "ring_timeout": 30}'

# CALL_ID=<call_id from step 1>

# 2. poll until "state": "talking"
curl -s "https://unpod.ai/api/v2/platform/sbc/v1/calls/$CALL_ID" \
  -H "Authorization: Token $UNPOD_PLATFORM_TOKEN" -H "Org-Handle: $UNPOD_ORG_HANDLE"

# 3. speak
curl -s -X POST "https://unpod.ai/api/v2/platform/sbc/v1/calls/$CALL_ID/speak" \
  -H "Authorization: Token $UNPOD_PLATFORM_TOKEN" -H "Org-Handle: $UNPOD_ORG_HANDLE" \
  -H "Content-Type: application/json" -d '{"text": "Hello from Unpod!"}'
```

`speak` works once the call is `talking` - no server needed.

**5. What you get back**

| `state` | Meaning |
| - | - |
| `initiated` | `create` returned. Unpod is dialing |
| `ringing` | The phone is ringing |
| `talking` | Answered. `speak` works from here |

After hangup, `GET /sbc/v1/calls/{id}` returns the recording summary instead of a state:

```json theme={null}
{"call_id": "...", "org_id": "...", "available": true,
 "url": "/api/v1/calls/<id>/recording", "duration_secs": 64,
 "format": "wav", "channels": 2}
```

That `url` is a path inside Unpod. To download the audio, see
[Recording](https://docs.unpod.ai/telephony/apis/recording.md).

> [!TIP]
>
> No ring? Check that `to` is E.164 (`+` and country code), that the `from` number isn't
> `NOT_LINKED`, and that `ring_timeout` gives the phone long enough.

## Receive your first call

When someone calls your number, Unpod posts the call to your answer URL and runs the XML you
reply with.

> [!NOTE]
>
> Inbound configuration needs the Call API switched on for your org: until then `set_did` (or
> the `PUT` in step 4) returns `403 call_api_disabled`. Ask the Unpod team to switch it on.

**1. Get your webhook secret**

One signing key for all call events in your org. Create it once with `rotate()` (org
owner or editor) - the only call that returns it in full - and store it as
`UNPOD_WEBHOOK_SECRET`:

> [!WARNING]
>
> Rotating invalidates the previous secret, so any server already verifying with it stops
> working.

```python Python theme={null}
from unpod import Client

with Client() as client:
    s = client.telephony.webhook_secret.rotate()
    print(s.pushed_to_sbc)  # False: still syncing, Unpod retries it
    print(s.secret)         # shown in full only here
```

```bash cURL theme={null}
curl -s -X POST "https://unpod.ai/api/v2/platform/sbc/webhook-secret/rotate" \
  -H "Authorization: Token $UNPOD_PLATFORM_TOKEN" -H "Org-Handle: $UNPOD_ORG_HANDLE" \
  -H "Content-Type: application/json" -d '{}'
```

If `pushed_to_sbc` comes back `false`, Unpod retries the update in the background. Until it
lands, events aren't signed with the new secret and your server answers `401`, so wait a
minute before the test call.

**2. Run a server**

```bash theme={null}
pip install fastapi uvicorn
```

```python app.py theme={null}
import os
from fastapi import FastAPI, HTTPException, Request, Response
from unpod.telephony import WebhookSignatureError, parse_call_event

SECRET = os.environ["UNPOD_WEBHOOK_SECRET"]
app = FastAPI()


@app.post("/answer")
async def answer(request: Request):
    try:
        ev = parse_call_event(await request.body(), request.headers, secret=SECRET)
    except WebhookSignatureError:
        raise HTTPException(401)

    if ev.event == "StartApp":               # a new inbound call is ringing
        xml = "<Response><Speak>Hello! Thanks for calling.</Speak><Hangup/></Response>"
        return Response(xml, media_type="application/xml")
    return Response(status_code=200)         # Hangup / RecordStop: acknowledge


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)
```

Run it with `python app.py`. It rejects unsigned requests, greets new calls and returns 200
for every other event.

**3. Expose it**

Install ngrok and add your authtoken first. Unpod only calls public https URLs. In a second
terminal:

```bash theme={null}
ngrok http 8000
```

Copy the https forwarding URL, e.g. `https://abc123.ngrok.app`.

**4. Point your number at it**

Run this in another terminal (the server and ngrok hold the first two), with
`UNPOD_BASE_URL`, `UNPOD_PLATFORM_TOKEN` and `UNPOD_ORG_HANDLE` exported:

```python Python theme={null}
from unpod import Client
from unpod.telephony import InboundConfig

URL = "https://abc123.ngrok.app/answer"  # your ngrok URL + /answer

with Client() as client:
    client.telephony.inbound.set_did("+919000000001", InboundConfig(
        answer_url=URL,
        answer_method="POST",
        status_callback_url=URL,
    ))
```

```bash cURL theme={null}
curl -s -X PUT "https://unpod.ai/api/v2/platform/sbc/v1/dids/%2B919000000001/inbound" \
  -H "Authorization: Token $UNPOD_PLATFORM_TOKEN" -H "Org-Handle: $UNPOD_ORG_HANDLE" \
  -H "Content-Type: application/json" \
  -d '{"answer_url": "https://abc123.ngrok.app/answer", "answer_method": "POST",
       "status_callback_url": "https://abc123.ngrok.app/answer"}'
```

`set_did` sets the config for one number. In the URL, `+` becomes `%2B`.

**5. Call your number**

Call your Unpod number from any phone. You hear "Hello! Thanks for calling." and the call
ends. For the full picture, see [Incoming Calls](https://docs.unpod.ai/telephony/apis/incoming-calls.md).

## Next

> [card] [Multiple Participants](https://docs.unpod.ai/telephony/apis/multiple-participants.md)
>
> Bridge two people on one call.

> [card] [Answer URL & XML](https://docs.unpod.ai/telephony/apis/answer-url.md)
>
> Reply to inbound calls with XML or JSON verbs.

> [card] [Incoming Calls](https://docs.unpod.ai/telephony/apis/incoming-calls.md)
>
> Route inbound calls to your app or an Unpod agent.

> [card] [Call Blasting](https://docs.unpod.ai/telephony/apis/call-blasting.md)
>
> Call many numbers at once.


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