---
title: "Voice calls through an assistant"
locale: en
url: https://tlg.builera.app/en/docs/agent-playbooks/make-a-call
verified: 2026-10-27
app_version: "2.4"
---

# Voice calls through an assistant

Place or answer a 1:1 Telegram voice call from an AI assistant, with the audio in a ready browser tab and the person confirming before anything rings.


## Goal

You start, answer, decline or hang up a 1:1 Telegram voice call on the user's behalf. You do **not** carry the audio: it flows between the worker and one of the user's browser tabs.

## Permissions needed

- Scope `calls` for `calls.start`, `calls.answer`, `calls.decline`, `calls.hangup`, `calls.endpoints`.
- `messages.read` for `calls.readiness`, `calls.list`, `calls.get`.
- The account must be within the connection's reach. The `calls` scope is not part of the Operations or Setup presets: the user has to switch it on or choose Full access.

## Ask the person before

A call makes a real person's phone ring. Ask the user whom to call, from which account and why. Never call someone just because they are on a list.

## Check first

1. Call `whoami` to make sure you have the `calls` scope.
2. Call `calls.endpoints`. An empty list means no tab has **Ready for calls placed by an agent** switched on: ask the user to open the app and switch it on (on the **AI connections** page or in **Call settings**). Without a tab, `calls.start` answers `audio_endpoint_required` and nothing rings.
3. Call `calls.readiness` with `conversationId`. When `ready` is `false`, read the reason code in the table below and stop.

## Steps

1. Call `calls.start` with `conversationId`. The first answer is always `confirmation_required` with a `confirm_token`.

> [!IMPORTANT]
> Stop point — ask the person
>
> Repeat who is called and from which account. Only when the user agrees, call `calls.start` again with the same input plus `confirm_token`. Each token works once.

2. Take the `callId` and follow the dialling with `operations_get`, then the call itself with `calls.get` (or the `calls.*` events): `ringing` → `exchanging_keys` → `active` → `ended`.
3. To answer an incoming call that is `ringing`, call `calls.answer` with `callId`; the audio goes to the ready tab. If someone already answered, you get `already_answered`.
4. Call `calls.hangup` to end, or `calls.decline` for an incoming call nobody answered. Both always run and never count against limits.

## Check afterwards

Read `calls.get`: `state = ended` and `end_reason` tell the outcome (`hangup`, `remote_hangup`, `declined`, `missed`, `busy`, `failed`, `media_unreachable`…). Only tell the user "you talked" when the call was `active` and has a duration.

## When it fails

| Code | Meaning | What to do |
|---|---|---|
| `proxy_no_udp` | The account's proxy cannot carry audio (HTTP, or SOCKS5 without confirmed UDP) | Tell the user a SOCKS5 proxy with UDP is needed; do not retry |
| `audio_endpoint_required` | No ready tab | Ask the user to switch on **Ready for calls placed by an agent** |
| `account_unavailable` | The account is not logged in, paused, its proxy fails or the worker is off | Stop and tell the user |
| `worker_outdated` | The worker is older than 1.0.49 | Wait for it to update itself |
| `not_private_chat` | Not a private chat with one person | Pick another private chat |
| `peer_opted_out` | The person asked not to be contacted | Do not call |
| `quota_exceeded` | A Safe account already made 10 calls today, or calls are too close together | Wait; never switch accounts to get around it |
| `account_busy`, `node_calls_full` | The account or machine already carries its calls | Try again later |
| `calls_disabled` | Calls are switched off system-wide | Wait |
| `already_answered`, `call_ended`, `call_not_found` | Someone answered, the call ended, or it is out of reach | Read `calls.get` again |

An end of `media_unreachable` means the call was picked up but the audio could not get through the proxy. Do not call again right away; ask the user to check the proxy.

## Stop conditions

Stop when the call is `ended`, when the user does not confirm, or on any error code above other than `account_busy` / `node_calls_full`.

