Голосовий AI-асистент на Vapi, Zadarma та Hermes Agent
Я хотів диктувати нотатки за кермом без рук на телефоні — і зрештою навчив свого AI-агента телефонувати мені самому. Історія про Vapi, український номер Zadarma, дорогі помилки на старті і покроковий рецепт, як зібрати те саме собі.
+380671234567), — вигаданий і слугує лише ілюстрацією; це не реальний номер.Стою на перехресті, тримаю телефон і надиктовую голосове повідомлення Сюзанні — думка щойно прийшла в голову, і якщо не зафіксувати її зараз, за хвилину вона розчиниться серед інших думок. Добре, що я стою на перехресті і чекаю і можу взяти телефон в руки. А коли світло зелене і руки на кермі — думка так і лишається незафіксованою. А до наступного перехрестя її вже можна і розплескати.

Навіщо мені це було потрібно
Ця проблема не нова для мене. Я вже намагався її вирішити через Shortcut на CarPlay — хотів відправляти голосові чи текстові повідомлення Сюзанні прямо з машини, одним дотиком, без телефону в руках. Технічно не склалось: Telegram Bot API не дає відправити повідомлення від імені користувача, тільки від бота, а бот, що пише сам собі, — це вже інша задача. Ідея так і лишилась відкладеною. Я знаю, як це можна реалізувати, проте шлях не простий.
А потім я побачив в інтернеті, що люди роблять AI-агентів, які самі телефонують — домовляються про зустрічі, записуються до лікаря. І це раптом оживило мою стару надію: якщо агент може дзвонити комусь замість мене, чому б йому не подзвонити мені? Або чого я не можу подзвонити своєму агенту за допомогою голосовго набору номеру через CarPlay? Тоді диктувати за кермом можна буде в телефонну розмову, а не в Telegram-повідомлення, яке треба ще й тримати в руках, щоб записати.
Був і другий мотив, чесніший: азарт. Чи вийде взагалі це зібрати — з українським номером, за адекватні гроші, і щоб не соромно було показати результат? Це і потягнуло мене в експеримент з Vapi, з якого зрештою вийшла Сюзанна, якою вона працює зараз.
Головна вимога лишалась незмінною: асистент має працювати з українськими номерами, коштувати адекватних грошей і бути повністю під моїм контролем.
Як усе це влаштовано
Коли я вперше розклав усе на схему, мене самого здивувало, скільки шарів довелось зібрати заради, здавалось би, простого «подзвони і скажи». Високорівнево стек виглядає так:

Кожен шар я обирав окремо, часто методом проб і помилок (детальніше — у секції «Граблі» нижче). Ось що вийшло в результаті:
- Hermes Agent (Susanna) — мій персональний AI-агент. Ініціює вихідні дзвінки через MCP-протокол, обробляє результати, веде базу знань в Obsidian.
- Vapi AI — голосова AI-платформа. Приймає голос, розпізнає мову (STT), пропускає через LLM, синтезує відповідь (TTS). Дає MCP-сервер для програмного керування.
- Zadarma — український VoIP-провайдер. Надає SIP-транк і віртуальний номер +380. Дешевше ніж міжнародні дзвінки в ~7 разів.
- Webhook (LXC) — легкий HTTP-сервер на Python. Приймає сповіщення від Vapi після завершення дзвінка.
- Obsidian vault — база знань. Нотатки дзвінків, профілі людей, daily notes.
Як Hermes навчився дзвонити
Найбільше мене зачепило те, що Hermes не потребує окремого «модуля дзвінків», який я мав би писати й підтримувати сам. Vapi AI надає MCP (Model Context Protocol) сервер — це стандартизований API, який дозволяє AI-агентам безпосередньо використовувати Vapi як набір інструментів. Я просто підключив його до конфіга Hermes — і агент одразу «навчився» дзвонити, без жодної додаткової логіки з мого боку.
На практиці це виглядає так:
# Hermes Agent робить це автоматично, але за лаштунками відбувається:
mcp_vapi_create_call(
assistantId="5bd4fcc6-...",
phoneNumberId="477e736a-...",
customer={"number": "+380671234567"},
assistantOverrides={
"variableValues": {
"інструкція": "Запитай чи зможе зустрітись у суботу"
}
}
)Ключова фіча — динамічні інструкції. Кожен дзвінок має своє завдання, яке передається через змінну {{інструкція}} в системному промпті асистента:
Головне правило: виконай те, що вказано в {{інструкція}}.Для мене це виявилось найкориснішою деталлю в усій інтеграції: замість того щоб тримати десяток окремих асистентів, я тримаю одного — а сценарій підставляється щоразу новий. Привітання, нагадування, опитування, домовленість про зустріч — усе через ту саму {{інструкція}}.

