Geliştirici kılavuzu
Bağla REST API ve webhook belgeleri: arama listeleri, kişiler, otomatik arama kuyruğu, görüşmeler, webhook’lar ve yapay zekâ ajanları için araç tanımları.
https://app.bagla.ai/api/v1Başlangıç
Genel bakış
Bağla asistanını kendi sisteminden yönetmek için tek bir REST API var: arama listeleri, kişiler, aranmayacaklar, otomatik arama kuyruğu, görüşmeler ve raporlar. Asistan aramaları senin belirlediğin saatlerde yapar; sonuçları ister API’den okur, ister webhook’larla olduğu an kendi sistemine alırsın (görüşme raporları, randevular, siparişler, mesajlar).
| Arama listeleri ve kişiler | Otomatik arama kuyruğu | |
|---|---|---|
| Ne için | Birden çok liste, her listeye ayrı amaç, çalışma saati ve numara | Tek bir kuyruğa kişi basıp sırayla aratmak |
| Kişi | Telefon numarası bir kişidir; aynı kişi birden çok listede olabilir | Kuyruğa özel |
| Çalışma saatleri | Gün başına birden çok aralık, saat dilimi, etkin tarih aralığı | Tek aralık ve günler |
| Numara | Havuzdan seçim | İşletmenin hattı |
| Başlangıç | Hızlı başlangıç | Kuyruk nasıl çalışır |
Raporları, randevuları ve diğer olayları sürekli sorgulamak yerine webhook’lara abone ol: olay olduğunda Bağla sana imzalı bir istek gönderir.
Yeni bir entegrasyon kuruyorsan arama listeleriyle başlamanı öneririz: alıcıları listelere ayırabilir, her listeye ayrı saat ve numara verebilir, tüm işlemleri toplu yapabilirsin. Küçük bir CRM’den “şu kişileri ara” demek için kuyruk yeterlidir.
Temel adres: https://app.bagla.ai/api/v1. Tüm yollar bu adrese eklenir; örneğin GET https://app.bagla.ai/api/v1/calls.
Başlamadan önce
Aramanın çalışması için şunlar gerekir. Biri eksikse API aşağıdaki hatayı döner; hata gövdesi her zaman { "error": "..." } biçiminde Türkçe bir cümledir.
| Gereken | Eksikse |
|---|---|
| Geliştirici ayarları açık | 403 Bu işletmede geliştirici ayarları kapalı |
| Planda giden arama | 402 Giden arama bu planda kullanılamıyor |
| Giden arama onayı (destek merkezinden talep edilir) | 403 Giden arama henüz onaylanmadı |
| Asistan ve telefon hattı bağlı (listeyi ya da kuyruğu başlatmak için) | 400 Asistan ve telefon hattı bağlı değil |
| Panelde otomatik arama ayarı (yalnızca kuyruk için) | 409 Otomatik arama henüz ayarlanmadı |
Panelde Ayarlar → Geliştirici bölümünden geliştirici ayarlarını aç; kenar çubuğunda Geliştirici sayfası belirir. Anahtarlar sekmesinden bir API anahtarı oluştur.
Okuma uçları (/calls, /reports/summary, /webhooks…) yalnızca geliştirici ayarlarının açık olmasını ister. Plan ve onay şartı listeleri, kişileri ve kuyruğu *değiştiren* uçlar için geçerlidir.
Kimlik doğrulama
Her isteğe API anahtarını ekle. Anahtar bgl_ ile başlar ve oluşturulurken yalnızca bir kez gösterilir.
Authorization: Bearer bgl_...X-API-Key: bgl_... başlığı da kabul edilir. Anahtarı kaynak koduna yazma; ortam değişkeninde tut. Sızdığından şüphelenirsen panelden iptal et, yenisini oluştur; diğer anahtarlar etkilenmez.
- Temel adres:
https://app.bagla.ai/api/v1 - Sınır: anahtar başına dakikada 120 istek. Aşınca
429veRetry-After(saniye) başlığı gelir;X-RateLimit-Remainingkalan hakkı gösterir. - Geçersiz ya da iptal edilmiş anahtar:
401. - Anahtar raporları okuyabilir; arama listelerini, kişileri, aranmayacakları, otomatik arama kuyruğunu ve webhook’ları yönetebilir. Randevu, sipariş ya da müşteri kaydını değiştiremez; ayar, ekip ve fatura uçlarına erişemez.
GET /me anahtarın bağlı olduğu işletmeyi, planı ve geliştirici durumunu döner; geliştirici ayarları kapalıyken bile çalışır, böylece bir 403’ün nedenini görebilirsin.
Hızlı başlangıç
Beş adımda bir liste kur, kişileri ekle ve aramaya başla. bgl_... yerine kendi anahtarını yaz.
1. Hat numaralarını gör. Dönen id değerini listeye atayacaksın (atamazsan işletmenin hattı kullanılır).
curl https://app.bagla.ai/api/v1/call-numbers \ -H "Authorization: Bearer bgl_..."2. Listeyi oluştur. Amaç, aranma dayanağı ve çalışma saatlerini ver.
curl -X POST https://app.bagla.ai/api/v1/call-lists \ -H "Authorization: Bearer bgl_..." -H "Content-Type: application/json" \ -d '{ "name": "Fiyat soranlar", "purpose": "Fiyat teklifimizi anlatıp randevu önermek için arıyoruz.", "consentBasis": "consent", "schedule": [ {"weekday": 1, "start": "10:00", "end": "11:45"}, {"weekday": 1, "start": "13:30", "end": "16:30"} ], "numberIds": ["903120000000"] }'3. Kişileri ekle. Aynı telefon ikinci kez eklenmez; update: true ise kayıtlı kişi dolu alanlarla güncellenir.
curl -X POST https://app.bagla.ai/api/v1/call-lists/LISTE_ID/contacts \ -H "Authorization: Bearer bgl_..." -H "Content-Type: application/json" \ -d '{"update": true, "contacts": [ {"phone": "0532 111 00 01", "name": "Ayşe Yılmaz", "company": "Yılmaz Ltd"}, {"phone": "0532 111 00 02", "name": "Mehmet Kaya"} ]}'4. Aktifleştir. Liste, çalışma aralıklarında kişileri sırayla arar.
curl -X POST https://app.bagla.ai/api/v1/call-lists/LISTE_ID/activate \ -H "Authorization: Bearer bgl_..."5. Sonuçları oku.
curl "https://app.bagla.ai/api/v1/call-lists/LISTE_ID/contacts?status=answered" \ -H "Authorization: Bearer bgl_..."Liste yanıtındaki state.openNow listenin şu an aramaya uygun olup olmadığını, state.nextOpenAt bir sonraki açılışı (UTC) söyler.
| Yol | Ne yapar | |
|---|---|---|
| GET | /me | Anahtar ve işletme bilgisi |
| GET | /call-lists/meta | Sınırlar ve sözlükler: saat aralığı, durumlar, toplu işlem sınırları |
Arama listeleri
Arama listeleri
Liste, aranacak kişileri bir amaç, bir çalışma programı ve bir numara kümesi etrafında toplar. Her liste diğerlerinden bağımsız çalışır.
| Yol | Ne yapar | |
|---|---|---|
| GET | /call-lists/meta | Saat sınırları, durumlar, toplu işlem sınırları |
| GET | /call-lists | Listeleri sayfalı döner |
| POST | /call-lists | Liste oluşturur (201) |
| GET | /call-lists/:id | Liste detayı: ayarlar, sayılar, numaralar, durum |
| PUT | /call-lists/:id | Verilen alanları günceller; gönderilmeyenler korunur |
| DELETE | /call-lists/:id | Listeyi ve üyeliklerini siler |
| POST | /call-lists/:id/activate | Listeyi aktif yapar |
| POST | /call-lists/:id/deactivate | Listeyi pasif yapar |
Liste oluşturma
{ "name": "Fiyat soranlar", "purpose": "Fiyat teklifimizi anlatıp randevu önermek için arıyoruz.", "greeting": "Merhaba %ad%, ben Derya, Nar Kuaför adına arıyorum.", "consentBasis": "consent", "timezone": "Europe/Istanbul", "activeFrom": "2026-11-01", "activeUntil": "2026-12-31", "schedule": [ { "weekday": 1, "start": "10:00", "end": "11:45" }, { "weekday": 1, "start": "13:30", "end": "16:30" } ], "numberIds": ["903120000000"], "maxAttempts": 2, "retryMinutes": 120, "concurrency": 1}| Alan | Açıklama |
|---|---|
name | Zorunlu. İşletmede benzersiz (aynı ad 409) |
purpose | Zorunlu, en az 10 karakter. Asistanın görüşmede ne anlatacağı; %ad%, %firma% gibi yer tutucular kullanılabilir |
consentBasis | Zorunlu. b2b işletmelere yönelik arama, consent aranma izni alınmış kişiler |
greeting | Açılış cümlesi şablonu; boşsa işletmenin giden arama selamlaması |
schedule, timezone, activeFrom, activeUntil | Çalışma saatleri bölümünde |
numberIds | Numaralar bölümünde |
maxAttempts | Açılmazsa en fazla kaç kez aranır (1–5, varsayılan 2) |
retryMinutes | Yeniden deneme aralığı (5–10080 dk, varsayılan 120) |
concurrency | Aynı anda aranan kişi sayısı (1–10) |
contactIds, contacts | Oluştururken listeye eklenecek mevcut ya da yeni kişiler |
Liste oluşturma yanıtı ve GET /call-lists/:id aynı biçimdedir:
{ "id": "672f1a2b3c4d5e6f7a8b9c0d", "name": "Fiyat soranlar", "status": "ready", "schedule": [{ "weekday": 1, "start": "10:00", "end": "11:45" }], "counts": { "total": 120, "pending": 118, "calling": 0, "answered": 2, "no_answer": 0, "meeting": 0, "opted_out": 0, "failed": 0, "skipped": 0 }, "numbers": { "strategy": "round_robin", "numbers": [{ "id": "903120000000", "number": "+903120000000", "numberDisplay": "0312 000 00 00", "active": true }] }, "state": { "active": false, "openNow": false, "nextOpenAt": "2026-11-09T07:00:00.000Z", "timezone": "Europe/Istanbul", "expired": false, "summary": "Pzt 10:00–11:45, 13:30–16:30", "blockedReason": null }}Durumlar
| status | Anlamı |
|---|---|
ready | Hazır: henüz başlatılmadı |
running | Aktif. Çalışma aralıklarında kişileri arar |
paused | Pasif. Hiç aranmaz; kalan kişiler bekler |
done | Aranacak kimse kalmadı |
Listeyi aktifleştirmek için asistan ve telefon hattı bağlı olmalı ve listede aranacak kişi bulunmalı, yoksa 400. Çalışan liste silinemez (409); önce pasife al.
Listeleri süzme
GET https://app.bagla.ai/api/v1/call-lists?status=active&q=fiyat&page=1&pageSize=25status: active (çalışanlar), passive (çalışmayanlar) ya da ready, running, paused, done. q ad ve amaçta arar.
Çalışma saatleri
Her liste kendi haftalık programıyla çalışır. Program, aramaların yapılabileceği aralıklardır; dışında liste hiç aramaz.
{ "schedule": [ { "weekday": 1, "start": "10:00", "end": "11:45" }, { "weekday": 1, "start": "13:30", "end": "16:30" }, { "weekday": 2, "start": "10:00", "end": "11:00" } ], "timezone": "Europe/Istanbul", "activeFrom": "2026-11-01", "activeUntil": "2026-12-31"}weekday: 0 Pazar, 1 Pazartesi … 6 Cumartesi. Bir günde birden çok aralık verebilirsin (en fazla 8).- Saatler
SS:DDbiçiminde ve 09:00–20:00 arasında olmalı. Başlangıç bitişten önce gelmeli. - Aynı günde çakışan aralıklar reddedilir (
400). Bitişik aralıklar (10:00–11:00 ve 11:00–12:00) tek aralığa birleşir. - Kısayol:
{ "days": [1, 2, 3], "ranges": [["10:00", "11:45"], ["13:30", "16:30"]] }bu günlerin hepsine aynı aralıkları uygular. timezone: IANA adı (örn.Europe/Istanbul). Boşsa işletmenin saat dilimi. Saatler bu dilimde uygulanır.activeFrom,activeUntil: listenin arama yapabileceği tarihler (YYYY-AA-GG, ikisi de dahil). İkisi de boşsa sınırsız.
Aralık dışında ne olur? Kişiler silinmez ve hata almaz: liste bir sonraki aralığın başında kaldığı yerden devam eder. Açılmayan numaranın yeniden denemesi çalışma saati dışına düşerse bir sonraki açılışa ertelenir. Pasif liste hiçbir saatte aranmaz.
Programı okumak için GET /call-lists/:id/schedule. Yanıt, listenin şu an açık olup olmadığını ve sonraki açılış zamanını verir:
{ "timezone": "Europe/Istanbul", "schedule": [{ "weekday": 1, "start": "10:00", "end": "11:45" }], "summary": "Pzt 10:00–11:45", "openNow": false, "nextOpenAt": "2026-11-09T07:00:00.000Z", "expired": false}nextOpenAt UTC’dir. Otomatik arama kuyruğunun tek aralıklı ayarı (windowStart, windowEnd, days) da arka planda aynı programa dönüştürülür.
Numaralar
İşletmeye bağlı hatlar bir numara havuzu oluşturur. Her liste havuzdan bir ya da birden çok numara seçer; aramaları yalnızca bunlardan yapar. Aynı numara birden çok listede kullanılabilir.
| Yol | Ne yapar | |
|---|---|---|
| GET | /call-numbers | Havuzdaki numaralar |
| PUT | /call-numbers/:id | Numarayı etkinleştirir ya da devre dışı bırakır ({ "active": false }), etiketi değiştirir |
| GET | /call-lists/:id/numbers | Listeye atanmış numaralar |
| PUT | /call-lists/:id/numbers | Numaraları değiştirir ({ "numberIds": [...] }) |
[ { "id": "903120000000", "number": "+903120000000", "numberDisplay": "0312 000 00 00", "label": "Asistan hattı", "active": true, "isDefault": true, "lists": 2 }]- Numara kimliği (
id) numaranın rakamlarıdır; yanıtlardakinumberE.164 (+90…),numberDisplayTürkiye yazımıdır. - Listeye numara atamazsan işletmenin varsayılan hattı kullanılır.
- Havuzda olmayan bir numara atanamaz (
400). Havuza yeni hat eklemeyi Bağla ekibi yapar. - Devre dışı bırakılan numara hiçbir listede kullanılmaz. Listeye atanan numaraların hepsi devre dışıysa liste arama yapmaz; nedeni
state.blockedReasonalanında görünür.
Aranmayacaklar listesi
“Beni aramayın” diyen numaralar işletmenin aranmayacaklar listesine girer. Listedeki numara hiçbir arama listesinde ve otomatik arama kuyruğunda aranmaz. Numarayı asistan görüşmede müşteri istediğinde kendisi ekler; sen de API ile okuyabilir, ekleyebilir ya da çıkarabilirsin. CRM’inde bir müşteri “aranmak istemiyorum” derse numarayı buraya yazman yeterli: sonraki gönderimlerde de aranmaz.
| Yol | Ne yapar | |
|---|---|---|
| GET | /do-not-call | Liste (en yeni önce). Süzgeç: q (numara ya da neden), source, since, page, pageSize |
| POST | /do-not-call | Numara ekler. Tek: { "phone": "...", "reason": "..." } · toplu: { "phones": [...], "reason": "..." } ya da { "numbers": [{ "phone", "reason" }] } (en fazla 5.000) |
| POST | /do-not-call/check | Numaralardan hangileri listede? { "phones": [...] } (en fazla 1.000) |
| GET | /do-not-call/:phone | Tek numara: listede mi, kaydı nedir |
| DELETE | /do-not-call/:phone | Numarayı listeden çıkarır; listede değilse 404 |
curl -X POST https://app.bagla.ai/api/v1/do-not-call \ -H "Authorization: Bearer bgl_..." -H "Content-Type: application/json" \ -d '{ "phone": "0532 111 00 01", "reason": "CRM: müşteri aranmak istemiyor" }'{ "phone": "+905321110001", "added": true, "affectedContacts": 2 }phonehangi biçimde yazılırsa yazılsın (0532 111 00 01,+90 532 111 00 01,5321110001) E.164 olarak (+905321110001) saklanır; yanıtlardaphoneDisplayokunur biçimi verir.added: falsenumaranın zaten listede olduğunu gösterir (200); yeni eklemede201döner. İşlem tekrarlanabilir, zararsızdır.affectedContacts, numaranın bekleyen liste kişilerinden kaç tanesininopted_outyapıldığıdır. Daha önce aranmış ya da görüşülmüş kişiler değişmez.- Toplu ekleme tek istekte çalışır; yanıt:
received,added,existing,invalidve geçersiz satırlarınerrorslistesi. Geçersiz bir satır diğerlerini engellemez. - Listeden çıkarılan numara tekrar aranabilir hâle gelir; daha önce
opted_outyapılmış liste kişileri olduğu gibi kalır.
| Alan | Açıklama |
|---|---|
reason | Eklenme nedeni (en fazla 200 karakter) |
source | assistant (görüşmede müşteri istediği için asistan ekledi), panel (elle), api |
createdAt | Eklendiği an (ISO 8601, UTC). since süzgeci bu değere bakar |
{ "phones": ["0532 111 00 01", "0532 000 00 00", "abc"] }{ "items": [ { "phone": "0532 111 00 01", "normalized": "+905321110001", "valid": true, "doNotCall": true }, { "phone": "0532 000 00 00", "normalized": "+905320000000", "valid": true, "doNotCall": false }, { "phone": "abc", "normalized": "", "valid": false, "doNotCall": false } ]}Webhook: numarayı asistan ya da panel eklediğinde do_not_call.added, panelden çıkarıldığında do_not_call.removed olayı gelir. API ile yaptığın ekleme ve silmeler için olay üretilmez; o değişikliği zaten sen yaptın.
Kişiler
Kişiler
Kişi (alıcı), bir telefon numarasının bir listedeki üyeliğidir. Aynı numara birden çok listede olabilir; ad, firma ve serbest alanlar tüm üyeliklerde ortak tutulur, arama durumu (sırada, açılmadı, görüşüldü…) ise listeye özeldir. /contacts uçları kişiyi listelerden bağımsız bir havuz gibi gösterir; /call-lists/:id/contacts ise bir listenin üyelerini.
Bağla’da her kişi bir listeye bağlıdır: POST /contacts çağrısında listIds zorunludur. Üyelik kimliği (targetId) ile kişi kimliği (id) aynı değerdir.
| Yol | Ne yapar | |
|---|---|---|
| GET | /contacts | Tüm kişiler. Süzgeç: q, listId, notInList, sort (name, oldest, newest), page, pageSize |
| POST | /contacts | Tek kişi ekler (201) ya da { "contacts": [...] } ile toplu ekleme |
| GET | /contacts/:id | Kişi ve üye olduğu listeler |
| PUT | /contacts/:id | Kişiyi günceller; aranmamış tüm üyeliklere yansır |
| DELETE | /contacts/:id | Kişiyi ve tüm üyeliklerini siler |
| GET | /call-lists/:id/contacts | Liste üyeleri ve arama durumları. Süzgeç: status, q, updatedSince, page, pageSize |
| POST | /call-lists/:id/contacts | Listeye kişi ekler: { "contactIds": [...] } ya da { "contacts": [...] }; CSV gövdesi de olur |
| DELETE | /call-lists/:id/contacts | { "contactIds": [...] } kişileri listeden çıkarır |
| DELETE | /call-lists/:id/contacts/:contactId | Tek kişiyi listeden çıkarır |
{ "phone": "0532 111 00 01", "name": "Ayşe Yılmaz", "company": "Yılmaz Ltd", "note": "Geçen ay demo istedi", "fields": { "sehir": "İzmir", "paket": "Pro" }, "externalId": "crm-1042", "listIds": ["672f1a2b3c4d5e6f7a8b9c0d"]}| Alan | Açıklama |
|---|---|
phone | Zorunlu. 0532 111 00 01, +90 532 111 00 01, 5321110001 aynı kişidir; +905321110001 olarak saklanır |
name, company, note | İsteğe bağlı (ad 80, firma 120, not 300 karakter) |
fields | Serbest alanlar (en fazla 20); açılış cümlesinde ve amaçta %sehir% biçiminde kullanılır |
externalId | Kendi sistemindeki kimlik (en fazla 100 karakter) |
listIds | Kişinin ekleneceği listeler (zorunlu) |
Kurallar
- Aynı telefon seçilen listelerin hepsinde zaten kayıtlıysa
409döner. Var olanı güncellemek içinPUT /contacts/:idya da toplu uçtaupdate: truekullan. - Kişi güncellenince değişiklik henüz aranmamış (ya da cevapsız) tüm üyeliklere yansır. Tamamlanmış aramaların kaydı değişmez;
externalIdve telefon ise her yerde güncellenir. - Şu an aranan kişi silinemez ve numarası değiştirilemez (
409). - Aranmayacaklar listesindeki numara listeye eklenir ama aranmaz (
opted_out).
Liste üyesi durumları
| status | Anlamı |
|---|---|
pending | Sırada |
calling | Şu an aranıyor |
answered | Görüşüldü |
meeting | Görüşmede randevu ya da görüşme ayarlandı |
no_answer | Açılmadı; deneme hakkı varsa yeniden denenir |
skipped | Ulaşılamadı, deneme hakkı bitti |
failed | Arama başlatılamadı |
opted_out | “Beni aramayın” dedi ya da aranmayacaklar listesinde |
Liste üyeleri yanıtı
GET /call-lists/:id/contacts sayfalı bir yanıt döner: items, total, page, pageSize, pages. Her öğe, kişinin alanları ile listedeki arama durumunu birlikte taşır.
{ "items": [ { "id": "673a0c1d2e3f4a5b6c7d8e9f", "targetId": "673a0c1d2e3f4a5b6c7d8e9f", "listId": "672f1a2b3c4d5e6f7a8b9c0d", "externalId": "crm-1042", "phone": "+905321110001", "phoneDisplay": "0532 111 00 01", "name": "Ayşe Yılmaz", "company": "Yılmaz Ltd", "note": "Geçen ay demo istedi", "fields": { "sehir": "İzmir" }, "status": "answered", "outcome": "Görüşüldü", "outcomeCode": "answered", "attempts": 1, "lastAttemptAt": "2026-10-30T10:02:11.000Z", "nextAttemptAt": null, "finishedAt": "2026-10-30T10:04:30.000Z", "callId": "673a0c1d2e3f4a5b6c7d8ea0", "createdAt": "2026-10-21T08:40:12.000Z", "updatedAt": "2026-10-30T10:04:30.000Z" } ], "total": 1, "page": 1, "pageSize": 25, "pages": 1}| Alan | Açıklama |
|---|---|
id, targetId | Kişi/üyelik kimliği; contactIds alanlarında bunu kullanırsın. call_list.contact.status_changed olayındaki targetId ile aynı |
status, outcome, outcomeCode | Listedeki arama durumu; outcome Türkçe serbest metin, outcomeCode sabit kod (Kişi durumları) |
attempts, lastAttemptAt, nextAttemptAt, finishedAt | Deneme sayısı ve zamanlar; nextAttemptAt yalnızca yeniden denenecek kişide doludur |
callId | Son aramanın görüşme kimliği (GET /calls/:id); henüz aranmadıysa null |
updatedAt | Kişi bilgisi ya da arama durumu en son ne zaman değişti |
Artımlı senkron: `updatedSince`. Yalnızca son senkrondan beri değişen üyeleri alırsın. Değer ISO 8601 olabilir; karşılaştırma dahildir (>=), aynı saniyedeki değişiklik kaçmasın diye. updatedSince verildiğinde sıra eskiden yeniye updatedAt iledir; sayfaları sırayla gezip son öğenin updatedAt değerini bir sonraki senkronun başlangıcı yaparsın.
curl "https://app.bagla.ai/api/v1/call-lists/LISTE_ID/contacts?updatedSince=2026-10-30T09:00:00Z&pageSize=100" \ -H "Authorization: Bearer bgl_..."Kişileri listelere bağlama
{ "contactIds": ["673a…", "673b…"], "listIds": ["672f…", "6730…"] }{ "contactIds": ["673a…"], "listIds": ["6730…"] }Yanıt her liste için eklenen (added) ya da çıkarılan (removed) sayıyı verir. Zaten üye olan kişi tekrar eklenmez.
Toplu işlemler ve içe aktarma
Büyük alıcı listelerini tek istekte yükle, güncelle ya da sil. Toplu uçlar tek istekte en fazla 5.000 kayıt işler ve satır satır rapor döner.
| Yol | Ne yapar | |
|---|---|---|
| POST | /contacts | { "contacts": [...] } ile toplu ekleme. update ve listIds desteklenir |
| PATCH | /contacts/bulk | Toplu güncelleme |
| POST | /contacts/bulk-delete | { "ids": [...] } toplu silme |
| POST | /contacts/import | CSV içe aktarma (listIds ile) |
| POST | /call-lists/:id/import | CSV’yi içe aktarır ve doğrudan bu listeye ekler |
Toplu ekleme
{ "update": true, "listIds": ["672f1a2b3c4d5e6f7a8b9c0d"], "contacts": [ { "phone": "05321110002", "name": "Mehmet Kaya", "company": "Kaya A.Ş." }, { "phone": "05321110003", "name": "Zeynep Çelik", "fields": { "sehir": "Ankara" } } ]}update: true: kayıtlı kişi gönderilen dolu alanlarla güncellenir, boş alanlar korunur. Kapalıysa kayıtlı kişi değişmez veexistingolarak sayılır.listIds: işlenen tüm kişiler bu listelere eklenir (kayıtlı olanlar dahil).
Toplu güncelleme
Her satır kişiyi id ya da match (telefon) ile bulur; yalnızca verilen alanlar değişir.
{ "updates": [ { "id": "673a0c1d2e3f4a5b6c7d8e9f", "note": "VIP" }, { "match": "05321110003", "name": "Zeynep Ç.", "company": "Çelik Ltd" } ]}CSV içe aktarma
CSV’yi doğrudan text/csv gövdesiyle ya da JSON içinde text alanıyla gönder. Ayırıcı (virgül, noktalı virgül, sekme) otomatik anlaşılır; Excel’den kopyalanan metin de olur.
curl -X POST https://app.bagla.ai/api/v1/call-lists/LISTE_ID/import \ -H "Authorization: Bearer bgl_..." -H "Content-Type: text/csv" \ --data-binary @kisiler.csvİlk satır başlıksa sütunlar adına göre eşlenir:
| Başlık (örnekler) | Alan |
|---|---|
| telefon, tel, gsm, cep, numara, phone | phone |
| ad, isim, ad soyad, musteri, name | name |
| firma, sirket, isletme, unvan, company | company |
| not, aciklama, note | note |
| id, kimlik, musteri no, referans, external id | externalId |
| diğer sütunlar | serbest alan (fields) |
Başlık yoksa sıra telefon, ad, firma, not olarak alınır. Yüklemeden önce { "text": "...", "preview": true } göndererek sütun eşlemesini ve kaç kişi okunacağını görebilirsin; önizleme hiçbir şey yazmaz.
Rapor
{ "total": 5, "created": 2, "updated": 0, "existing": 1, "assigned": 2, "removed": 0, "skipped": 1, "failed": 1, "errors": [{ "row": 4, "phone": "12", "reason": "Geçerli bir telefon numarası gerekli" }], "items": [{ "id": "673a…", "phone": "+905321110002", "status": "pending", "updated": false, "row": 2 }]}| Alan | Anlamı |
|---|---|
created | Yeni oluşan kişi |
updated | Güncellenen kayıtlı kişi |
existing | Kayıtlı olduğu için değiştirilmeyen kişi |
assigned | Listelere eklenen üyelik |
removed | Silinen kişi (bulk-delete) |
skipped | Aynı istekte tekrar eden numara ya da aranmayacaklardaki numara |
failed | Doğrulamadan geçemeyen satır |
errors | Her hatalı satırın numarası, telefonu ve nedeni |
Toplu silmede şu an aranan kişiler silinmez; raporda errors içinde görünür.
Sayfalama ve tekrarlanan istekler
Sayfalama
Liste uçları page (1’den başlar) ve pageSize (varsayılan 25, en fazla 200; limit de kabul edilir) alır. Toplam sayı yanıt gövdesinde ve X-Total-Count başlığında gelir:
{ "items": [ ... ], "total": 120, "page": 1, "pageSize": 25, "pages": 5 }Tekrarlanan istekler (Idempotency-Key)
Liste oluşturma, kişi ekleme, içe aktarma, atama ve webhook ekleme uçları Idempotency-Key başlığını destekler:
curl -X POST https://app.bagla.ai/api/v1/call-lists/LISTE_ID/import \ -H "Authorization: Bearer bgl_..." -H "Idempotency-Key: yukleme-2026-11-02" \ -H "Content-Type: text/csv" --data-binary @kisiler.csvAynı anahtarla gelen ikinci istek işlenmez: ilk yanıt döner ve yanıtta Idempotent-Replay: true başlığı bulunur. Ağ hatasında isteği güvenle yeniden gönderebilirsin. Anahtarlar 24 saat saklanır (8–128 karakter); aynı anahtar farklı bir gövdeyle gelirse 422.
Anahtarsız istekler de mükerrer kayıt oluşturmaz: aynı telefon tek kişidir, aynı ad tek listedir (409).
Otomatik arama kuyruğu
Kuyruk nasıl çalışır
Otomatik arama kuyruğu, tek bir kuyruğa kişi basıp sırayla aratmanın en kısa yoludur; liste ve numara seçimi gerekmiyorsa bunu kullan. Asistana kendi sisteminden (CRM, ERP, tablo, bir yapay zekâ ajanı) arayacağı kişileri gönder; asistan onları belirlediğin saatlerde, sırayla ve kişiye özel bir açılış cümlesiyle arasın.
- Panelde Geliştirici sayfasında geliştirici ayarlarını etkinleştirip bir API anahtarı oluşturmuş olmalısın.
- Arama ayarları sekmesinde açılış cümlesini, arama yönergesini, arama saatlerini, günleri ve aramalar arası beklemeyi ayarla.
- Sisteminden
POST /outbound/contactsile kişileri gönder. Her kişi kuyruğun sonuna eklenir. POST /outbound/campaign/startile kuyruğu başlat. Asistan, arama saatleri içinde aynı anda en fazlaconcurrency(1–3) kişiyi arar; bir görüşme bitincegapSecondskadar bekleyip boşalan hatta sıradakini alır.GET /outbound/contactsveGET /outbound/reportile sonuçları oku.
Kuyruk işletme başına tektir ve ilk dokunuşta kendiliğinden oluşur. Panelden elle açılan arama listelerinden bağımsızdır; panelde Geliştirici → Otomatik arama sekmesinde görünür.
Kişi gönderme
POST /outbound/contacts — gövde üç biçimde olabilir: tek kişi, kişi dizisi ya da { "contacts": [...] }.
{ "contacts": [ { "phone": "0532 123 45 67", "name": "Ahmet Yılmaz", "company": "Yılmaz Ticaret", "externalId": "crm-10234", "note": "Geçen ay demo istemişti", "fields": { "yas": 21, "paket": "Pro", "son_alisveris": "12 Ekim" } } ]}| Alan | Zorunlu | Sınır |
|---|---|---|
phone | evet | Boşluk ve tire temizlenir; 0532…, +90 532…, 90532… yazımları E.164’e çevrilir. Yurt dışı numaraları + ile yaz |
name | hayır | en fazla 80 karakter |
company | hayır | en fazla 120 karakter |
note | hayır | en fazla 300 karakter |
externalId | hayır | en fazla 100 karakter; kendi sistemindeki kimlik |
fields | hayır | en fazla 20 alan; anahtar harf, rakam ve _ (en fazla 40 karakter); değer metin, sayı ya da doğru/yanlış, en fazla 200 karakter |
Tek istekte en fazla 500 kişi, kuyrukta toplam en fazla 2.000 kişi olabilir.
Yanıt 201:
{ "added": 1, "updated": 0, "skipped": [], "contacts": [{ "id": "673a0c1d2e3f4a5b6c7d8e9f", "phone": "+905321234567", "updated": false, "status": "pending" }], "status": "paused"}- Aynı numarayı tekrar gönderirsen ve kişi henüz aranmamışsa bilgileri güncellenir (
updated: true). Aranmış kişi atlanır (skipped, “Listede zaten var”). - Aranmayacaklar listesindeki numaralar kuyruğa
opted_outolarak girer, aranmaz veskippediçinde görünür. - Geçersiz numaralar
skippediçinde nedeniyle birlikte döner; diğer kişiler yine eklenir. statuskuyruğun durumudur (runningya dapaused). Kişi eklemek kuyruğu başlatmaz.
Açılış cümlesi ve şablonlar
Panelde yazdığın açılış cümlesi ve arama yönergesi her kişi için doldurulur. Yer tutucu %alan% ya da {{alan}} biçimindedir.
Merhaba %ad%, ben Derya, Nar Kuaför adına arıyorum. %firma% için %paket% paketiyle ilgili kısa bir bilgi vermek istiyorum; müsait misiniz?- Hazır alanlar:
%ad%,%firma%,%telefon%,%not%. - Kişiye gönderdiğin her
fieldsanahtarı da kullanılabilir:%yas%,%paket%,%son_alisveris%. - Büyük/küçük harf ve Türkçe karakter farkı gözetilmez (
%Şirket%,%sirket%ve%firma%aynı alanı bulur). - Karşılığı olmayan yer tutucu boş kalır ve cümle toparlanır (“Merhaba %ad%,” ad yoksa “Merhaba,” okunur).
- Gönderdiğin tüm alanlar ayrıca asistana “Bu kişi hakkında bilgiler” olarak verilir; asistan şablonda geçmeyen alanları da konuşmada kullanabilir.
- Panelde Arama ayarları sekmesinin altındaki önizleme, örnek bir kişiyle cümlenin nasıl okunacağını gösterir.
Kuyruğu yönetme
| Yol | Ne yapar | |
|---|---|---|
| GET | /outbound/campaign | Ayarları, durumu ve kişi sayılarını verir |
| POST | /outbound/campaign/start | Kuyruğu başlatır |
| POST | /outbound/campaign/pause | Kuyruğu duraklatır; süren görüşme kesilmez |
Ayarlar (açılış cümlesi, yönerge, saatler, günler, bekleme) panelde yapılır. GET /outbound/campaign yanıtı:
{ "id": "6730a1b2c3d4e5f6a7b8c9d0", "status": "running", "greeting": "Merhaba %ad%, ...", "purpose": "...", "windowStart": "10:00", "windowEnd": "18:00", "days": [1, 2, 3, 4, 5], "gapSeconds": 1, "concurrency": 1, "maxAttempts": 2, "retryMinutes": 120, "consentBasis": "consent", "timezone": "Europe/Istanbul", "counts": { "total": 40, "pending": 31, "calling": 0, "answered": 5, "no_answer": 2, "meeting": 1, "opted_out": 1, "failed": 0, "skipped": 0 }}days: 0 = pazar, 1 = pazartesi … 6 = cumartesi.- Arama saatleri 09:00–20:00 dışına ayarlanamaz. Saatler işletmenin saat dilimine (varsayılan İstanbul) göredir.
concurrency(1–3): aynı anda kaç kişi aranır.gapSeconds, biten bir görüşmeden sonra sıradakine geçmeden önceki bekleme süresidir. Her görüşme ayrı ücretlendirilir.- Saat ya da gün dışındayken kuyruk bekler, kişiler
pendingkalır. maxAttempts(1–3): bir kişiye en fazla kaç kez ulaşılmaya çalışılır. Cevap yoksaretryMinutes(30–1440) sonra yeniden denenir; öncelik hiç aranmamış kişilerdedir.- Başlatmak için açılış cümlesi ya da yönergeden biri yazılmış olmalı, yoksa
409. Aylık tavan dolduğunda kuyruk bekler, kişiler hata sayılmaz; tavan yükseltilince ya da ay başında kaldığı yerden devam eder.
CSV ile ve panel listelerine aktarma
POST /outbound/contacts JSON’un yanında CSV de kabul eder: Content-Type: text/csv ile başlıklı bir dosya gönder, tek istekte en fazla 2.000 kişi.
curl -X POST "https://app.bagla.ai/api/v1/outbound/contacts" \ -H "Authorization: Bearer $BAGLA_KEY" -H "Content-Type: text/csv" \ --data-binary @kisiler.csvtelefon;ad;firma;not;musteri_no;paket0532 000 00 01;Ayşe Yılmaz;Örnek Ambalaj;Kalite müdürü;M-1001;Pro- Ayraç (virgül, noktalı virgül, sekme) kendiliğinden bulunur. Excel’in Türkçe CSV’si olduğu gibi çalışır.
- İlk satır başlıksa sütunlar adına göre eşlenir: telefon (tel, gsm, cep, phone), ad (isim, ad soyad, name), firma (şirket, company), not (note, açıklama), kimlik (id, musteri_no, externalId). Tanınmayan her sütun serbest alan olur (
paket→%paket%); boş hücreler atlanır. - Başlık yoksa sıra telefon, ad, firma, not’tur.
- Geçerli telefonu olmayan satırlar atlanır; yanıttaki
skippediçinde görünür.
Panelden açtığın bir arama listesine de aynı şekilde aktarabilirsin: POST /call-lists/:id/contacts JSON ya da CSV alır; aranmamış kişi tekrar gelirse güncellenir, tamamlanmış liste için 409, olmayan liste için 404 döner.
Kişi durumları
GET /outbound/contacts ve GET /outbound/contacts/:id aşağıdaki durumlardan birini döner.
| Durum | Anlamı |
|---|---|
pending | Sırada, henüz aranmadı |
calling | Şu an aranıyor |
answered | Telefon açıldı, görüşme yapıldı |
meeting | Görüşme sonunda randevu / toplantı alındı |
no_answer | Açılmadı ya da çok kısa sürdü; deneme hakkı varsa yeniden denenecek |
opted_out | Aranmak istemedi (görüşmede ya da listede); bir daha aranmaz |
failed | Arama başlatılamadı |
skipped | Deneme hakkı bitti, ulaşılamadı |
Sonuç kodu (`outcomeCode`). outcome serbest metindir; entegrasyonlar bunun yerine aşağıdaki sabit koda bağlanmalıdır. Liste üyelerinde ve call_list.contact.status_changed olayında bulunur; pending kişide nulldır.
| outcomeCode | Ne zaman | status |
|---|---|---|
calling | Şu an aranıyor | calling |
answered | Telefon açıldı, görüşüldü | answered |
meeting | Görüşmede randevu / toplantı alındı | meeting |
no_answer | Açılmadı ya da çok kısa sürdü | no_answer |
unreachable | Ulaşılamadı (operatör hatası: kapalı, meşgul vb.) | no_answer |
retries_exhausted | Deneme hakkı bitti | skipped |
automated | Karşıdaki sesli menü ya da telesekreter; asistan kapattı | skipped |
opted_out | Aranmak istemedi | opted_out |
start_failed | Arama başlatılamadı | failed |
Kişi nesnesi:
{ "id": "673a0c1d2e3f4a5b6c7d8e9f", "externalId": "crm-10234", "phone": "+905321234567", "phoneDisplay": "0532 123 45 67", "name": "Ahmet Yılmaz", "company": "Yılmaz Ticaret", "note": "", "fields": { "yas": 21 }, "status": "answered", "outcome": "İlgilendi, fiyat bilgisi istedi", "outcomeCode": "answered", "attempts": 1, "lastAttemptAt": "2026-10-21T09:12:44.000Z", "finishedAt": "2026-10-21T09:14:02.000Z", "order": null, "callId": "673a0c1d2e3f4a5b6c7d8ea0"}GET /outbound/contacts/:id ayrıca call alanını verir: süre, özet ve konuşma dökümü. :id olarak Bağla kimliğini ya da kendi externalId değerini kullanabilirsin. order, sıradaki (pending) kişinin kuyruk sırasıdır; diğerlerinde null.
Okuma ve silme
| Yol | Ne yapar | |
|---|---|---|
| GET | /outbound/contacts | Kişi listesi. Süzgeçler: status, externalId, limit (1–500, varsayılan 100), offset |
| GET | /outbound/contacts/:id | Tek kişi, görüşme özeti ve dökümüyle |
| DELETE | /outbound/contacts/:id | Kişiyi kuyruktan çıkarır. Yalnızca pending olanlar silinir, diğerleri için 409 |
| GET | /outbound/live | Şu an aranan kişiler: durum (ringing çalıyor / talking konuşuyor), geçen süre, deneme |
| GET | /outbound/report | Kuyruk raporu (from, to; varsayılan son 30 gün) |
Rapor yanıtı:
{ "business": { "id": "6701…", "name": "Nar Kuaför" }, "from": "2026-10-01", "to": "2026-10-31", "status": "running", "contacts": { "total": 40, "pending": 31, "calling": 0, "answered": 5, "meeting": 1, "no_answer": 2, "opted_out": 1, "failed": 0, "skipped": 0 }, "calls": { "attempted": 9, "answered": 6, "meetings": 1, "noAnswer": 3, "totalMinutes": 14.2, "avgDurationSecs": 94 }, "byDay": [{ "date": "2026-10-21", "calls": 9, "answered": 6, "meetings": 1, "minutes": 14.2 }]}Tüm aramaları (gelen ve giden) kapsayan okuma uçları da vardır:
| Yol | Ne yapar | |
|---|---|---|
| GET | /calls | Görüşme listesi: from, to (YYYY-AA-GG), direction (inbound / outbound), outcome, pageSize (en fazla 200), before (ISO imleç) |
| GET | /calls/:id | Özet ve konuşma dökümü |
| GET | /calls/:id/recording | Ses kaydı (Ses kayıtları) |
| GET | /reports/summary | Toplam, gelen, giden, yanıtlanan görüşme, dakika, alınan randevu ve günlük dağılım |
Görüşmeler ve kayıtlar
Ses kayıtları
Kaydı açık olan işletmelerde görüşmenin ses kaydını API ile alabilirsin. Kayıt görüşme bittikten bir süre sonra işlenir; hazır olunca call.recording_ready olayı gelir. Olayda ses dosyası değil, indirme yolu bulunur: dosyayı API anahtarınla ayrıca istersin. Böylece ses kaydı hiçbir zaman herkese açık bir adresle paylaşılmaz ve webhook gövdesinde dolaşmaz.
# 1) Olayda gelen yol# "recording": { "available": true, "path": "/api/v1/calls/673a…/recording" }# 2) Dosyayı indircurl -H "Authorization: Bearer $BAGLA_KEY" -o gorusme.wav \ "https://app.bagla.ai/api/v1/calls/673a…/recording"| Durum | Yanıt |
|---|---|
| Kayıt var | 200, ses dosyası (audio/wav ya da audio/ogg) |
| Görüşme senin işletmenin değil ya da yok | 404 |
| Kayıt tutulmamış ya da saklama süresi dolup silinmiş | 404 |
| Kayıt henüz işlenmedi | 404 (“Kayıt henüz hazır değil”); call.recording_ready olayını bekle |
| Dakikada 20’den fazla indirme | 429 ve Retry-After |
Güvenlik: yalnızca kendi işletmenin görüşmelerine erişilir; yanıt private, no-store ile gelir ve her indirme denetim kaydına yazılır (panelde KVKK ve gizlilik → İşlem kayıtları). Ses kaydı kişisel veridir: saklama süresi dolunca silinir; indirdiğin kopyanın KVKK sorumluluğu sana aittir. İstemiyorsan webhook’a call.recording_ready ekleme; kayıtlar yine panelden dinlenebilir.
OpenAPI tanımı
Tüm uçların makine okunur tanımı herkese açık olarak /api/v1/openapi.json adresinde (OpenAPI 3.1, anahtar gerekmez). Postman ya da Insomnia’ya “içe aktar” ile verirsen koleksiyon hazır gelir; kod üreticileri (openapi-generator vb.) bu dosyadan istemci üretir.
Aynı tanım bu sitede /openapi.json yolundan ve YAML olarak Bağla deposunda docs/api/openapi.yaml dosyasında bulunur.
Webhook’lar
Webhook’lara giriş
Webhook’ta sen Bağla’yı sormazsın, Bağla seni arar. Bir görüşme bitince, randevu alınınca ya da bir mesaj gidince Bağla, belirlediğin adrese imzalı bir POST isteği gönderir. Raporları, dökümleri ve durum değişikliklerini sürekli sorgulamak (polling) yerine olay olduğu an kendi sisteminde işlersin.
| Sorgulama (polling) | Webhook | |
|---|---|---|
| Veri ne zaman gelir | Sorduğun anda | Olay olduğu an |
| İstek sayısı | Çoğu boş döner | Yalnızca olay olunca |
| Kaçan olay riski | Aralığı yanlış seçersen | Yeniden deneme ve olay geçmişi korur |
| Sınır | Dakikada 120 istek | Yok |
Müşteri asistanı arar ──► görüşme biter ──► Bağla olayı kaydeder │ POST https://sistemin.com/bagla/webhook Bagla-Signature: t=...,v1=... │ sen 2xx döndürürsün ──► teslim edildi 2xx dönmezsen ──► artan aralıklarla yeniden denenirWebhook’lar işletmeye özeldir: yalnızca kendi işletmenin olayları gelir. Panelde Geliştirici → Webhook’lar sekmesinden ya da bu sayfadaki API’den yönetilir. Çalışması için geliştirici ayarlarının açık olması yeterlidir; giden arama onayı ya da özel bir plan gerekmez. Geliştirici ayarlarını kapatırsan yeni olay gönderilmez.
Webhook hızlı başlangıç
Dört adımda ilk olayını al. bgl_... yerine kendi anahtarını yaz.
1. Alıcı adresini hazırla. Herkese açık bir https adresi olmalı ve olayı alınca hızlıca 2xx dönmeli. İmzayı doğrulayan örnek kodlar İmza doğrulama bölümünde.
2. Webhook’u ekle. Yalnızca görüşme raporlarını istiyorsan call.completed yeterli.
curl -X POST https://app.bagla.ai/api/v1/webhooks \ -H "Authorization: Bearer bgl_..." \ -H "Content-Type: application/json" \ -d '{ "url": "https://sistemin.com/bagla/webhook", "description": "CRM senkronu", "events": ["call.completed"] }'{ "id": "6740b1c2d3e4f5a6b7c8d9e0", "url": "https://sistemin.com/bagla/webhook", "description": "CRM senkronu", "events": ["call.completed"], "active": true, "includeTranscript": true, "secretHint": "whsec_aB3d…", "previousSecretExpiresAt": null, "failureStreak": 0, "disabledReason": null, "lastDeliveryAt": null, "lastSuccessAt": null, "lastFailureAt": null, "createdAt": "2026-10-30T09:12:44.000Z", "secret": "whsec_aB3dE5gH7jK9mN1pQ3sT5vW7yZ9bC2dF"}secret yalnızca bu yanıtta tam görünür. Gelen isteklerin Bağla’dan geldiğini bununla doğrularsın; sunucunun gizli ayarlarına (ortam değişkeni) kaydet. Kaybedersen anahtarı yenile (Webhook yönetimi).
3. Dene. Bir deneme olayı gönderir ve sonucu hemen döner.
curl -X POST https://app.bagla.ai/api/v1/webhooks/WEBHOOK_ID/test \ -H "Authorization: Bearer bgl_..."{ "delivery": { "id": "6740…", "eventType": "webhook.test", "status": "delivered", "attempts": 1, "responseCode": 200, "durationMs": 143, "error": null } }4. Gerçek bir görüşme yap. Görüşme bitip işlendiğinde call.completed olayı adresine gelir. Hata alırsan Sorun giderme tablosuna bak.
Webhook yönetimi
Webhook’lar panelden ya da API’den yönetilir. Yetki için Kimlik doğrulama geçerlidir.
| Yol | Ne yapar | |
|---|---|---|
| GET | /webhooks | Webhook’lar, olay kataloğu ve teslimat kuralları |
| POST | /webhooks | Webhook ekler (201). Yanıtta secret bir kez görünür |
| GET | /webhooks/:id | Tek webhook |
| PATCH | /webhooks/:id | url, description, events, active, includeTranscript alanlarından gönderdiğini günceller |
| DELETE | /webhooks/:id | Webhook’u ve teslimat geçmişini siler |
| POST | /webhooks/:id/rotate-secret | İmza anahtarını yeniler. Gövde: { "graceHours": 24 } (isteğe bağlı, 0–168; varsayılan 24). Yanıtta yeni secret görünür |
| POST | /webhooks/:id/test | Örnek olay gönderir, sonucu hemen döner. Gövde: { "eventType": "appointment.created" } (isteğe bağlı; varsayılan webhook.test) |
| GET | /webhooks/:id/deliveries | Teslimat geçmişi: limit (en fazla 100), before, status (pending, retrying, delivered, failed) |
| POST | /webhook-deliveries/:id/retry | Başarısız ya da bekleyen teslimatı hemen yeniden dener |
| GET | /webhook-events | Olay geçmişi (son 7 gün): type, limit, before |
| GET | /webhook-events/:id | Tek olay (gönderilen zarfın aynısı) |
| POST | /webhook-events/:id/resend | Olayı seçilen webhook’a yeniden gönderir: { "webhookId": "..." } |
| Alan | Açıklama |
|---|---|
url | Zorunlu. https:// ile başlamalı, en fazla 500 karakter. Kullanıcı adı/parola ve # parçası içeremez. Herkese açık bir alan adı ya da IP’ye işaret etmeli |
events | Abone olunan olaylar. ["*"] ya da boş: hepsi. ["call.*"]: önek. ["call.completed", "order.created"]: tek tek. Bilinmeyen tür 400 döner |
description | İsteğe bağlı, en fazla 200 karakter |
active | false yapınca olay gönderilmez; yeniden true yapınca hata sayaçları sıfırlanır |
includeTranscript | Varsayılan true. false yapınca call.completed olayında konuşma dökümü hiç gönderilmez, yalnızca özet gider (dökümü saklamak istemeyenler için) |
Kurallar
- İşletme başına en fazla 10 webhook tanımlanır (
409). - Bir olay, abone olan her webhook’a ayrı ayrı gönderilir.
- Adres kaydedilirken ve her gönderimde denetlenir.
localhost, özel ağ (10.x, 192.168.x, 172.16–31.x), 169.254.x ve benzeri adresler reddedilir; alan adı özel bir IP’ye çözümlenirse istek hiç gönderilmez.
Anahtar yenileme ve geçiş süresi
Anahtarı yenilerken alıcını kesintisiz taşımak için graceHours ver (varsayılan 24). Geçiş süresince her istek iki anahtarla imzalanır ve başlıkta iki v1 bulunur: Bagla-Signature: t=1761815000,v1=<yeni anahtarla>,v1=<eski anahtarla>. Alıcın bildiği anahtarla doğrulayan imzayı bulur (örnekler tüm v1 değerlerine bakar). Süre dolunca yalnızca yeni anahtarla imzalanır; webhook nesnesindeki previousSecretExpiresAt geçiş bitiş zamanını gösterir. Önerilen sıra: anahtarı yenile → yeni whsec_... değerini sisteminin ayarlarına ekle → süre dolmasını bekle.
Eski alıcı kodun yalnızca ilk v1 değerine bakıyorsa (yeni anahtarı henüz bilmiyorsa) geçiş süresinde doğrulama tutmaz. Alıcını önce tüm v1 değerlerine bakacak şekilde güncelle.
Örnek olay gönderme
POST /webhooks/:id/test ile seçtiğin olay türünden örnek yük gönderebilirsin; böylece gerçek bir görüşme ya da randevu beklemeden alıcı kodunu denersin:
curl -X POST https://app.bagla.ai/api/v1/webhooks/WEBHOOK_ID/test \ -H "Authorization: Bearer bgl_..." -H "Content-Type: application/json" \ -d '{ "eventType": "appointment.created" }'- Yük, gerçek olayla aynı alanlara sahiptir; değerler belirgin biçimde örnektir (
000000000000000000000000,#0000,+905320000000). - Zarfta
"sample": truebulunur. Örnek olayları CRM’ine kaydetme: bu alana bakıp atla. - Webhook o türe abone olmasa da gönderilir.
includeTranscript: falseise örnekcall.completed’de de döküm olmaz. - Örnek olaylar olay geçmişinde listelenmez;
eventTypeverilmezsewebhook.testgider. - Kapalı bir webhook’a olay gönderilmez ama olay geçmişine yazılır. Webhook’u yeniden açınca kaçanları Olay geçmişi’nden gönderebilirsin.
Olay zarfı
Her istek aynı biçimdedir: gövde bir JSON zarfı, olaya özgü veri data içindedir.
{ "id": "6740c1d2e3f4a5b6c7d8e9f0", "type": "call.completed", "apiVersion": "1", "createdAt": "2026-10-30T09:16:31.402Z", "businessId": "6701a2b3c4d5e6f7a8b9c0d1", "data": { }}| Alan | Açıklama |
|---|---|
id | Olayın kimliği. Aynı olay yeniden denemelerde aynı id ile gelir; tekilleştirmek için bunu kullan |
type | Olay türü, örn. call.completed |
apiVersion | Zarf sürümü. Bugün "1". Geriye uyumsuz değişiklik olursa yeni sürüm numarası verilir; yeni alan eklemek uyumsuz sayılmaz, bilmediğin alanları yok say |
createdAt | Olayın oluştuğu an (ISO 8601, UTC) |
businessId | Olayın ait olduğu işletme |
sample | Yalnızca örnek olaylarda bulunur ve truedur. Gerçek olaylarda bu alan yoktur |
data | Olaya özgü veri; her olay için Olay türleri’nde |
İstek başlıkları
| Başlık | Değer |
|---|---|
Content-Type | application/json |
User-Agent | Bagla-Webhooks/1.0 |
Bagla-Event-Id | Zarftaki id ile aynı |
Bagla-Event-Type | Zarftaki type ile aynı. Gövdeyi okumadan yönlendirmek için kullan |
Bagla-Delivery-Id | Bu teslimatın kimliği |
Bagla-Attempt | Kaçıncı deneme: 1, 2, … |
Bagla-Timestamp | İmzanın üretildiği an (Unix saniye) |
Bagla-Signature | t=<zaman>,v1=<imza> (İmza doğrulama) |
İmza doğrulama
Adresin herkese açıktır; bu yüzden gelen isteğin gerçekten Bağla’dan geldiğini her istekte imzayla doğrula. İmza yoksa ya da tutmuyorsa isteği işleme ve 400 dön.
İmza şöyle hesaplanır:
Bagla-Signaturebaşlığındant(Unix saniye) ve tümv1(imza) değerlerini al. Normalde birv1vardır; anahtar yenileme geçiş süresinde iki tane gelir (önce yenisi, sonra eskisi).- İmzalanan metin:
t+.+ ham gövde (gelen baytlar, hiç değiştirmeden). - Beklenen imza: bu metnin, webhook’un
whsec_...anahtarıyla HMAC-SHA256 özeti, küçük harfli onaltılık. v1değerlerinden biri beklenen imzayla tutuyorsa istek geçerlidir; karşılaştırmayı sabit zamanlı yap.tdeğeri şimdiden 5 dakikadan eskiyse isteği reddet (kaydedilmiş bir isteğin sonradan tekrar gönderilmesine karşı).
Ham gövdeyi kullan. JSON’u çözüp yeniden yazarsan boşluklar ve alan sırası değişir, imza tutmaz. Çerçevenin gövdeyi otomatik çözmesini kapat ya da ham baytlara eriş. Yeniden denemelerde her deneme için yeni t ve imza üretilir; imzayı saklayıp karşılaştırma.
Node.js (Express)
import crypto from "node:crypto";import express from "express";const SECRET = process.env.BAGLA_WEBHOOK_SECRET; // whsec_...const app = express();// Ham gövde için express.json() yerine express.raw() kullanapp.post("/bagla/webhook", express.raw({ type: "application/json" }), (req, res) => { const header = req.get("bagla-signature") ?? ""; const t = /t=(\d+)/.exec(header)?.[1]; const signatures = [...header.matchAll(/v1=([0-9a-f]{64})/g)].map((m) => m[1]); // anahtar geçişinde birden çok olabilir if (!t || !signatures.length) return res.sendStatus(400); if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(400); const expected = crypto.createHmac("sha256", SECRET).update(Buffer.concat([Buffer.from(`${t}.`), req.body])).digest("hex"); if (!signatures.some((v1) => v1.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)))) { return res.sendStatus(400); } const event = JSON.parse(req.body.toString("utf8")); res.sendStatus(200); // önce yanıt ver, işi sonra yap queue.add(event); // kendi iş kuyruğun});Python (Flask)
import hashlib, hmac, os, re, timefrom flask import Flask, abort, requestSECRET = os.environ["BAGLA_WEBHOOK_SECRET"].encode() # whsec_...app = Flask(__name__)@app.post("/bagla/webhook")def bagla_webhook(): header = request.headers.get("Bagla-Signature", "") m = re.search(r"t=(\d+)", header) signatures = re.findall(r"v1=([0-9a-f]{64})", header) # anahtar geçişinde birden çok olabilir if not m or not signatures: abort(400) t = m.group(1) if abs(time.time() - int(t)) > 300: abort(400) expected = hmac.new(SECRET, t.encode() + b"." + request.get_data(), hashlib.sha256).hexdigest() if not any(hmac.compare_digest(expected, v1) for v1 in signatures): abort(400) event = request.get_json() enqueue(event) # kendi iş kuyruğun return "", 200Bağla’nın TypeScript paketini kullananlar verifyWebhookSignature({ header, body, secret }) yardımcısını aynı kurallarla hazır bulur.
Teslimat ve yeniden denemeler
Bağla olayı en az bir kez (at-least-once) teslim etmeyi hedefler. Bu yüzden aynı olay nadiren iki kez gelebilir; id ile tekilleştir.
Ne başarı sayılır. Adresin 10 saniye içinde 2xx döndürürse teslim edilmiştir. Yanıt gövdesi önemsizdir. 3xx (yönlendirme izlenmez), 4xx, 5xx, zaman aşımı, bağlantı ya da TLS hatası başarısızlıktır.
Yeniden deneme. Başarısız teslimat aşağıdaki aralıklarla yeniden denenir. Toplam 8 deneme, yaklaşık 46 saat.
| Deneme | Bir önceki başarısız denemeden sonra bekleme |
|---|---|
| 1 | hemen |
| 2 | 1 dakika |
| 3 | 5 dakika |
| 4 | 15 dakika |
| 5 | 1 saat |
| 6 | 3 saat |
| 7 | 6 saat |
| 8 | 12 saat |
| — | (24 saat: son denemeden sonra teslimat failed olur) |
Sekiz deneme de başarısız olursa teslimat failed olur. Panelde Teslimatlar çekmecesinden ya da POST /webhook-deliveries/:id/retry ile elle yeniden denenebilir (deneme sayacı sıfırdan başlar).
Otomatik kapanma. Art arda 5 olay tüm denemelerine rağmen teslim edilemezse webhook kapatılır (active: false, nedeni disabledReason’da yazar). Adresi düzeltip active: true yaparak yeniden aç; açınca sayaçlar sıfırlanır. Kapalıyken kaçan olayları Olay geçmişi ile geri alabilirsin.
Alıcı için öneriler
- Hızlı yanıt ver. İmzayı doğrula, olayı kendi kuyruğuna yaz, hemen
200dön; işi sonra yap. Yavaş yanıt zaman aşımına, tekrara ve sonunda kapanmaya yol açar. - Tekilleştir. İşlediğin
iddeğerlerini sakla. Aynıidtekrar gelirse200dön ama yeniden işleme. - Sıraya güvenme. Olaylar üretildikleri sırayla gitmeyebilir (bir olay yeniden denenirken sonraki teslim edilir). Sıra gerekiyorsa zarftaki
createdAtya da nesnenin kendi zamanını kullan. - Bilinmeyen alanları ve olay türlerini yok say. Zamanla yeni alan ve olay eklenir. Tanımadığın türe
200dön. - IP izin listesine güvenme. Gönderim IP’leri değişebilir. Doğrulama için imzayı kullan.
- Anahtarı korumak senin işin. Sızdığından şüphelenirsen
rotate-secretile yenile.
Veri saklama. Gönderilen olaylar 7 gün, teslimat kayıtları 14 gün saklanır, sonra silinir. Görüşme dökümleri işletmenin saklama süresine de tabidir.
Olay türleri
| Tür | Ne zaman |
|---|---|
call.completed | Bir görüşme bitti (gelen/giden; açılmayan ve başarısız aramalar dahil) |
call.recording_ready | Görüşmenin ses kaydı hazır oldu; kayıt olaydaki yoldan API anahtarınla indirilir (Ses kayıtları) |
appointment.created | Yeni bir randevu oluşturuldu |
appointment.cancelled | Randevu iptal edildi (panelden, müşterinin mesajıyla ya da asistan tarafından) |
appointment.confirmed | Müşteri randevusunu onayladı |
order.created | Asistan yeni bir sipariş aldı |
order.status_changed | Siparişin durumu değişti |
customer.created | Yeni müşteri ya da potansiyel müşteri kaydı açıldı |
customer.updated | Müşteri kaydı güncellendi (durum, ad, e-posta, etiket…) |
message.sent | SMS, WhatsApp ya da e-posta gönderildi |
message.failed | Bir mesaj gönderilemedi |
message.received | Müşteriden SMS ya da WhatsApp yanıtı geldi |
call_list.contact.status_changed | Arama listesindeki bir kişinin durumu değişti |
call_list.status_changed | Bir arama listesi (ya da kuyruk) başladı, durdu ya da tamamlandı |
do_not_call.added | Numara aranmayacaklar listesine eklendi (panelden ya da asistan tarafından) |
do_not_call.removed | Numara aranmayacaklar listesinden çıkarıldı (panelden) |
webhook.test | Deneme olayı (yalnızca sen istediğinde) |
Aşağıdaki örneklerde yalnızca data gösterilir; zarf Olay zarfı’ndaki gibidir. Zamanlar ISO 8601 UTC’dir; randevu tarihi ve saati işletmenin saat dilimindedir (timezone). Telefonlar E.164 (phone) ve okunur biçimde (phoneDisplay) birlikte gelir.
call.completed
Bir görüşme bitip kaydı işlendiğinde gelir; çoğunlukla 1–3 dakika içinde. data.call, GET /calls/:id ile aynı nesnedir ve konuşma dökümünü içerir.
{ "call": { "id": "673a0c1d2e3f4a5b6c7d8ea0", "startedAt": "2026-10-30T09:14:02.000Z", "endedAt": "2026-10-30T09:16:24.000Z", "direction": "inbound", "phone": "+905321234567", "phoneDisplay": "0532 123 45 67", "customerId": "6720…", "customerName": "Ayşe Yılmaz", "durationSecs": 142, "answered": true, "result": "served", "title": "Cumartesi için randevu", "summary": "Müşteri cumartesi 14:00 için saç kesimi randevusu aldı.", "campaign": null, "appointmentId": "6731…", "orderId": null, "recording": { "available": false, "status": "pending", "path": null }, "transcript": [ { "role": "asistan", "text": "Nar Kuaför, ben Derya. Size nasıl yardımcı olabilirim?", "secs": 1 }, { "role": "musteri", "text": "Cumartesi için randevu almak istiyorum.", "secs": 6 } ] }}| Alan | Açıklama |
|---|---|
id | Görüşme kimliği (GET /calls/:id ile aynı) |
direction | inbound (gelen) ya da outbound (giden) |
durationSecs, answered | Konuşma süresi ve açılıp açılmadığı |
result | served, greeted_only, no_answer, busy, voicemail, escalated, system_fault |
campaign | Görüşme bir arama listesinden ya da kuyruktan yapıldıysa { id, contactId }, değilse null |
recording | { available, status, path }. Ses kaydı hazırsa available: true ve indirme yolu |
transcript | Konuşma dökümü: role (asistan / musteri), text, secs. Saklama süresi dolmuş görüşmede boştur. Webhook’ta includeTranscript: false ise bu alan hiç bulunmaz |
appointment.created, appointment.cancelled, appointment.confirmed
Üç olay da aynı appointment nesnesini taşır; status ve customerConfirmed değişir.
{ "appointment": { "id": "6731a2b3c4d5e6f7a8b9c0d2", "code": "#4821", "status": "confirmed", "customerId": "6720…", "customerName": "Ayşe Yılmaz", "phone": "+905321234567", "phoneDisplay": "0532 123 45 67", "service": { "id": "sac-kesimi", "name": "Saç kesimi" }, "staff": { "id": "stf_ece", "name": "Ece" }, "date": "2026-11-07", "startTime": "14:00", "endTime": "14:30", "timezone": "Europe/Istanbul", "note": null, "notifyChannel": "sms", "customerConfirmed": false, "source": "phone", "createdAt": "2026-10-30T09:16:12.000Z", "cancelledAt": null, "cancelReason": null, "callId": "673a0c1d2e3f4a5b6c7d8ea0" }}| Alan | Açıklama |
|---|---|
code | Müşterinin bildiği randevu kodu (işletme içinde benzersiz) |
status | pending, confirmed, cancelled, completed, no_show |
staff | Personel atanmadıysa null |
notifyChannel | Müşterinin bilgilendirme tercihi: sms, whatsapp, email ya da null |
customerConfirmed | Müşteri mesajla onayladıysa true |
callId | Randevunun alındığı görüşme; panelden açıldıysa null |
order.created, order.status_changed
{ "order": { "id": "6732b3c4d5e6f7a8b9c0d1e3", "code": "#87", "status": "preparing", "type": "delivery", "customerName": "Mehmet Demir", "phone": "+905329876543", "phoneDisplay": "0532 987 65 43", "address": "Moda Cad. 12/4 Kadıköy", "items": [{ "name": "Karışık pizza", "qty": 2, "price": 280 }], "subtotal": 560, "deliveryFee": 30, "discount": 0, "total": 590, "note": null, "notifyChannel": null, "source": "phone", "createdAt": "2026-10-30T11:02:40.000Z", "callId": "673a…" }, "previousStatus": "pending"}previousStatus yalnızca order.status_changed olayında bulunur. status: pending, preparing, ready, delivered, cancelled. type: pickup ya da delivery (address yalnızca teslimatta dolu). Tutarlar TL cinsinden sayıdır.
customer.created, customer.updated
{ "customer": { "id": "6720c4d5e6f7a8b9c0d1e2f4", "phone": "+905321234567", "phoneDisplay": "0532 123 45 67", "name": "Ayşe Yılmaz", "email": "ayse@ornek.com", "status": "customer", "source": "phone", "tags": ["düzenli"], "memberNo": null, "memberPlan": null, "createdAt": "2026-10-30T09:14:05.000Z", "updatedAt": "2026-10-30T09:16:12.000Z" }}status: lead (potansiyel), customer, member (üye). source: kaydın nereden geldiği (phone, panel, api, import). Müşteri notları ve özel alanlar webhook’ta gönderilmez; gerekirse API’den oku.
message.sent, message.failed, message.received
{ "message": { "id": "6733d5e6f7a8b9c0d1e2f3a5", "channel": "sms", "direction": "out", "kind": "appointment_confirm", "status": "sent", "phone": "+905321234567", "email": null, "error": null, "appointmentId": "6731…", "orderId": null, "customerId": "6720…", "attempts": 1, "createdAt": "2026-10-30T09:16:12.000Z", "sentAt": "2026-10-30T09:16:14.000Z" }}| Alan | Açıklama |
|---|---|
channel | sms, whatsapp, email |
direction | out (gönderilen) ya da in (müşteriden gelen) |
kind | appointment_confirm, appointment_reminder, appointment_cancel, order_confirm, order_status, info, auto_reply… |
status | sent, delivered, simulated (test ortamı; gerçek gönderim yok), failed, received |
error | Yalnızca message.failed olayında doludur |
Mesaj gövdesi kişisel veridir ve webhook’ta hiç gönderilmez; yalnızca üst veri gider.
call_list.contact.status_changed
Arama listesindeki bir kişi aranmaya başlayınca, arama sonuçlanınca ya da aranmaktan vazgeçince gelir. Bir kişi için birden çok olay gelir (calling, sonra sonuç).
{ "contact": { "id": "673a0c1d2e3f4a5b6c7d8e9f", "targetId": "673a0c1d2e3f4a5b6c7d8e9f", "externalId": "crm-1042", "phone": "+905321110001", "phoneDisplay": "0532 111 00 01", "name": "Ayşe Yılmaz", "company": "Yılmaz Ltd", "status": "answered", "outcome": "Görüşüldü", "outcomeCode": "answered", "summary": "Müşteri fiyat bilgisi istedi, çarşamba tekrar aranacak.", "attempts": 1, "maxAttempts": 2, "nextAttemptAt": null, "lastAttemptAt": "2026-10-30T10:02:11.000Z", "finishedAt": "2026-10-30T10:04:30.000Z", "callId": "673a0c1d2e3f4a5b6c7d8ea0" }, "list": { "id": "672f1a2b3c4d5e6f7a8b9c0d", "name": "Sıcak leadler" }}status:calling,answered,meeting,no_answer,skipped,opted_out,failed. Görüşmenin ayrıntısı içincallIdileGET /calls/:idçağır ya dacall.completedolayını bekle.summaryarama sonuçlandığında dolar; kişicallingiken ya da özet henüz yoksanulldır.outcomeTürkçe serbest metindir ve değişebilir; kodastatusveoutcomeCodeile bağlan.- Yeniden deneme: açılmayan kişi
pending’e dönmez. Deneme hakkı varkenno_answerkalır venextAttemptAtbir sonraki deneme zamanını gösterir; hak bitinceskippedolur.
call_list.status_changed
{ "list": { "id": "672f1a2b3c4d5e6f7a8b9c0d", "name": "Sıcak leadler", "kind": "list", "status": "done", "previousStatus": "running", "counts": { "answered": 31, "meeting": 6, "no_answer": 2, "skipped": 4, "opted_out": 1 }, "updatedAt": "2026-10-30T14:40:02.000Z" }}status: ready, running, paused, done. kind: list (panel/API listesi) ya da queue (otomatik arama kuyruğu). counts, listedeki kişilerin duruma göre sayısıdır (olmayan durum yazılmaz).
do_not_call.added, do_not_call.removed
{ "entry": { "phone": "+905321110001", "phoneDisplay": "0532 111 00 01", "reason": "Görüşmede istedi", "source": "assistant", "createdAt": "2026-10-30T09:16:12.000Z" }}source: assistant (asistan ekledi) ya da panel. do_not_call.removed olayında reason, source ve createdAt nulldır; yalnızca phone anlamlıdır. API ile yapılan ekleme ve silmeler için olay gelmez. Numara eklenince bekleyen liste kişileri opted_out olur.
webhook.test
{ "message": "Bu bir deneme olayıdır. Webhook adresin çalışıyor.", "webhookId": "6740b1c2d3e4f5a6b7c8d9e0" }Olay geçmişi ve kaçan olaylar
Adresin bir süre kapalı kaldıysa ya da bir olayı yanlışlıkla işlemediysen, Bağla son 7 günün olaylarını saklar.
Kaçan olayları bul. Teslimat geçmişi hangi olayların ulaşmadığını gösterir:
curl "https://app.bagla.ai/api/v1/webhooks/WEBHOOK_ID/deliveries?status=failed" \ -H "Authorization: Bearer bgl_..."{ "items": [ { "id": "6741…", "webhookId": "6740…", "eventId": "6740c1d2e3f4a5b6c7d8e9f0", "eventType": "call.completed", "status": "failed", "attempts": 8, "nextAttemptAt": null, "lastAttemptAt": "2026-11-01T07:14:45.000Z", "deliveredAt": null, "responseCode": 503, "responseBody": "Service Unavailable", "error": "Beklenen 2xx, gelen 503", "durationMs": 212, "createdAt": "2026-10-30T09:16:31.000Z" } ], "next": null}Yeniden gönder. Adresini düzelttikten sonra:
# Tek teslimatı yeniden denecurl -X POST https://app.bagla.ai/api/v1/webhook-deliveries/TESLIMAT_ID/retry \ -H "Authorization: Bearer bgl_..."# Bir olayı (aboneliği olmasa bile) başka bir webhook’a göndercurl -X POST https://app.bagla.ai/api/v1/webhook-events/OLAY_ID/resend \ -H "Authorization: Bearer bgl_..." -H "Content-Type: application/json" \ -d '{ "webhookId": "6742…" }'Zaten başarıyla teslim edilmiş bir olay aynı webhook’a yeniden gönderilmez (409).
Olayları listele. GET /webhook-events?type=call.completed&limit=50 son olayları (en yeni önce) verir; yanıttaki next değerini before olarak vererek sayfalarsın. Her öğe gönderilen zarfın aynısıdır. Bir kesinti sonrası boşluğu kapatmak için sayfaları, işlediğin son olayın id değerine rastlayana kadar gez.
Bu uç polling için değil, telafi içindir: günlük işleyişi webhook taşır, geçmiş yalnızca kesintiyi kapatır.
Sorun giderme
| Belirti | Olası neden ve çözüm |
|---|---|
Webhook eklerken 400 url https:// ile başlamalı | Yalnızca https desteklenir. Geliştirme için ngrok / Cloudflare Tunnel gibi bir https tüneli kullan |
400 url herkese açık bir adres olmalı | localhost, özel ağ ve iç adresler reddedilir. Adres internetten erişilebilir olmalı |
Deneme olayında Beklenen 2xx, gelen 401/403 | Güvenlik duvarı ya da kimlik doğrulama Bağla’yı engelliyor. Bu adres için imza dışında kimlik doğrulama isteme |
| Zaman aşımı (10 sn içinde yanıt gelmedi) | Alıcı çok yavaş. Olayı kuyruğa yazıp hemen 200 dön, işi sonra yap |
| Adres herkese açık bir IP’ye çözümlenmedi | Alan adı özel bir IP’ye işaret ediyor. DNS kaydını herkese açık IP’ye çevir |
| TLS ya da sertifika hatası | Sertifika geçerli, süresi dolmamış ve zinciri tam olmalı. Kendi imzaladığın sertifika kabul edilmez |
| İmza hep tutmuyor | Ham gövdeyi değil çözülüp yeniden yazılmış JSON’u imzalıyorsun; ya da yanlış (eski) whsec_ anahtarı. Anahtar yenilendiyse yenisini kullan |
| Webhook kendiliğinden kapandı | Art arda 5 olay teslim edilemedi. Adresi düzelt, active: true yap, kaçanları geçmişten yeniden gönder |
call.completed geç geliyor | Görüşme, ses altyapısı kaydı işleyince tamamlanır (genellikle 1–3 dakika). Daha uzun sürerse destek merkezinden yaz |
| Hiç olay gelmiyor | Webhook açık mı, olay seçili mi (events), geliştirici ayarları açık mı? Deneme olayı gelip gerçek olay gelmiyorsa abonelikleri kontrol et |
| Aynı olay iki kez geldi | Beklenen davranış (en az bir kez). id ile tekilleştir |
Referans
Hatalar
Hata yanıtı her zaman { "error": "...", "code": "..." } biçimindedir; error Türkçe, kullanıcıya gösterilebilir bir cümledir, code makine için sabit bir anahtardır. Doğrulama hatalarında details alan bazında açıklama içerir.
{ "error": "İstek doğrulanamadı.", "code": "validation", "details": [{ "path": "schedule.0.start", "message": "Saat SS:DD biçiminde olmalı" }] }| Kod | Ne zaman |
|---|---|
400 | Gövde boş ya da geçersiz; doğrulama hatası (geçersiz saat, çakışan aralık, geçersiz saat dilimi, geçersiz telefon); kuyrukta 500, toplu uçta 5.000 kayıttan fazla; liste ya da kuyruk için asistan ya da hat yok |
401 | Anahtar geçersiz ya da iptal edilmiş |
402 | Plan giden aramayı kapsamıyor |
403 | Geliştirici ayarları kapalı ya da giden arama onaylı değil |
404 | Liste, kişi, numara, görüşme ya da kuyruk kişisi bulunamadı (başka işletmenin kaydı da 404 döner) |
409 | Aynı ad ya da telefon zaten var; en fazla 10 webhook; zaten teslim edilmiş olay yeniden gönderilemez; çalışan liste silinemez; kişi şu an aranıyor ya da aranmış; otomatik arama ayarlanmamış |
413 | Gövde izin verilen boyutu aşıyor |
422 | Aynı Idempotency-Key farklı bir gövdeyle kullanılmış |
429 | Dakikada 120 istek aşıldı; Retry-After kadar bekle |
Ücretlendirme
Giden aramalar da diğer görüşmeler gibi dakika başına ücretlendirilir; sabit ücret yoktur. İşletmenin aylık tavanı dolunca yeni arama başlatılmaz ve kuyruk bekler. Ayrıntılar panelde Faturalar & Ödeme sayfasındadır; API çağrılarının kendisi ücretsizdir.
Yasal uyum
Aramalar yalnızca işletmeler (B2B) ya da aranma izni alınmış kişiler için yapılabilir; hangisi olduğunu liste oluştururken (consentBasis) ya da panelde seçersin. Kişi görüşmede “beni aramayın” derse asistan opted_out olarak işaretler ve numarayı aranmayacaklar listesine ekler; aynı numara sonraki gönderimlerde de aranmaz. Bu listeyi API ile okuyabilir ve yazabilirsin.
- Kişi verisini yalnızca aranma dayanağın olan kişiler için gönder;
fieldsiçine gerekmeyen kişisel veri koyma. - Görüşme dökümleri ve ses kayıtları işletmenin KVKK saklama süresine tabidir; indirdiğin kopyadan sen sorumlusun.
- Her API anahtarı işlemi (kayıt indirme dahil) denetim kaydına yazılır; panelde KVKK ve gizlilik → İşlem kayıtları.
İlgili bağlantılar
Aydınlatma metni ve işletmenin kamuya açık KVKK sayfası: https://bagla.ai/kvkk/<işletme-kodu>.
Örnekler
Örnekler
curl
export BAGLA_KEY="bgl_..."BASE="https://app.bagla.ai/api/v1"# Kişi eklecurl -X POST "$BASE/outbound/contacts" \ -H "Authorization: Bearer $BAGLA_KEY" -H "Content-Type: application/json" \ -d '{"contacts":[{"phone":"05321234567","name":"Ahmet","company":"Yılmaz Ticaret","externalId":"crm-1","fields":{"paket":"Pro"}}]}'# Başlatcurl -X POST "$BASE/outbound/campaign/start" -H "Authorization: Bearer $BAGLA_KEY"# Sonuçlarcurl "$BASE/outbound/contacts?status=answered" -H "Authorization: Bearer $BAGLA_KEY"curl "$BASE/outbound/report?from=2026-10-01" -H "Authorization: Bearer $BAGLA_KEY"JavaScript (Node 18+)
const BASE = "https://app.bagla.ai/api/v1";const headers = { Authorization: `Bearer ${process.env.BAGLA_KEY}`, "Content-Type": "application/json" };async function bagla(path, init = {}) { const res = await fetch(BASE + path, { ...init, headers }); if (res.status === 429) { await new Promise((r) => setTimeout(r, Number(res.headers.get("Retry-After") ?? 5) * 1000)); return bagla(path, init); } const body = await res.json(); if (!res.ok) throw new Error(`${res.status}: ${body.error}`); return body;}// 500'erli gruplar hâlinde gönderasync function push(contacts) { for (let i = 0; i < contacts.length; i += 500) { const r = await bagla("/outbound/contacts", { method: "POST", body: JSON.stringify({ contacts: contacts.slice(i, i + 500) }) }); if (r.skipped.length) console.warn("Atlananlar:", r.skipped); }}Python
import os, requestsBASE = "https://app.bagla.ai/api/v1"S = requests.Session()S.headers["Authorization"] = "Bearer " + os.environ["BAGLA_KEY"]r = S.post(f"{BASE}/outbound/contacts", json={"contacts": [ {"phone": "05321234567", "name": "Ahmet", "externalId": "crm-1", "fields": {"paket": "Pro"}},]})r.raise_for_status()print(r.json()["added"], "kişi eklendi")S.post(f"{BASE}/outbound/campaign/start").raise_for_status()for k in S.get(f"{BASE}/outbound/contacts", params={"status": "meeting"}).json()["items"]: print(k["name"], k["outcome"])Arama listesi ve kişiler (JavaScript)
const base = "https://app.bagla.ai/api/v1";const headers = { Authorization: "Bearer " + process.env.BAGLA_KEY, "Content-Type": "application/json" };const call = async (method, path, body) => { const res = await fetch(base + path, { method, headers, body: body && JSON.stringify(body) }); const data = await res.json(); if (!res.ok) throw new Error(`${res.status}: ${data.error}`); return data;};const list = await call("POST", "/call-lists", { name: "Fiyat soranlar", consentBasis: "consent", purpose: "Fiyat teklifimizi anlatıp randevu önermek için arıyoruz.", schedule: [1, 2, 3, 4, 5].flatMap((d) => [ { weekday: d, start: "10:00", end: "12:30" }, { weekday: d, start: "14:00", end: "17:00" }, ]),});const report = await call("POST", `/call-lists/${list.id}/contacts`, { update: true, contacts: [{ phone: "0532 111 00 01", name: "Ayşe Yılmaz", fields: { paket: "Pro" } }],});console.log(report); // { created: 1, assigned: 1, failed: 0, ... }await call("POST", `/call-lists/${list.id}/activate`);Arama listesi ve kişiler (Python)
import os, requestsbase = "https://app.bagla.ai/api/v1"s = requests.Session()s.headers["Authorization"] = "Bearer " + os.environ["BAGLA_KEY"]def call(method, path, **kw): r = s.request(method, base + path, **kw) if not r.ok: raise RuntimeError(f"{r.status_code}: {r.json().get('error')}") return r.json()lst = call("POST", "/call-lists", json={ "name": "Fiyat soranlar", "consentBasis": "consent", "purpose": "Fiyat teklifimizi anlatıp randevu önermek için arıyoruz.", "schedule": [{"weekday": 1, "start": "10:00", "end": "11:45"}, {"weekday": 1, "start": "13:30", "end": "16:30"}],})with open("kisiler.csv", "rb") as f: report = call("POST", f"/call-lists/{lst['id']}/import", data=f.read(), headers={"Content-Type": "text/csv", "Idempotency-Key": "ilk-yukleme"})print(report["created"], report["failed"])call("POST", f"/call-lists/{lst['id']}/activate")Yapay zekâ ajanları için
Bir LLM ajanına (Claude, GPT, kendi ajanın) kuyruğu yönettirmek istiyorsan aşağıdaki iki aracı ver. Ajan arama yapmaz; kişiyi kuyruğa koyar ve sonucu okur. Aramayı Bağla asistanı yapar.
Araç tanımları
[ { "name": "bagla_kisi_ekle", "description": "Bağla asistanının arayacağı kişileri kuyruğa ekler. Arama hemen yapılmaz; asistan belirlenen saatlerde sırayla arar. Aynı externalId ile tekrar çağırmak güvenlidir: aranmamış kişinin bilgileri güncellenir.", "input_schema": { "type": "object", "properties": { "contacts": { "type": "array", "maxItems": 500, "items": { "type": "object", "properties": { "phone": { "type": "string", "description": "Telefon numarası, örn. 05321234567" }, "name": { "type": "string" }, "company": { "type": "string" }, "externalId": { "type": "string", "description": "Kendi sistemindeki kişi kimliği; tekrar gönderimde çift kayıt oluşmasını önler" }, "note": { "type": "string" }, "fields": { "type": "object", "description": "Açılış cümlesinde %anahtar% olarak kullanılacak serbest alanlar; değerler metin ya da sayı" } }, "required": [ "phone" ] } } }, "required": [ "contacts" ] } }, { "name": "bagla_sonuc_oku", "description": "Kuyruktaki kişilerin arama sonuçlarını getirir. status ile süz: pending, calling, answered, meeting, no_answer, opted_out, failed, skipped.", "input_schema": { "type": "object", "properties": { "status": { "type": "string" }, "externalId": { "type": "string" }, "limit": { "type": "integer", "minimum": 1, "maximum": 500 } } } }]Araçları şu istekler karşılar: bagla_kisi_ekle → POST /outbound/contacts, bagla_sonuc_oku → GET /outbound/contacts. Kuyruğu başlatıp durdurmayı (/outbound/campaign/start, /pause) ajana vermek istemiyorsan araç olarak tanımlama; panelden yönet.
Ajan için kurallar
Ajanın sistem istemine şunları ekle:
- Kişiyi göndermeden önce numaranın bir insan ya da işletme numarası olduğundan ve aranma dayanağından emin ol. Dayanağı olmayan kişiyi ekleme.
- Her kişiye
externalIdver. Aynı kişiyi tekrar göndermek güvenlidir; çift arama olmaz. fieldsiçine yalnızca açılış cümlesinde ya da görüşmede işe yarayacak bilgiyi koy. Kişisel veri gerektirmeyen alanları ekleme.skippedboş değilse nedenini kullanıcıya bildir; aynı numarayı yeniden gönderip durma.429gelirseRetry-Afterkadar bekle. Sonuçları saniyede bir sorgulama; görüşmeler dakikalar sürer. Birkaç dakikada birGET /outbound/contacts?status=...yeterlidir.opted_outolan kişiyi bir daha gönderme.402,403,409hataları ajanın çözebileceği şeyler değildir (plan, onay, ayar). Hatayı olduğu gibi kullanıcıya ilet.
Önerilen akış
- İsteğe bağlı:
GET /outbound/campaignile kuyruğun hazır olduğunu ve durumunu kontrol et. - Kişileri 500’erli gruplar hâlinde
POST /outbound/contactsile gönder. - Kullanıcı onayıyla (ya da panelden) kuyruğu başlat.
- Birkaç dakikada bir
GET /outbound/contacts?status=meetingvestatus=answeredile sonuçları topla.outcomealanı görüşmenin tek cümlelik sonucudur; ayrıntı içinGET /outbound/contacts/:idile özet ve dökümü al. - Günün sonunda
GET /outbound/reportile özet çıkar.
Takıldığın bir yer mi var? Panelde Destek merkezinden yaz ya da app.bagla.ai üzerinden bize ulaş.