Інтеграція CRM-систем з PearlPBX2: технічний посібник
Версія: 2.7.2
Цей документ описує процедуру інтеграції зовнішньої CRM-системи з платформою PearlPBX2. Він призначений для розробників, які не мають попереднього досвіду роботи з телефонними системами, і містить повний обсяг інформації, необхідної для реалізації інтеграції: модель подій, формат переданих даних, повний опис REST API (включно з ініціалізацією вихідних дзвінків), механізм отримання аудіозаписів розмов та вимоги до перевірки автентичності запитів.
Для роботи з документом достатньо базових навичок реалізації HTTP-обробників (webhook-ендпоінтів) та виконання автентифікованих запитів до REST API. Додаткових знань предметної області телефонії не вимагається.
Документ є самодостатнім джерелом інформації: усі відомості, потрібні для реалізації інтеграції, викладені нижче, без необхідності звертатися до додаткових джерел.
Зміст
- Загальна схема взаємодії
- Термінологія
- Розподіл відповідальності сторін
- Життєвий цикл дзвінка
- Опис подій та їх полів
- Отримання запису розмови
- REST API PearlPBX2
- Dashboard API та WebSocket: канал реального часу (опційно)
- Перевірка автентичності запитів
- Налаштування власного формату JSON
- Поведінка системи при збоях доставки
- Приклад реалізації сервера-приймача
- Питання, що часто виникають
- Контрольний список інтеграції
1. Загальна схема взаємодії
PearlPBX2 — система управління телефонією підприємства: приймає вхідні дзвінки, розподіляє їх між операторами через черги обслуговування та здійснює запис розмов. Для цілей інтеграції CRM-системі потрібні три відомості: момент початку дзвінка, момент його завершення та розташування аудіозапису (якщо запис здійснювався).
Інтеграція реалізована на основі двох механізмів:
- Веб-хуки (webhooks) — PearlPBX2 надсилає повідомлення на вказаний CRM-системою URL у момент настання події (початок дзвінка, завершення дзвінка, пропущений виклик у черзі). Запит має тип
POSTз тілом у форматі JSON. Ініціювання запиту з боку CRM не потрібне — достатньо забезпечити прийом вхідних запитів на визначеному ендпоінті. - REST API — файл аудіозапису не передається безпосередньо через веб-хук з огляду на обсяг даних. Замість файлу веб-хук містить посилання на нього. Отримання файлу здійснюється окремим запитом до API з використанням токена доступу.
Розділи нижче описують кожен з цих механізмів детально.
2. Термінологія
- Дзвінок (call) — телефонний виклик від моменту надходження до моменту завершення.
- uniqueid — унікальний ідентифікатор одного каналу Asterisk, наприклад:
1753000000.42. Не є номером телефону чи ідентифікатором клієнта. Присвоюється в момент створення каналу. Є ключем для зіставлення подій одного й того самого ланцюга (call.incoming→call.answered/call.missed→call.ended, абоcall.outgoing→call.outgoing_answered→call.outgoing_ended) — усі вони стосуються одного каналу й несуть однаковийuniqueid. - linkedid — ідентифікатор, спільний для всіх каналів одного логічного дзвінка (наприклад, обох ніг внутрішнього дзвінка між двома співробітниками — кожна нога має власний
uniqueid, але однаковийlinkedid, що дорівнюєuniqueidканалу, який дзвінок ініціював). Якщо CRM потрібно об’єднати кілька окремих webhook-подій (наприклад, дваcall.outgoingвід двох різних SIP-користувачів) в один запис про дзвінок — слід порівнювати самеlinkedid, а не покладатися на близькістьuniqueidчиtimestamp. - Caller ID — номер (і за наявності — ім’я) абонента, який здійснює виклик. Передається у полях
caller_id_num/caller_id_name. - Контекст (context) та внутрішній номер (exten) — параметри внутрішньої маршрутизації АТС, що визначають напрямок і кінцеву точку дзвінка. Для інтеграції CRM зазвичай достатньо знати про існування цих полів без деталізації логіки dialplan.
- Черга (queue) — якщо підприємство використовує розподіл дзвінків між кількома операторами, виклик спершу потрапляє в чергу обслуговування, з якої передається вільному оператору. Якщо дзвінок здійснюється напряму, без черги, поле
queueматиме значенняnull. - Запис розмови (recording) — аудіофайл дзвінка, за умови що функція запису увімкнена в налаштуваннях конкретної АТС (застосовується не завжди і не до всіх дзвінків).
- Причина завершення (hangup cause) — код і текстовий опис причини завершення дзвінка (нормальне завершення, зайнята лінія, відсутність відповіді тощо). Використовується переважно для аналітичних цілей.
- SIP-користувач — внутрішній номер співробітника (телефон, софтфон), зареєстрований в АТС. Коли такий співробітник сам ініціює дзвінок, це породжує події вихідного ланцюга (
call.outgoing, розділ 5.5). - Транк (trunk) — з’єднання АТС із зовнішнім телефонним провайдером, через яке приходять дзвінки від клієнтів і йдуть дзвінки назовні на звичайні номери. Дзвінки, що надходять через транк, завжди належать до вхідного ланцюга (
call.incoming), навіть якщо технічно це вихідний виклик провайдера — з точки зору CRM-інтеграції значення має те, хто ініціює дзвінок усередині АТС (співробітник чи зовнішній абонент), а не фізичний напрямок сигналу на лінії.
3. Розподіл відповідальності сторін
- Адміністратор PearlPBX2 створює конфігурацію веб-хука в адміністративній панелі АТС: вказує URL сервера CRM, перелік подій для надсилання та, за потреби, секретний ключ для підпису запитів. Ця дія виконується виключно на стороні АТС; від розробника CRM потрібне лише надання URL та, за потреби, узгодження секретного ключа.
- Розробник CRM реалізує HTTP-ендпоінт для прийому
POST-запитів з тілом у форматі JSON і, за потреби, здійснює запити до API PearlPBX2 для отримання файлів записів — для цього видається окремий токен доступу.
Таким чином, взаємодія відбувається у двох напрямках: вхідному (веб-хуки, що надходять від АТС) та вихідному (запити CRM до API для отримання записів).
4. Життєвий цикл дзвінка
Розглянемо послідовність подій на прикладі одного дзвінка. Клієнт здійснює виклик до служби підтримки підприємства.
Крок 1. Дзвінок надходить до системи. За умови відповідності критеріям, визначеним адміністратором (певний вхідний напрямок або черга), на сервер CRM надсилається подія call.incoming. Повідомлення містить uniqueid, номер абонента та попередню оцінку (prediction) щодо здійснення запису (детально — у розділі 6).
На цьому етапі CRM-система, як правило, відображає картку поточного дзвінка — наприклад, спливаюче сповіщення оператору або чернетку запису в історії взаємодій з клієнтом.
Крок 2. Дзвінок надходить до черги обслуговування, абонент очікує з’єднання з оператором. Можливі два варіанти розвитку подій.
Варіант А: оператор приймає виклик. Надсилається подія call.answered, яка містить ім’я оператора, його внутрішній номер, тривалість дзвінка до відповіді та час очікування абонента в черзі. Ця подія призначена для відображення картки клієнта оператору в момент з’єднання.
Варіант Б: абонент завершує виклик, не дочекавшись відповіді. Надсилається подія call.missed (пропущений виклик). Ця подія надходить незалежно від того, чи підписана CRM-система на подію call.incoming — пропущений виклик розглядається як самостійно значуща подія.
Крок 3. Після завершення розмови (незалежно від того, відбулась вона одразу чи після кроку 2А) надсилається подія call.ended. Вона містить тривалість розмови, причину завершення, дані оператора, який прийняв виклик (за наявності події call.answered), а також посилання на аудіозапис, якщо розмова записувалась.
Ключове правило: подія call.ended надсилається виключно для дзвінків, про які раніше було повідомлено подією call.incoming. Система відстежує цей стан внутрішньо, тому отримання події завершення без попередньої події початку виключене, так само як і повторне надсилання події завершення для одного дзвінка. Події call.missed та call.answered, на відміну від цього, є самостійними та надходять незалежно від підписки на call.incoming.
Описаний вище ланцюг (call.incoming → call.answered/call.missed → call.ended) стосується дзвінків, що надходять до підприємства. Для дзвінків, які ініціює співробітник (наприклад, оператор передзвонює клієнту), існує окремий, повністю незалежний ланцюг подій.
Крок 1’ (вихідний дзвінок). Співробітник бере слухавку і набирає номер. На сервер CRM надсилається подія call.outgoing — аналог call.incoming, але для дзвінка, ініційованого зсередини АТС.
Крок 2’ (вихідний дзвінок). Якщо викликана сторона піднімає слухавку, надсилається подія call.outgoing_answered з часом відповіді. Якщо лінія зайнята, ніхто не відповідає, або виклик скасовано — ця подія не надсилається взагалі.
Крок 3’ (вихідний дзвінок). Після завершення дзвінка (незалежно від того, відповіли на нього чи ні) надсилається подія call.outgoing_ended. Вона містить поле answered, яке прямо каже, чи вдалось додзвонитися, тож CRM не потрібно вгадувати результат за кодом причини завершення.
Ці дві послідовності подій — вхідна та вихідна — завжди надходять окремо одна від одної, з різними назвами подій (call.ended проти call.outgoing_ended), навіть якщо в адмінці налаштовано лише один-єдиний веб-хук, підписаний одразу на обидва ланцюги. Ідентифікатор uniqueid дозволяє простежити всі події одного дзвінка незалежно від того, до якого ланцюга він належить.
Розділ 5 містить детальний опис кожної з семи подій.
5. Опис подій та їх полів
Усі запити мають тип POST з тілом у форматі application/json. Тип події визначається значенням поля event.
5.1. call.incoming — початок дзвінка
{
"event": "call.incoming",
"uniqueid": "1753000000.42",
"linkedid": "1753000000.42",
"channel": "PJSIP/trunk1-0000001a",
"caller_id_num": "380501234567",
"caller_id_name": "Іван Іванович",
"exten": "s",
"context": "incoming",
"queue": null,
"timestamp": "2026-07-21T18:58:51.811673",
"recording_expected": null,
"recording_url": "https://pbx.example.com/api/v1/recordings/1753000000.42/",
"channel_vars": {}
}
Особливості полів:
queueматиме значенняnull, якщо дзвінок класифіковано за напрямком (context), а не за чергою. Якщо дзвінок надійшов через чергу — тут буде вказана її назва, наприклад"support".- Поле
recording_urlприсутнє вже на цьому етапі, до фактичного створення файлу запису. Це не є помилкою: посилання формується детерміновано на основіuniqueidзаздалегідь (детально — у розділі 6). Файл за цим посиланням стане доступним пізніше, за умови завершення дзвінка із записом. recording_expected— попередня оцінка ймовірності запису. У деяких випадках система вже має визначену відповідь (true/false), в інших — значенняnull, що відображає проміжний стан невизначеності, а не помилку.
5.2. call.answered — оператор прийняв виклик
{
"event": "call.answered",
"uniqueid": "1753000000.42",
"linkedid": "1753000000.42",
"channel": "PJSIP/trunk1-0000001a",
"caller_id_num": "380501234567",
"caller_id_name": "Іван Іванович",
"queue": "support",
"member_name": "Оператор Петренко",
"member_interface": "PJSIP/101",
"member_number": "101",
"ringtime": "3500",
"holdtime": "18",
"timestamp": "2026-07-21T18:58:51.812900",
"channel_vars": {"ULINE": "42"}
}
Опис полів:
member_name— ім’я оператора, визначене в налаштуваннях PearlPBX2 для відповідного члена черги.member_interface— технічний ідентифікатор оператора в Asterisk, наприклад:PJSIP/101.member_number— те саме значення у спрощеному вигляді (101); зазвичай зручніше для пошуку оператора в CRM-системі.ringtime— тривалість дзвінка на пристрої оператора до моменту відповіді, у мілісекундах.holdtime— тривалість очікування клієнта в черзі до моменту відповіді, у секундах (за змістом відповідає полюwait_timeу пропущеному виклику, але за умови успішного з’єднання).
Ця подія формується виключно для дзвінків, що пройшли через чергу обслуговування; пряме з’єднання поза чергою її не ініціює.
5.3. call.missed — пропущений виклик у черзі
{
"event": "call.missed",
"uniqueid": "1753000000.42",
"linkedid": "1753000000.42",
"channel": "PJSIP/trunk1-0000001a",
"caller_id_num": "380501234567",
"queue": "support",
"wait_time": 21,
"timestamp": "2026-07-21T18:58:51.813698",
"channel_vars": {}
}
Клієнт очікував у черзі support протягом wait_time секунд (у прикладі — 21 секунду) і завершив виклик, не дочекавшись відповіді оператора. Ця подія призначена, зокрема, для автоматичного створення задачі зворотного дзвінка в CRM-системі.
5.4. call.ended — завершення дзвінка
{
"event": "call.ended",
"uniqueid": "1753000000.42",
"linkedid": "1753000000.42",
"channel": "PJSIP/trunk1-0000001a",
"caller_id_num": "380501234567",
"caller_id_name": "Іван Іванович",
"exten": "s",
"context": "incoming",
"queue": null,
"timestamp": "2026-07-21T18:58:51.814936",
"duration": 42,
"cause": "16",
"cause_txt": "Normal Clearing",
"answered_time": "38",
"billsec": "38",
"missed": false,
"answered_by_member": "Оператор Петренко",
"answered_by_interface": "PJSIP/101",
"recorded": true,
"recording_url": "https://pbx.example.com/api/v1/recordings/1753000000.42/",
"recording_file": "/var/spool/asterisk/monitor/2026/07/21/x.wav",
"channel_vars": {"ULINE": "42"}
}
Подія містить найбільший обсяг даних. Основні поля:
duration— загальна тривалість дзвінка в секундах, від початку до завершення.cause_txt— текстовий опис причини завершення. Значення"Normal Clearing"відповідає штатному завершенню розмови. Значення на кшталт"Busy"чи"No Answer"також є коректними результатами, а не ознакою помилки системи.missed— значенняtrue, якщо цей дзвінок раніше вже фіксувався як пропущений (подіяcall.missed).answered_by_member/answered_by_interface— ім’я та інтерфейс оператора, який прийняв виклик, за умови що раніше надходила подіяcall.answered. Зіставлення даних між подіями виконується системою автоматично. Якщо виклик залишився без відповіді (наприклад, пропущений), обидва поля мають значенняnull.recorded— підтверджений факт здійснення запису дзвінка (на відміну від попередньої оцінкиrecording_expected). Можливі значення:true,false, зрідкаnull(якщо визначити факт запису не вдалось з технічних причин).recording_url— посилання на файл запису за умовиrecorded: true. За відсутності запису значення дорівнюєnull, і звернення за цим посиланням не має сенсу.recording_file— шлях до файлу запису на файловій системі АТС, наведений у прикладі вище зі значенням.wav, оскільки саме в цьому форматі Asterisk створює запис одразу після завершення дзвінка. Це поле відображає внутрішній шлях на момент формування події й не призначене для використання CRM-системою: файл згодом може бути автоматично конвертований у.mp3за розкладом на боці АТС (детально — у розділі 6). Для отримання аудіозапису слід використовувати виключноrecording_url.
Примітка: не всі поля обов’язково заповнені для кожного дзвінка — наприклад, queue часто матиме значення null для прямих викликів. Рекомендується проєктувати обробку подій з урахуванням можливих null-значень.
5.5. call.outgoing — початок вихідного дзвінка
{
"event": "call.outgoing",
"uniqueid": "1753000000.55",
"linkedid": "1753000000.55",
"channel": "PJSIP/1001-0000002a",
"caller_id_num": "1001",
"caller_id_name": "Оператор Петренко",
"exten": "380671112233",
"context": "outbound-users",
"direction": "outbound",
"timestamp": "2026-08-11T18:58:51.811673",
"channel_vars": {}
}
Особливості полів:
caller_id_num/caller_id_nameтут — це номер і ім’я співробітника, який ініціює дзвінок, а не клієнта (на відміну відcall.incoming, де ці поля належать абоненту, що телефонує).exten— номер, який набрав співробітник (номер клієнта).direction— завжди"outbound"для цієї події; поле присутнє в усіх подіях обох ланцюгів і дозволяє однією перевіркою визначити, до якого з них належить подія, не аналізуючи саму назвуevent.- Ця подія надсилається лише тоді, коли дзвінок ініціює саме внутрішній SIP-користувач (співробітник), а не транк (з’єднання з телефонним провайдером). Дзвінки, що приходять ззовні через транк, завжди йдуть через
call.incoming, ніколи черезcall.outgoing. - За замовчуванням АТС також не надсилає
call.outgoingдля внутрішнього каналу, який Asterisk щойно створив (черезDial()/Originate()), але ще не з’єднав з конкретним номером — у такий моментextenдорівнює службовому значенню"s", а не реальному номеру, і показувати CRM таку подію немає сенсу. Якщо цей behavior потрібно змінити — уточніть у адміністратора АТС (налаштуванняWEBHOOK_SEND_SYSTEM_CHANNELS).
5.6. call.outgoing_answered — викликана сторона підняла слухавку
{
"event": "call.outgoing_answered",
"uniqueid": "1753000000.55",
"linkedid": "1753000000.55",
"channel": "PJSIP/1001-0000002a",
"caller_id_num": "1001",
"caller_id_name": "Оператор Петренко",
"exten": "380671112233",
"context": "outbound-users",
"dest_channel": "PJSIP/trunk1-0000002a",
"dial_status": "ANSWER",
"direction": "outbound",
"timestamp": "2026-08-11T18:58:56.203112",
"channel_vars": {}
}
Опис полів:
dest_channel— технічний ідентифікатор напрямку, яким пішов виклик (наприклад, конкретний транк). Поле інформативне; для більшості інтеграцій CRM-системі достатньоexten, щоб знати, кому телефонували.dial_status— значення AMI-поля AsteriskDialStatusу момент відповіді, тут завжди"ANSWER"(значення для інших результатів дзвінка описані в розділі 5.7).
Ця подія надходить, лише якщо виклик було прийнято. Якщо лінія зайнята, ніхто не відповів, або дзвінок скасовано до відповіді — ця подія не надсилається взагалі, і послідовність одразу переходить до call.outgoing_ended.
5.7. call.outgoing_ended — завершення вихідного дзвінка
{
"event": "call.outgoing_ended",
"uniqueid": "1753000000.55",
"linkedid": "1753000000.55",
"channel": "PJSIP/1001-0000002a",
"caller_id_num": "1001",
"caller_id_name": "Оператор Петренко",
"exten": "380671112233",
"context": "outbound-users",
"queue": null,
"direction": "outbound",
"dial_status": "ANSWER",
"answered": true,
"timestamp": "2026-08-11T18:59:24.550012",
"duration": 28,
"cause": "16",
"cause_txt": "Normal Clearing",
"answered_time": "26",
"billsec": "26",
"missed": false,
"answered_by_member": null,
"answered_by_interface": null,
"recorded": false,
"recording_url": null,
"recording_file": null,
"channel_vars": {}
}
Ця подія структурно повторює call.ended (розділ 5.4) — ті самі поля duration, cause, cause_txt, recorded/recording_url/recording_file для запису розмови, якщо він увімкнений і для вихідних дзвінків. Додатково є два поля, специфічні для вихідного ланцюга:
answered— булеве значення:true, якщо перед цією подією надходилаcall.outgoing_answered, інакшеfalse. Це найпростіший спосіб визначити результат дзвінка, не аналізуючи код причини завершення.dial_status— останнє відоме значенняDialStatus:"ANSWER"за успішного з’єднання, або"BUSY"/"NOANSWER"/"CANCEL"тощо за невдалої спроби.
Поля answered_by_member / answered_by_interface тут завжди null — вони стосуються виключно оператора черги на вхідному ланцюгу (розділ 5.4) і не мають значення для дзвінка, ініційованого співробітником напряму.
6. Отримання запису розмови
Файл запису не передається разом із веб-хуком. З огляду на обсяг аудіоданих, веб-хук містить лише посилання на файл, за яким CRM-система може звернутися окремим запитом у момент фактичної потреби (наприклад, при відкритті картки дзвінка оператором для прослуховування).
Посилання формується детерміновано на основі uniqueid і тому присутнє вже в першій події call.incoming, задовго до завершення дзвінка та фактичної появи файлу на диску.
Важливо щодо формату файлу. На стороні АТС записи спершу створюються у форматі WAV, після чого за розкладом (регулярне фонове завдання) конвертуються у MP3, а вихідний WAV-файл видаляється. Таким чином, на момент звернення CRM-системи за записом фактичний формат файлу заздалегідь невідомий і залежить від того, чи встигло відпрацювати завдання конвертації. Ендпоінт враховує це автоматично: сервер самостійно визначає, який файл існує на диску (.mp3 чи .wav), і повертає відповідні заголовки Content-Type (audio/mpeg або audio/wav) та Content-Disposition з іменем файлу, що містить фактичне розширення. CRM-системі не потрібно (і не рекомендується) вважати розширення фіксованим — коректна реалізація повинна визначати формат отриманого файлу за заголовком Content-Type відповіді, а не за самим URL чи заздалегідь заданим іменем файлу.
Для отримання файлу виконується запит GET із заголовком авторизації:
curl -H "Authorization: Token ВАШ_ТОКЕН" \
https://pbx.example.com/api/v1/recordings/1753000000.42/ \
-o call_recording
Токен видається адміністратором АТС одноразово, за аналогією з API-ключами інших сервісів, і підлягає зберіганню з тим самим рівнем захисту, що й пароль.
Можливі відповіді сервера:
| Код відповіді | Значення |
|---|---|
200 або 206 з аудіофайлом |
запит виконано успішно; 206 повертається при запиті частини файлу (перемотка/стрімінг) |
401 |
токен відсутній або некоректний — перевірте заголовок запиту |
404 |
запис відсутній (дзвінок не записувався, або файл ще не встиг сформуватися на диску) |
Якщо запит виконується одразу після call.ended і повертає 404, це не обов’язково свідчить про помилку: запис файлу на диск може відбуватися з незначною затримкою відносно надсилання JSON-повідомлення. У такому випадку рекомендується повторити запит через кілька секунд.
Для примусового завантаження файлу браузером (замість відтворення) додайте до URL параметр ?download=1.
7. REST API PearlPBX2
Окрім прийому веб-хуків, CRM-система може самостійно звертатися до REST API PearlPBX2 — зокрема для ініціалізації вихідних дзвінків, зведення кількох учасників в одну конференцію, а також (за потреби) для роботи зі списками номерів. Усі ендпоінти розташовані під префіксом /api/v1/ і потребують автентифікації.
7.1. Автентифікація
API використовує токен-автентифікацію. Токен передається в заголовку Authorization кожного запиту:
Authorization: Token ВАШ_ТОКЕН
Токен видається адміністратором АТС окремо від токена для завантаження записів розмов (розділ 6) — рекомендується уточнити у адміністратора, чи використовується спільний токен, чи для кожної цілі видається окремий. Запит без токена або з недійсним токеном повертає 401 Unauthorized.
7.2. Інтерактивна документація (Swagger / ReDoc)
Окрім цього документа, PearlPBX2 автоматично генерує машинозчитувану специфікацію API (на основі drf-spectacular) і надає до неї два готових веб-інтерфейси. Це зручно як довідник з актуальним переліком полів та як інструмент для ручного тестування запитів без написання коду:
GET /api/v1/schema/— сама OpenAPI-специфікація у форматі JSON/YAML. Придатна для імпорту в Postman, Insomnia чи для автоматичної генерації клієнтського коду (SDK) вашою мовою програмування.GET /api/v1/docs/— інтерактивний інтерфейс Swagger UI. Дозволяє переглянути всі ендпоінти, їх параметри та приклади відповідей, а також виконувати тестові запити прямо з браузера через кнопку «Try it out».GET /api/v1/redoc/— те саме джерело даних, оформлене як статична, зручна для читання довідкова сторінка (ReDoc), без можливості виконання запитів.
Важливо щодо доступу. Ці сторінки не виключені із загальної вимоги автентифікації — так само, як і решта API, вони захищені токен-автентифікацією (див. розділ 7.1), а не автентифікацією через сесію Django. Це означає, що просте відкриття /api/v1/docs/ у браузері без додаткових дій поверне 401 Unauthorized, оскільки браузер не додає заголовок Authorization автоматично. Щоб скористатися Swagger UI, потрібен браузерний плагін або розширення, яке дозволяє додати заголовок Authorization: Token ВАШ_ТОКЕН до запитів сторінки, або перегляд специфікації через інструмент на кшталт Postman/Insomnia, де токен можна вказати в налаштуваннях запиту. Для одноразової перевірки специфікації без браузера достатньо звичайного запиту з токеном:
curl -H "Authorization: Token ВАШ_ТОКЕН" https://pbx.example.com/api/v1/schema/
Рекомендується звірятися з цими джерелами у разі сумнівів щодо актуальної версії API — вони генеруються безпосередньо з коду сервера і завжди відповідають поточному стану.
7.3. Ініціалізація вихідного дзвінка
POST /api/v1/calls/originate/
Ендпоінт ставить в чергу на виконання вихідний дзвінок через Asterisk Manager Interface (AMI). Типовий сценарій використання з боку CRM — дзвінок «у два кроки» (click-to-call): спершу викликається внутрішній номер оператора (channel), і лише після того, як оператор підніме слухавку, АТС з’єднує його з номером клієнта (exten).
Поля тіла запиту:
| Поле | Тип | Обов’язкове | Значення за замовчуванням | Опис |
|---|---|---|---|---|
channel |
рядок (до 256 символів) | Так | — | Канал, який АТС викликає першим, наприклад Local/0503856087@default або PJSIP/0504139380@mega-provider. |
exten |
рядок (до 128 символів) | Так | — | Внутрішній номер або номер, з яким з’єднується канал channel після відповіді, наприклад 0675653380. |
context |
рядок (до 128 символів) | Ні | "default" |
Контекст dialplan, у якому виконується з’єднання з exten (детальне пояснення — нижче). |
priority |
ціле число | Ні | 1 |
Пріоритет dialplan (мінімальне значення — 1). |
callerid |
рядок (до 128 символів) | Ні | — | Caller ID, який побачить викликана сторона, у форматі ім'я<номер>, наприклад 380443333333<0675653380>. |
variable |
об’єкт (пари рядок → рядок) | Ні | — | Довільні канальні змінні Asterisk, наприклад {"userId": "0"}. |
timeout_ms |
ціле число | Ні | 30000 |
Максимальний час очікування відповіді на виклик, у мілісекундах (від 1000 до 120000). |
Що таке context у цьому запиті. Значення "default" у таблиці вище — лише приклад-заглушка, а не системна константа. Насправді context — це назва таблиці маршрутизації (RoutingTable) або контексту dialplan (DialplanContext), налаштованих адміністратором PearlPBX2 конкретно для цієї інсталяції в адмін-панелі. Назви таких контекстів довільні (наприклад, Incoming, Outgoing, internal-users) і не підпорядковуються жодній універсальній конвенції — кожна інсталяція АТС може мати власний набір назв залежно від того, скільки провайдерів, транків і сценаріїв маршрутизації налаштовано. Перед реалізацією інтеграції обов’язково уточніть у адміністратора АТС точні назви контекстів, які слід використовувати для ваших сценаріїв, і попросіть надати готові приклади запитів channel/exten/context саме для вашої інсталяції — самостійно вгадати ці значення неможливо.
Приклад 1: дзвінок «у два кроки» (click-to-call) через внутрішнього оператора.
Спершу АТС дзвонить на внутрішній номер оператора (channel), і лише після того, як оператор підніме слухавку, з’єднує його з номером клієнта (exten):
curl -X POST https://pbx.example.com/api/v1/calls/originate/ \
-H "Authorization: Token ВАШ_ТОКЕН" \
-H "Content-Type: application/json" \
-d '{
"channel": "Local/0503856087@default",
"exten": "0675653380",
"context": "default",
"callerid": "380443333333<0675653380>",
"variable": {"userId": "0"}
}'
Приклад 2: прямий дзвінок через конкретний транк провайдера, без Local-каналу.
У деяких сценаріях Local-канал взагалі не потрібен: channel може одразу вказувати на реальний канал провайдера, а exten/context/priority — це точка dialplan, куди Asterisk приземлить цей канал одразу після того, як провайдер відповість на виклик. Це стандартна поведінка AMI-команди Originate — Local-канал потрібен лише тоді, коли перший «крок» сам по собі є внутрішнім номером (як у прикладі 1), а не тоді, коли channel уже є фінальним каналом виклику.
Наприклад: потрібно подзвонити напряму на номер 0504139380 через транк провайдера mega-provider, а результат (після відповіді) направити на внутрішній номер 222 у таблиці маршрутизації Incoming, підставивши CallerID 0442222222:
curl -X POST https://pbx.example.com/api/v1/calls/originate/ \
-H "Authorization: Token ВАШ_ТОКЕН" \
-H "Content-Type: application/json" \
-d '{
"channel": "PJSIP/0504139380@mega-provider",
"exten": "222",
"context": "Incoming",
"priority": 1,
"callerid": "0442222222<0442222222>",
"timeout_ms": 30000
}'
Тут PJSIP/0504139380@mega-provider означає «набрати номер 0504139380 через SIP-транк (peer) з іменем mega-provider»; mega-provider і Incoming — назви, які мають реально існувати в конкретній інсталяції АТС (узгоджуються з адміністратором, як описано вище).
Важливо про callerid у сценарії прямого виклику транка. На відміну від click-to-call через внутрішній номер (приклад 1), де CallerID зазвичай є лише інформаційним і може перезаписуватися логікою dialplan за замовчуванням, у сценарії прямого виклику транка (приклад 2) значення callerid реально передається на бік провайдера/мережі як номер, з якого нібито здійснюється виклик. Це не косметичне поле: якщо вказаний номер не належить вашому пулу номерів або не дозволений провайдером для підстановки, виклик може бути відхилений оператором зв’язку, позначений як спам/підозрілий, або, залежно від законодавства та політики провайдера, це може розглядатися як підміна номера (CLI spoofing). Перед використанням цього сценарію обов’язково узгодьте з адміністратором АТС і провайдером, які саме номери дозволено підставляти як CallerID для конкретного транка.
Успішна відповідь (200 OK):
{
"status": "originated",
"message": "Originate successfully queued"
}
Важливо: статус 200 та "status": "originated" означають лише те, що команду на встановлення з’єднання прийнято та передано в AMI. Це не є підтвердженням того, що виклик фактично відбувся чи що абонент відповів — цю інформацію CRM-система отримує окремо через веб-хуки (call.answered, call.ended), зіставляючи їх за uniqueid дзвінка, що виник у результаті originate.
Можливі помилки:
| Код | Тіло відповіді | Причина |
|---|---|---|
400 Bad Request |
{"channel": ["This field is required."]} (приклад для поля channel; аналогічно для будь-якого іншого обов’язкового поля) |
Не заповнене обов’язкове поле або порушено обмеження (наприклад, timeout_ms поза межами 1000–120000). |
401 Unauthorized |
{"detail": "Authentication credentials were not provided."} |
Відсутній або недійсний токен автентифікації. |
502 Bad Gateway |
{"detail": "AMI unavailable."} |
АТС не змогла встановити або підтримати з’єднання з Asterisk Manager Interface. |
502 Bad Gateway |
{"detail": "AMI originate timed out."} |
Відповідь від AMI не надійшла протягом timeout_ms (плюс службовий запас часу). |
502 Bad Gateway |
{"detail": "Extension does not exist"} (приклад; текст відповідає повідомленню AMI) |
AMI повернула помилку виконання команди Originate — текст повідомлення береться безпосередньо з відповіді Asterisk і може відрізнятися залежно від причини відмови. |
503 Service Unavailable |
{"detail": "Asterisk is disabled in this DEVMODE."} |
АТС працює в режимі розробки без підключення до реального Asterisk (лише на тестових стендах, у продакшн-середовищі не зустрічається). |
Для цього ендпоінта не передбачено окремих обмежень на частоту запитів (rate limiting) — практичне обмеження визначається пропускною спроможністю самої лінії/транків АТС, тому CRM-системі рекомендується самостійно контролювати інтенсивність ініціалізації дзвінків відповідно до домовленостей з адміністратором АТС.
7.4. Ініціалізація конференції (три і більше учасники)
POST /api/v1/calls/conference/
Ендпоінт calls/originate/ (розділ 7.3) завжди з’єднує рівно двох учасників. Якщо потрібно звести в одну розмову трьох і більше осіб одночасно (наприклад, Оператора, Клієнта та Водія), використовується calls/conference/: ендпоінт приймає перелік каналів і заводить кожен із них у спільну конференц-кімнату на базі ConfBridge.
Модель конференції. Кімнати не потрібно створювати заздалегідь: кімната виникає в момент, коли до неї заходить перший канал, і зникає, коли виходить останній. Номер кімнати — довільний числовий рядок; усі учасники, приземлені на один і той самий номер, чують один одного.
Поля тіла запиту:
| Поле | Тип | Обов’язкове | Значення за замовчуванням | Опис |
|---|---|---|---|---|
parties |
список рядків (мінімум 2) | Так | — | Канали учасників, наприклад ["PJSIP/101", "PJSIP/0504139380@mega-provider", "Local/2222@internal"]. |
room |
рядок (до 64 символів) | Ні | генерується автоматично | Номер конференц-кімнати. Якщо не вказано, сервер згенерує новий і поверне його у відповіді. |
context |
рядок (до 128 символів) | Ні | контекст конференції, налаштований на АТС | Дialplan-контекст, що приземляє кожне плече в ConfBridge. Для більшості інтеграцій змінювати не потрібно. |
callerid |
рядок (до 128 символів) | Ні | — | Caller ID, застосований до кожного з originate-плечей. |
timeout_ms |
ціле число | Ні | 30000 |
Максимальний час очікування відповіді на кожне плече, у мілісекундах (від 1000 до 120000). |
Як і в прикладі з водієм у розділі 7.3, канал, що має вести до внутрішнього номера з підтримкою декількох пристроїв/фолбеку, варто вказувати через Local-канал (наприклад, Local/2222@internal), а не напряму — це дозволяє dialplan-логіці (кілька пристроїв, follow-me) самій вирішити, куди зрештою потрапить виклик.
Приклад запиту (Оператор, Клієнт через транк провайдера, Водій через внутрішній номер з фолбеком):
curl -X POST https://pbx.example.com/api/v1/calls/conference/ \
-H "Authorization: Token ВАШ_ТОКЕН" \
-H "Content-Type: application/json" \
-d '{
"parties": [
"PJSIP/101",
"PJSIP/0504139380@mega-provider",
"Local/2222@internal"
]
}'
Відповідь (202 Accepted):
{
"room": "184920573",
"results": [
{"channel": "PJSIP/101", "queued": true, "detail": "Originate successfully queued"},
{"channel": "PJSIP/0504139380@mega-provider", "queued": true, "detail": "Originate successfully queued"},
{"channel": "Local/2222@internal", "queued": true, "detail": "Originate successfully queued"}
]
}
Код 202 і "queued": true означають лише постановку відповідної команди Originate в чергу AMI — усі плечі набираються паралельно, а не по черзі.
Це не підтвердження відповіді чи встановлення розмови: цю інформацію CRM отримує окремо через веб-хуки (call.answered, call.ended) для кожного плеча, за власним uniqueid.
Частковий збій можливий: якщо одне плече не додзвонилось, а решта приєдналися успішно, відповідний елемент results матиме "queued": false з поясненням у detail, а решта — "queued": true.
Можливі помилки:
| Код | Тіло відповіді | Причина |
|---|---|---|
400 Bad Request |
{"parties": ["Ensure this field has at least 2 elements."]} |
Передано менше двох учасників, або порушено інше обмеження полів. |
401 Unauthorized |
{"detail": "Authentication credentials were not provided."} |
Відсутній або недійсний токен автентифікації. |
502 Bad Gateway |
{"detail": "AMI unavailable."} |
АТС не змогла встановити з’єднання з Asterisk Manager Interface (жодне плече не було поставлено в чергу). |
503 Service Unavailable |
{"detail": "Asterisk is disabled in this DEVMODE."} |
АТС працює в режимі розробки без підключення до реального Asterisk. |
7.5. Інші ендпоінти
API також надає ендпоінти для роботи зі списками номерів. Вони не є обов’язковими для базової інтеграції (прийом подій дзвінків та ініціалізація вихідних дзвінків), але можуть бути корисними, якщо CRM-система бере на себе керування чорними/білими списками чи довідником контактів.
| Ендпоінт | Методи | Призначення |
|---|---|---|
/api/v1/blacklist/, /api/v1/blacklist/{id}/ |
GET, POST, PUT, PATCH, DELETE |
Керування списком заблокованих номерів (callerid + destination). Повторний POST з тими самими callerid/destination оновлює наявний запис (200), новий запис створюється зі статусом 201. |
/api/v1/whitelist/, /api/v1/whitelist/{id}/ |
GET, POST, PUT, PATCH, DELETE |
Аналогічно до blacklist/, але для дозволених номерів. |
/api/v1/contacts/, /api/v1/contacts/{id}/ |
GET, POST, PUT, PATCH, DELETE |
Довідник відповідності callerid → ім’я контакту. |
/api/v1/lists/, /api/v1/lists/{id}/ |
GET, POST, PUT, PATCH, DELETE |
Керування довільними іменованими списками. |
/api/v1/lists/{id}/entries/ |
GET, POST |
Перегляд і додавання записів у межах конкретного списку. |
/api/v1/lists/{id}/entries/{entry_id}/ |
DELETE |
Видалення окремого запису зі списку. |
/api/v1/recordings/{uniqueid}/ |
GET |
Отримання аудіозапису дзвінка (детально описано в розділі 6). |
Усі перелічені ендпоінти для списків підтримують стандартну пагінацію (за замовчуванням 50 записів на сторінку) та повертають типові для DRF помилки валідації.
7.6. Формат помилок
Помилки валідації запиту повертаються у стандартному форматі Django REST Framework — об’єкт, де ключ відповідає назві поля, а значення — список текстових повідомлень:
{
"callerid": ["This field is required."]
}
Помилки, не прив’язані до конкретного поля (наприклад, відмова в автентифікації чи відсутність ресурсу), повертаються у полі detail:
{
"detail": "Authentication credentials were not provided."
}
Спроба створити ресурс, що порушує обмеження унікальності на рівні бази даних, повертає 409 Conflict:
{
"detail": "Resource already exists or violates a uniqueness constraint."
}
Рекомендується, щоб обробник відповідей CRM-системи орієнтувався насамперед на код HTTP-статусу, а вміст поля detail/назв полів використовував для діагностики та логування.
8. Dashboard API та WebSocket: канал реального часу (опційно)
Окрім веб-хуків та REST API (розділи 1–7), для деяких інсталяцій PearlPBX2 адміністратор може додатково надати доступ до внутрішнього API операторського дашборду. Це опційний, альтернативний канал — переважній більшості інтеграцій достатньо веб-хуків і REST API, описаних вище. Розділ призначений для випадків, коли CRM-системі потрібен саме поточний знімок стану АТС (черги, канали, активні дзвінки) чи неперервний потік подій, а не лише повідомлення про ключові моменти дзвінка.
Принципова відмінність від решти документа: формат даних цього каналу — внутрішній формат операторського дашборду PearlPBX2, а не стабілізований контракт інтеграції. Набір полів і типів подій може змінюватися між версіями без окремого циклу узгодження. Якщо є вибір — веб-хуки (розділ 5) є пріоритетним і рекомендованим джерелом даних для CRM.
8.1. Автентифікація
Обидва механізми нижче приймають той самий токен доступу, що видається адміністратором АТС за схемою токен-автентифікації Django REST Framework (та сама схема, що й у розділі 7.1; уточніть у адміністратора, чи використовується спільний токен з REST API, чи видається окремий).
8.2. Dashboard API (GET /dashboard/api/...)
Ендпоінти лише для читання, кожен приймає токен у заголовку Authorization: Token ВАШ_ТОКЕН (сесія Django теж підходить, але для CRM-інтеграції актуальний саме токен):
| Ендпоінт | Призначення |
|---|---|
GET /dashboard/api/queues/ |
Стан усіх черг обслуговування (учасники, дзвінки, статистика). |
GET /dashboard/api/queues/{queue_name}/ |
Стан конкретної черги. |
GET /dashboard/api/channels/ |
Стан усіх активних каналів Asterisk. |
GET /dashboard/api/channels/{channel_name}/ |
Стан конкретного каналу. |
GET /dashboard/api/channels/type/{channel_type}/ |
Канали, відфільтровані за типом (PJSIP, Local тощо). |
GET /dashboard/api/calls/active/ |
Активні (з’єднані) дзвінки з інформацією про обидва плеча. |
GET /dashboard/api/endpoints/ |
Перелік внутрішніх SIP-користувачів і зовнішніх SIP-транків, налаштованих на АТС. |
GET /dashboard/api/missed-calls/?queue={назва} |
Пропущені за сьогодні дзвінки в конкретній черзі. |
Приклад запиту:
curl -H "Authorization: Token ВАШ_ТОКЕН" \
https://pbx.example.com/dashboard/api/queues/
Запит без токена або з недійсним токеном повертає 401 Unauthorized.
Важливо: дві дії дашборду токеном не відкриваються. POST /dashboard/api/channels/hangup/ (примусове завершення дзвінка) і POST /dashboard/api/queues/pause/ (постановка/зняття оператора з паузи) — це керуючі дії, що напряму викликають Asterisk Manager Interface. Вони навмисно лишаються доступними лише через сесію Django зі статусом персоналу (is_staff) та CSRF-захистом і не приймають токен інтеграції. Токен, виданий для читання, не дає можливості керувати живими дзвінками чи чергами.
8.3. WebSocket /ws/asterisk/ — потік подій у реальному часі
wss://pbx.example.com/ws/asterisk/?token=ВАШ_ТОКЕН — той самий потік подій, що живить операторський дашборд: кожне повідомлення надходить одразу після відповідної події Asterisk, без опитування (polling). З’єднання лише для читання — сервер не приймає жодних команд від клієнта в межах цього сокета.
Формат кожного повідомлення:
{
"type": "channel_new",
"data": { "channel": "PJSIP/101-0000001a", "uniqueid": "1753000000.42", "..." : "..." },
"timestamp": "2026-07-24T14:32:10.123456"
}
type набуває значень на кшталт channel_new, channel_state_change, channel_dial_begin, channel_dial_end, channel_hangup, queue_caller_join, queue_caller_leave, queue_caller_abandon, queue_member_status, agent_connect та інших — це набагато детальніший і нижчорівневий потік, ніж чотири події веб-хука з розділу 5, і призначений радше для live-відображення стану АТС (наприклад, дошки диспетчера), ніж для бізнес-логіки на кшталт створення задачі в CRM. Для типової інтеграції (картка дзвінка, зворотний дзвінок за пропущеним викликом, прикріплення запису) веб-хуки з розділу 5 лишаються правильним і достатнім джерелом.
Браузерний WebSocket API не дозволяє додавати кастомні заголовки, тому токен передається саме через query-параметр ?token=; для нативних (не браузерних) клієнтів так само приймається заголовок Authorization: Token ВАШ_ТОКЕН. Без коректного токена і без активної сесії Django з’єднання одразу закривається сервером.
9. Перевірка автентичності запитів
Для запобігання прийому підроблених запитів, що імітують веб-хук PearlPBX2, передбачено механізм цифрового підпису.
За умови налаштування секретного ключа адміністратором АТС кожен запит супроводжується заголовком:
X-PearlPBX-Signature: sha256=abc123...
Значення заголовка є HMAC-SHA256-підписом тіла запиту, обчисленим із використанням секретного ключа, відомого обом сторонам (АТС і серверу CRM). Перевірка підпису підтверджує, що запит дійсно надіслано PearlPBX2 і його вміст не було змінено під час передачі.
Приклад перевірки на Python:
import hashlib
import hmac
SECRET = "секретний-ключ-з-налаштувань-атс"
def is_signature_valid(raw_body: bytes, header_value: str) -> bool:
expected = "sha256=" + hmac.new(
SECRET.encode(), raw_body, hashlib.sha256
).hexdigest()
# Порівняння виконується з константним часом; пряме порівняння рядків неприпустиме
return hmac.compare_digest(expected, header_value)
Аналогічний приклад на Node.js:
const crypto = require("crypto");
const SECRET = "секретний-ключ-з-налаштувань-атс";
function isSignatureValid(rawBody, headerValue) {
const expected =
"sha256=" + crypto.createHmac("sha256", SECRET).update(rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(headerValue));
}
Важливе застереження: підпис обчислюється на основі сирих байтів тіла запиту, до будь-якого парсингу JSON. Якщо використовуваний веб-фреймворк автоматично парсить JSON до отримання доступу до сирого тіла запиту, необхідно забезпечити доступ саме до вихідних байтів (у Flask — метод request.get_data(), в Express — middleware express.raw() або еквівалентний механізм збереження сирого буфера).
Якщо секретний ключ не налаштовано, заголовок підпису буде відсутній — це є штатною поведінкою, що означає відсутність перевірки підпису. Рекомендується, тим не менш, налаштувати цей механізм з міркувань безпеки.
10. Налаштування власного формату JSON
У випадку, якщо CRM-система очікує тіло запиту іншої структури (з відмінними назвами полів або додатковою вкладеністю), адміністратор АТС може налаштувати власний шаблон тіла запиту в адміністративній панелі, де стандартні поля замінюються довільними з підстановкою значень через синтаксис ${назва_змінної}. Приклад:
{
"call_id": "${uniqueid}",
"customer_phone": "${caller_id_num}",
"type": "phone_call",
"recording": "${recording_url}"
}
Перелік усіх доступних змінних для підстановки: event, uniqueid, linkedid, channel, caller_id_num, caller_id_name, exten, context, queue, timestamp, duration, cause, cause_txt, answered_time, billsec, recorded, recording_expected, recording_url, recording_file, missed, wait_time, member_name, member_interface, member_number, ringtime, holdtime, answered_by_member, answered_by_interface, direction, dest_channel, dial_status, answered, channel_vars.
channel_vars — об’єкт із дозволеними адміністратором АТС змінними каналу Asterisk (наприклад, {"ULINE": "42"}); порожній об’єкт {}, якщо жодна ще не встановлена. У шаблоні ${channel_vars} як єдиний вміст рядкового поля підставляється вкладеним JSON-об’єктом, а не текстом.
Налаштування шаблону виконується виключно на стороні адміністратора АТС і не потребує участі розробника CRM. За потреби зміни стандартного формату достатньо звернутися до адміністратора з відповідним запитом. Під час створення нового веб-хука поле шаблону в адміністративній панелі вже заповнене прикладом з переліком усіх доступних змінних — редагування зводиться до видалення зайвих рядків.
11. Поведінка системи при збоях доставки
Механізм доставки веб-хуків побудований за принципом «найкращого зусилля» (best-effort):
- Гарантована черга повторної доставки протягом тривалого періоду не передбачена.
- У разі відсутності своєчасної відповіді сервера CRM або його недоступності виконується кілька повторних спроб з інтервалом, що визначається налаштуваннями адміністратора АТС.
- Якщо всі повторні спроби завершуються невдало, подія вважається втраченою й повторно не надсилається.
Практичні наслідки для реалізації CRM-системи:
- Швидкість відповіді. Ендпоінт має повертати
200 OKякнайшвидше, в ідеалі протягом секунди. Якщо обробка події вимагає тривалих операцій (наприклад, звернення до сторонньої системи), рекомендується підтвердити прийом запиту негайно, а обробку виконати асинхронно (черга завдань, фоновий обробник тощо). - Обробка дублікатів. Повторне отримання однієї й тієї самої події теоретично можливе (наприклад, унаслідок мережевого збою в момент надсилання відповіді). Рекомендований підхід — використання пари
uniqueid+eventяк ключа ідемпотентності: якщо подія з таким ключем уже оброблена, повторний запит слід ігнорувати. - Відсутність подій. Відсутність вхідних запитів протягом певного періоду відповідає відсутності дзвінків і не є ознакою несправності.
12. Приклад реалізації сервера-приймача
Нижче наведено приклад реалізації на Python (Flask), що приймає події обох ланцюгів (вхідного та вихідного), виконує перевірку підпису та завантажує файл запису за умови його наявності. Приклад може використовуватися як основа для власної реалізації.
import hashlib
import hmac
import os
import requests
from flask import Flask, request, abort
app = Flask(__name__)
# Секретний ключ для перевірки підпису веб-хука, узгоджений з адміністратором АТС
WEBHOOK_SECRET = os.environ["PEARLPBX_WEBHOOK_SECRET"]
# Токен для завантаження записів розмов через API
API_TOKEN = os.environ["PEARLPBX_API_TOKEN"]
def is_signature_valid(raw_body: bytes, header_value: str | None) -> bool:
if not header_value:
return False
expected = "sha256=" + hmac.new(
WEBHOOK_SECRET.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header_value)
@app.post("/pearlpbx/webhook")
def handle_webhook():
raw_body = request.get_data() # сирі байти, до парсингу JSON
if not is_signature_valid(raw_body, request.headers.get("X-PearlPBX-Signature")):
abort(401)
payload = request.get_json()
event = payload["event"]
call_id = payload["uniqueid"]
if event == "call.incoming":
open_live_call_card(call_id, payload["caller_id_num"], payload.get("caller_id_name"))
elif event == "call.answered":
mark_call_answered(call_id, payload["member_name"])
elif event == "call.missed":
create_callback_task(call_id, payload["caller_id_num"], payload["wait_time"])
elif event == "call.ended":
close_call_card(call_id, duration=payload["duration"])
if payload.get("recorded"):
download_recording(call_id, payload["recording_url"])
elif event == "call.outgoing":
open_live_call_card(call_id, payload["exten"], payload.get("caller_id_name"))
elif event == "call.outgoing_answered":
mark_call_answered(call_id, payload["caller_id_name"])
elif event == "call.outgoing_ended":
close_call_card(call_id, duration=payload["duration"])
if payload.get("recorded"):
download_recording(call_id, payload["recording_url"])
return "", 200
def download_recording(call_id: str, recording_url: str) -> None:
response = requests.get(
recording_url,
headers={"Authorization": f"Token {API_TOKEN}"},
timeout=10,
)
if response.status_code == 404:
# Файл ще не сформовано на диску; повторна спроба можлива пізніше
return
response.raise_for_status()
# Реальний формат файлу (wav чи mp3) визначається за Content-Type,
# оскільки на боці АТС записи конвертуються з wav у mp3 за розкладом
extension = "mp3" if response.headers.get("Content-Type") == "audio/mpeg" else "wav"
with open(f"/data/recordings/{call_id}.{extension}", "wb") as f:
f.write(response.content)
def open_live_call_card(call_id, phone, name):
print(f"[incoming] {call_id}: виклик від {name or phone}")
def mark_call_answered(call_id, member_name):
print(f"[answered] {call_id}: відповів {member_name}")
def create_callback_task(call_id, phone, wait_time):
print(f"[missed] {call_id}: {phone}, очікування {wait_time}с, потрібен зворотний дзвінок")
def close_call_card(call_id, duration):
print(f"[ended] {call_id}: тривалість {duration}с")
Аналогічна реалізація на Node.js (Express):
const express = require("express");
const crypto = require("crypto");
const app = express();
const WEBHOOK_SECRET = process.env.PEARLPBX_WEBHOOK_SECRET;
const API_TOKEN = process.env.PEARLPBX_API_TOKEN;
// Збереження сирих байтів тіла запиту для перевірки підпису
app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }));
function isSignatureValid(rawBody, headerValue) {
if (!headerValue) return false;
const expected =
"sha256=" + crypto.createHmac("sha256", WEBHOOK_SECRET).update(rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(headerValue));
}
app.post("/pearlpbx/webhook", async (req, res) => {
if (!isSignatureValid(req.rawBody, req.headers["x-pearlpbx-signature"])) {
return res.sendStatus(401);
}
const payload = req.body;
switch (payload.event) {
case "call.incoming":
console.log(`[incoming] ${payload.uniqueid}: виклик від ${payload.caller_id_num}`);
break;
case "call.answered":
console.log(`[answered] ${payload.uniqueid}: відповів ${payload.member_name}`);
break;
case "call.missed":
console.log(`[missed] ${payload.uniqueid}: очікування ${payload.wait_time}с`);
break;
case "call.ended":
console.log(`[ended] ${payload.uniqueid}: тривалість ${payload.duration}с`);
if (payload.recorded) {
await downloadRecording(payload.uniqueid, payload.recording_url);
}
break;
case "call.outgoing":
console.log(`[outgoing] ${payload.uniqueid}: дзвінок на ${payload.exten}`);
break;
case "call.outgoing_answered":
console.log(`[outgoing_answered] ${payload.uniqueid}: відповіли`);
break;
case "call.outgoing_ended":
console.log(`[outgoing_ended] ${payload.uniqueid}: answered=${payload.answered}`);
if (payload.recorded) {
await downloadRecording(payload.uniqueid, payload.recording_url);
}
break;
}
res.sendStatus(200);
});
async function downloadRecording(callId, recordingUrl) {
const response = await fetch(recordingUrl, {
headers: { Authorization: `Token ${API_TOKEN}` },
});
if (response.status === 404) return; // файл ще не сформовано, спроба пізніше
if (!response.ok) throw new Error(`Recording fetch failed: ${response.status}`);
// Реальний формат файлу визначається за Content-Type: записи конвертуються
// з wav у mp3 за розкладом на боці АТС, тому розширення заздалегідь не фіксоване
const extension = response.headers.get("Content-Type") === "audio/mpeg" ? "mp3" : "wav";
const buffer = Buffer.from(await response.arrayBuffer());
require("fs").writeFileSync(`/data/recordings/${callId}.${extension}`, buffer);
}
app.listen(3000, () => console.log("Webhook receiver listening on :3000"));
13. Питання, що часто виникають
Чи можливе отримання call.ended без попереднього call.incoming?
Ні. Система відстежує цей стан внутрішньо: подія завершення надсилається виключно для дзвінків, про початок яких уже було повідомлено. Наявність call.ended гарантує, що раніше надходила подія call.incoming з тим самим uniqueid.
Чи можливі call.missed або call.answered без попереднього call.incoming?
Так. Обидві події є самостійними: пропущений виклик і факт відповіді оператора розглядаються як достатньо значущі для надсилання незалежно від підписки на подію початку дзвінка.
Чому подія call.answered відсутня для прямих дзвінків без черги?
Ця подія відповідає AMI-події Asterisk AgentConnect, яка формується виключно для дзвінків, що пройшли через чергу обслуговування. Прямий виклик конкретному співробітнику без участі черги цю подію не ініціює — це є очікуваною поведінкою.
Що означає значення null у полі queue?
Дзвінок здійснювався без участі черги обслуговування (наприклад, прямий виклик конкретному співробітнику). Це не є помилкою.
Чи є uniqueid стабільним ідентифікатором у часі?
Так, у межах одного дзвінка uniqueid є стабільним і унікальним ідентифікатором, придатним для використання як первинний ключ при зіставленні подій.
Чи є значення recording_expected: null у call.incoming помилкою?
Ні. Це означає, що на момент початку дзвінка система ще не визначила, чи буде здійснено запис. Остаточне значення міститься в полі recorded події call.ended.
Чи потрібна окрема логіка для дзвінків без запису?
Достатньо перевіряти умову recorded === true перед зверненням за файлом запису. За значень false або null звернення за посиланням поверне 404.
Яка поведінка передбачена у разі тимчасової недоступності сервера CRM під час надсилання події? Система виконає кілька повторних спроб доставки автоматично. Якщо жодна зі спроб не буде успішною, подія не надсилається повторно. Це слід враховувати при плануванні надійності роботи власного ендпоінта (зокрема, моніторингу його доступності).
Чи означає успішна відповідь POST /api/v1/calls/originate/ (200 OK), що абонент відповів на дзвінок?
Ні. Успішна відповідь підтверджує лише постановку команди Originate в чергу на виконання в Asterisk Manager Interface. Фактичний перебіг дзвінка (відповідь, тривалість, завершення) відстежується виключно через відповідні події веб-хука (call.answered, call.ended), зіставлені за uniqueid цього дзвінка.
Чому запит на ініціалізацію дзвінка повернув 502 Bad Gateway?
Це означає помилку на рівні взаємодії АТС з Asterisk Manager Interface — саме Asterisk, а не CRM-система, є джерелом проблеми. Причина конкретизується в полі detail відповіді (див. розділ 7.2): відсутність з’єднання з AMI, перевищення часу очікування (timeout_ms) або відмова Asterisk виконати команду (наприклад, неіснуючий внутрішній номер чи контекст).
Чим call.outgoing відрізняється від call.incoming, якщо обидва означають «дзвінок почався»?
Напрямком ініціації. call.incoming — дзвінок надходить до підприємства (від клієнта через транк, або внутрішній дзвінок у контекст/чергу). call.outgoing — дзвінок ініціює співробітник, набираючи номер зі свого телефону. Це два повністю незалежні ланцюги подій з різними назвами на кожному кроці (call.ended проти call.outgoing_ended), і жоден дзвінок не породжує події з обох ланцюгів одночасно.
Чи може подія вихідного ланцюга (call.outgoing*) прийти для дзвінка, що насправді йде через транк (з’єднання з провайдером)?
Ні, за жодних обставин. Система визначає ініціатора дзвінка за технічним ідентифікатором каналу конкретного співробітника, а не за напрямком чи назвою маршруту — навіть якщо адміністратор налаштував транк і внутрішніх користувачів на один і той самий маршрут, дзвінки транка це не переплутає з дзвінками співробітників.
Чи прийде call.outgoing_ended без попереднього call.outgoing_answered, якщо лінія була зайнята?
Так, і це штатна поведінка. call.outgoing_answered надходить, лише якщо викликана сторона підняла слухавку. Якщо лінія зайнята, ніхто не відповів, або дзвінок скасовано — одразу надходить call.outgoing_ended з полем answered: false і кодом причини в dial_status.
14. Контрольний список інтеграції
- Реалізовано HTTP-ендпоінт, що приймає
POST-запити з тілом у форматі JSON і оперативно повертає200 OK. - URL ендпоінта передано адміністратору АТС, узгоджено секретний ключ для підпису запитів.
- Отримано від адміністратора АТС API-токен для завантаження записів розмов та (за потреби) для звернень до REST API.
- Реалізовано обробку чотирьох подій вхідного ланцюга:
call.incoming,call.answered,call.missed,call.ended. - За потреби обробки вихідних дзвінків співробітників — реалізовано обробку трьох подій вихідного ланцюга:
call.outgoing,call.outgoing_answered,call.outgoing_ended. - За потреби ініціалізації дзвінків з боку CRM — реалізовано та протестовано звернення до
POST /api/v1/calls/originate/, з обробкою кодів400,401,502,503. - За потреби зведення трьох і більше учасників в одну розмову — реалізовано звернення до
POST /api/v1/calls/conference/, з обробкою часткових збоїв у масивіresults. - Реалізовано перевірку підпису
X-PearlPBX-Signatureна основі сирих байтів тіла запиту, до парсингу JSON. - Реалізовано завантаження запису розмови через
GET /api/v1/recordings/{uniqueid}/з використанням токена, за умовиrecorded: true. - Передбачено обробку відповіді
404при спробі отримання запису (можлива затримка появи файлу на диску). - Враховано можливість значення
nullдля окремих полів. - Реалізовано ідемпотентну обробку подій на випадок повторної доставки (за ключем
uniqueid). - (Опційно, лише якщо адміністратор надав доступ) Використання Dashboard API / WebSocket (розділ 8) узгоджене з адміністратором АТС, з розумінням, що цей формат внутрішній і не версіонований.
Виконання наведених пунктів є достатнім для повноцінної реалізації інтеграції.