MCP сервер надає 13 інструментів: створення дзвінка, список асистентів, отримання транскрипта, керування номерами тощо. Інтеграція в Hermes — це один блок у конфігу:
mcp_servers:
vapi:
url: "https://mcp.vapi.ai/mcp"
headers:
Authorization: "Bearer YOUR_VAPI_API_KEY"Скільки це насправді коштує
Найболючіше питання. Починали з Twilio (міжнародний US-номер) — $1.00/хв на термінацію в Україну. Це непідйомно для регулярного використання.
Після переходу на Zadarma:
| Складова | Вартість/хв |
|---|---|
| Vapi AI (транскрибація + модель + голос) | ~$0.10 |
| Zadarma (термінація на UA номери) | ~$0.14 |
| Разом | ~$0.24/хв |
Для порівняння: Twilio давав ~$1.10/хв. Економія — 4.5 рази.
Якщо оптимізувати Vapi (GPT-4o-mini замість GPT-4o, легший голос) — можна знизити до $0.18/хв.
Фіксовані витрати: номер Zadarma — 90 грн/міс (~$2).
Один дзвінок на 30 секунд коштує ~$0.07. Цілком прийнятно для персонального асистента. Ось як ця цифра розкладається, якщо рахувати всю автоматизацію навколо дзвінка, а не тільки сам виклик:
| Етап | Вартість |
|---|---|
| Дзвінок (30с, Vapi + Zadarma) | ~$0.07 |
| Транскрипт + summary (включено в Vapi) | $0 |
| Поллінг (наш сервер) | $0 |
| Call Handler (owl-alpha, безкоштовний) | $0 |
| Переклад (deepseek, ~$0.0001) | ~$0 |
| Разом | ~$0.07 |
Найбільша частина — сам дзвінок. Уся автоматизація навколо — практично безкоштовна.

Вебхук, поллінг і Call Handler
Це найцікавіша частина, де Hermes Agent показує себе як платформа автоматизацій.
Проблема
Коли Vapi завершує дзвінок, він надсилає POST на вебхук. Але транскрипт і підсумок розмови не приходять одразу — Vapi генерує їх асинхронно, з затримкою до кількох хвилин.
Рішення
Архітектура з трьох кроків:
1. Вебхук (Python HTTP сервер)
Приймає подію ended від Vapi. Одразу запускає фоновий потік для поллінгу і повертає 200 — не блокує Vapi.
# Спрощено — насправді це threading + HTTP сервер
def webhook_handler(event):
call_id = event["message"]["call"]["id"]
thread = Thread(target=poll_and_process, args=(call_id,))
thread.start()
return {"status": "ok"}2. Поллінг транскрипту
Кожні 30 секунд запитує Vapi REST API, доки не з'явиться transcript. Максимум 20 спроб (~10 хвилин). Якщо дзвінок не відбувся (зайнято, немає відповіді) — нотатка створиться без транскрипту одразу.
def poll_and_process(call_id):
for attempt in range(20):
time.sleep(30)
call_data = fetch_vapi_call(call_id)
if call_data.get("transcript"):
break
process_call(call_data)3. Call Handler (субагент Hermes)
Коли транскрипт отримано, вебхук:
- Створює нотатку в
Calls/YYYY/MM/YYYY-MM-DD_HH-MM-SS.md - Запускає легкого субагента Hermes (
hermes chat -q) - Субагент читає нотатку, знаходить людину в базі, збагачує аналізом
- Виконує дії (нагадування, записи в daily note)
- Вебхук надсилає фінальний звіт в Telegram
Пропущені дзвінки
Якщо дзвінок не відбувся (customer-busy, no-answer) — вебхук одразу створює нотатку і сповіщає в Telegram. Без поллінгу, без очікування.
Куди зникає дзвінок, коли розмова закінчилась
Нотатка в Obsidian
Мені було важливо, щоб жоден дзвінок не зникав у нікуди — тому кожен з них я перетворив на структуровану нотатку в Obsidian vault, в каталозі Calls/YYYY/MM/. Це той самий принцип, на якому в мене тримається весь vault: якщо подія не залишила слід у нотатці — вважай, що її не було.
Приклад:
---
type: call
date: "2026-05-29"
time: "14:26"
timezone: Europe/Kyiv
caller: "+380671234567"
direction: "inbound"
cost: 0.0694
ended_reason: "customer-ended-call"
person: "[[Volodymyr Lavrynovych]]"
tags: [type/call]
---
# Дзвінок 2026-05-29 14:26
## Підсумок
[[Volodymyr Lavrynovych|Вова]] попросив Сюзанну
зателефонувати його дружині Вікі.
## Розмова
AI: Привіт, Володимире!
User: Привіт, подзвони Вікі...
## Аналіз
- Хто: Вова (+380671234567)
- Що просив: подзвонити Вікі
## Дії
- ✅ Дзвінок до Віки ініційованоШаблон нотатки визначено в Templates/Нотатка дзвінка.md. Call Handler збагачує нотатку секцією ## Аналіз та ## Дії. Пошук людини за номером — через People/ профілі (поле phone_primary і phones).
Хто дзвонив
Одна з речей, яку я хотів отримати від самого початку — щоб мені не доводилось вручну підписувати, хто дзвонив. При отриманні дзвінка вебхук автоматично знаходить людину в базі за номером телефону:
- Спеціальний випадок: мій власний номер (+380671234567) — одразу резолвиться
- Пошук по People/ — кожен файл має
phone_primary:іphones:[]в frontmatter - Якщо не знайдено — Call Handler визначає з контексту розмови
Завдяки цьому вікілінк на людину в нотатці з'являється сам, і кожен дзвінок одразу лягає в загальну картину — без того, щоб я щоразу відкривав vault і прописував зв'язки руками.
Звіт у Telegram



Реальний Telegram-звіт про дзвінок з ресторану «Йоші» — підсумок, аналіз з діями і повний транскрипт
Мені важливо було не відкривати vault щоразу, коли хочеться перевірити, що там за дзвінок був — тому весь підсумок я хотів бачити одразу в телефоні, там, де й так постійно сиджу. Після повної обробки вебхук надсилає одне повідомлення в Telegram:
📞 Дзвінок завершено
👤 [[Person]]
Напрямок: вхідний | Номер: +380671234567
Тривалість: 38с | Вартість: $0.07
Підсумок:
(українською, перекладено через OpenRouter)
Транскрипт:
AI: ...
User: ...
📝 Нотатка дзвінкаОдин момент мене трохи зачепив: підсумок (summary) Vapi повертає тільки англійською, а я хочу читати звіт про власний дзвінок українською, не перемикаючись подумки на переклад щоразу. Тож вебхук перекладає його через OpenRouter (deepseek/deepseek-v4-flash) перед відправкою — дешево і швидко, і я навіть не помічаю цей крок, поки читаю повідомлення.
Як зібрати те саме собі
Якщо дочитали до цього моменту і теж хочете спробувати — пройдусь з вами по кроках так, як проходив їх сам. Попереджаю одразу: місцями буде трохи занудно (API, curl, конфіги), але кожен крок — саме те, на чому я сам спіткнувся чи навпаки здивувався, що спрацювало з першого разу.
Частина 1: Zadarma — отримуємо український номер
Починаємо з номера, бо без нього все інше не має сенсу.
- Реєструємось на Zadarma.
- Проходимо верифікацію: фото з паспортом + листочком з датою (саме тут я застряг на два дні — детальніше в секції «Граблі» нижче).
- Купуємо віртуальний номер +380 (90 грн/міс — не найдешевше на ринку, але прийнятно для персонального проєкту).
- У налаштуваннях номера знаходимо Зовнішній сервер (SIP URL) і вписуємо туди адресу, яка й з'єднає Zadarma з Vapi:
[email protected]- Створюємо SIP-підключення і зберігаємо логін з паролем — вони знадобляться вже на наступному кроці.
Частина 2: Vapi AI — створюємо асистента
Тепер даємо номеру мозок.
- Реєструємось на Vapi.ai.
- Створюємо асистента. Тут є вибір, і я обрав так:
- Модель: GPT-4o (якщо хочете економити — GPT-4o-mini впорається теж)
- Голос: Clara (ElevenLabs)
- Транскрибація: Deepgram nova-2
- Системний промпт з
{{інструкція}}
- Далі додаємо SIP Trunk через API — робиться це одним запитом:
curl -X POST "https://api.vapi.ai/credential" \
-H "Authorization: Bearer $VAPI_TOKEN" \
-d '{
"provider": "byo-sip-trunk",
"gateways": [{"ip": "185.45.152.180"}],
"outboundAuthenticationPlan": {
"authUsername": "LOGIN",
"authPassword": "PASSWORD"
}
}'- Тим самим способом реєструємо сам номер:
curl -X POST "https://api.vapi.ai/phone-number" \
-H "Authorization: Bearer $VAPI_TOKEN" \
-d '{
"provider": "byo-phone-number",
"number": "+380XXXXXXX",
"credentialId": "SIP_TRUNK_ID"
}'- І останній штрих цієї частини — прив'язуємо асистента до номера прямо в Dashboard.
Частина 3: Webhook — обробка дзвінків
Номер дзвонить, асистент відповідає — але щоб розмова не зникла безслідно після завершення виклику, потрібен вебхук. У мене це Python-скрипт, який просто слухає на порту 5050, крутиться на LXC-контейнері і дивиться назовні через Cloudflare Tunnel.
# call-webhook.py — спрощена схема
from http.server import HTTPServer
import threading, time, json
class CallWebhookHandler(BaseHTTPRequestHandler):
def do_POST(self):
# Отримуємо подію від Vapi
body = self.rfile.read()
event = json.loads(body)
# Запускаємо обробку в фоні
thread = threading.Thread(
target=process_call_async,
args=(event["message"]["call"]["id"],)
)
thread.start()
self.send_response(200)Це справді спрощена схема — повний код можу надати, якщо звернетесь до мене в особисті повідомлення, це якщо захочете відтворити один в один. А так то ваш AI-асистент має і сам гарно впоратись з такою простою задачею.
Частина 4: Hermes Agent — фінальний клей
Останній крок — зробити так, щоб Hermes Agent взагалі знав, що вміє дзвонити. Для цього він має бути налаштований з Vapi MCP сервером у config.yaml. Далі все просто: дзвінок ініціюється звичайною командою, на кшталт
Подзвони Роману і привітай зі святома Hermes сам знаходить номер в phone_primary, створює дзвінок через MCP — і після завершення розмова вже автоматично обробляється вебхуком, без мого втручання.
Граблі, на які я наступив
1. Twilio Trial — дорогий урок
Перший дзвінок одразу впав з помилкою call.start.error-get-transport. Я налаштовував і тестував усе самостійно, без чужої покрокової інструкції — тож ці граблі, здається, були неминучі: Trial-акаунт Twilio просто не пускає дзвінки на непідтверджені номери. Роздратування минуло швидко, як тільки знайшов причину — поповнив Twilio на $20, і дзвінки пішли. Але, фінально сервіс Twilio був дорогим і я відмовився від нього, а ці $20 так там і зависли.
2. Міжнародні дзвінки — $1/хв
Twilio US → Ukraine коштує ~$1/хв. Для регулярних дзвінків — неприйнятно. Рішення: Zadarma + SIP trunk.
3. Верифікація Zadarma — 2 дні
А от тут дратувало по-справжньому. Я доволі технічно підкована людина, а верифікація в Zadarma все одно розтягнулась майже на два дні: автозаповнення браузера перезаписало дані в профілі, документ відхиляли повторно, довелось окремо пояснювати в чаті підтримки мету використання номера. За ці два дні очікування я трохи не втратив творчий запал — ризик перегоріти посеред експерименту виявився реальнішим, ніж будь-яка технічна проблема з коду.
4. Вхідні дзвінки через SIP — довго налаштовували
Проблема: Zadarma не міг достукатись до Vapi SIP. Рішення: правильний формат номера в Zadarma — [email protected].
5. Транскрипт не приходить одразу
Vapi не включає transcript в вебхук — потрібен додатковий API-запит. Рішення: поллінг кожні 30 секунд.
6. Підсумок англійською
Vapi генерує summary тільки англійською. Рішення: автоматичний переклад через OpenRouter.
7. Два повідомлення в Telegram
Спочатку вебхук слав проміжне "обробляється...", потім Call Handler — фінальне. Я подивився на це і зрозумів, що то занадто шумно для мене в чаті. Рішення: одне повідомлення, після повної обробки. Так, це буде з затримкою, але вже з повним контекстом.
8. "Номер незнайомий" — перший дзвінок абоненту
Перший дзвінок з нового номера (Zadarma) до контакта — людина може не відповісти, бо не знає номер. Рішення: в інструкції до дзвінка додавати фразу "це дзвінок з нового номера Вови", або "Привіт, це асистент Вови".
З чого складається вебхук
Коли я вже розібрав вебхук на частини, зловив себе на думці: жодна з цих функцій сама по собі не складна, а разом вони раптом склались у щось, що виглядає майже як маленька платформа. Ось з чого він складається (call-webhook.py):
| Функція | Призначення |
|---|---|
fetch_vapi_call() | Отримує дані дзвінка з Vapi REST API (через curl — Python urllib блокується) |
find_person_by_number() | Шукає людину в People/ за номером телефону |
create_call_note() | Створює нотатку дзвінка з шаблону |
spawn_call_handler() | Запускає субагента для збагачення нотатки |
translate_text() | Перекладає summary українською через OpenRouter |
send_telegram() | Надсилає звіт у Telegram |
process_call_async() | Основний пайплайн: поллінг → нотатка → обробка → звіт |
Call Handler — це ще один екземпляр Hermes Agent, запущений з мінімальним контекстом:
hermes chat -q "Інструкція..." \
--model openrouter/owl-alpha \
-s vault-search,obsidianВін використовує безкоштовну модель (owl-alpha) з fallback на deepseek-v4-flash — і мене досі трохи бавить, що аналіз кожного дзвінка виходить практично задарма. Його завдання — тільки збагатити нотатку, не надсилати Telegram.
За кермом — чи спрацювало те, з чого все почалось
Обіцянку з початку статті я вже перевірив наживо: диктувати за кермом дійсно можна, дзвінок приймає думку так само надійно, як текстове повідомлення, тільки без тримання телефону в руках.
Ось один із таких дзвінків:
«Запиши, що в ресторані Йоші по понеділках 1+1 акція, замовляєш один рол, другий у подарунок. І нагадай мені на наступний понеділок поїсти там суші»
Сюзанна відповіла «Добре, зроблю» — і обидва завдання (нотатка про акцію і нагадування) лягли в систему без жодного дотику до екрана. Це саме той сценарій, ради якого все затівалось.
Але не кожен дзвінок пройшов так гладко. В іншому тесті я попросив: «передзвони мені через півгодини і скажи, щоб я посвідався» — і транскрипція перетворила останнє слово на щось нерозбірливе. Навіть Call Handler, розбираючи нотатку, чесно написав в аналізі, що не певен, що я мав на увазі.
Це не крах ідеї, а її реалістична межа: розпізнавання мови непогано справляється з чітким запитом у тихому приміщенні і трохи гірше — зі швидкою фразою в машині з фоновим шумом. Виправити це — окрема робота: тонше налаштування моделей транскрибації у Vapi, а не те, що вирішується самим фактом підключення дзвінка. Але базовий сценарій — подзвонити собі й зафіксувати думку — вже працює, і це головне.
Але це закриває тільки половину задуму. Пригадайте початок статті: ідея ожила саме тоді, коли я побачив агентів, що самі телефонують і домовляються про зустрічі замість людини. Дзвінки собі — це та половина, що вже працює. Друга половина — щоб Сюзанна дзвонила іншим людям і домовлялась за мене — досі попереду. Технічно дзвінок вона вже вміє ініціювати, а от надійно вести розмову з чужою людиною й довести домовленість до кінця — це геть інший рівень: якісніше налаштування AI в самому Vapi і чіткіші, детальніші інструкції, ніж просте «подзвони і привітай». Такий тюнінг голосового асистента — це те, чим мені ще треба зайнятись окремо.
Висновки
Зараз, коли Сюзанна вже сама піднімає слухавку, я інколи забуваю, з чого все почалось — з думки, яка розчинялась десь на перехресті, поки я тримав руки на кермі. Той самий дзвінок, який мене колись дратував затримкою верифікації і Trial-обмеженнями Twilio, зараз просто працює: приймає вхідні на український номер, дзвонить кому скажу, сам пише нотатку, сам перекладає підсумок, сам знаходить, кому я дзвонив.
Найбільше задоволення мені принесло не те, що воно зрештою запрацювало — а те, що весь цей стек зібрався з частин, які кожна сама собою прості: MCP, вебхук, поллінг, легкий субагент. Складність з'явилась не тому, що задача була складна, а тому що я збирав це вперше і напомацки.
Далі хочу спробувати дешевшу модель для щоденних дзвінків і, мабуть, придумати Сюзанні ще пару сценаріїв — щоб дзвінок був не тільки способом зафіксувати думку, а й способом щось реально зробити за мене.
Ще одна лінія, яку хочу дослідити — LiveKit Agents як заміна Vapi. Десь в Instagram-коментарях трапилась людина, яка зібрала схожого голосового агента на зв'язці Zadarma SIP → LiveKit SIP → LiveKit Agents → OpenAI Realtime і заявляла ціну до $0.03/хв. Цифра здається мені підозріло низькою: якщо це справді GPT-4o Realtime, аудіо на вхід і вихід саме по собі коштує більше — швидше за все, там або локальний STT/TTS, або дешевший LLM, або ціна рахувалась без самої моделі. Але сама ідея хостити LiveKit самостійно — цікава: платформного «податку» Vapi ($0.05–0.07/хв) не буде, і контроль повний — сам вирішуєш, який STT/TTS/LLM ставити і як обробляти помилки. Плата за це — більше devops-роботи: власний сервер, SIP-транк, свій пайплайн замість готового API. Vapi + Zadarma — правильний старт, коли хочеш швидко перевірити ідею; якщо захочеться дешевше і з повним контролем — LiveKit Agents як наступний крок.
То ж бажаю вам вдалих експериментів і щоб у вашого асистента був не лише характер, але й голос. Дякую за цікавість.