Bağla API

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ı.

Temel adreshttps://app.bagla.ai/api/v1
OpenAPI tanımı (JSON)

Baş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şilerOtomatik arama kuyruğu
Ne içinBirden çok liste, her listeye ayrı amaç, çalışma saati ve numaraTek bir kuyruğa kişi basıp sırayla aratmak
KişiTelefon numarası bir kişidir; aynı kişi birden çok listede olabilirKuyruğa özel
Çalışma saatleriGün başına birden çok aralık, saat dilimi, etkin tarih aralığıTek aralık ve günler
NumaraHavuzdan 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.

GerekenEksikse
Geliştirici ayarları açık403 Bu işletmede geliştirici ayarları kapalı
Planda giden arama402 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.

HTTP
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 429 ve Retry-After (saniye) başlığı gelir; X-RateLimit-Remaining kalan 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).

Terminal
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.

Terminal
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.

Terminal
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.

Terminal
curl -X POST https://app.bagla.ai/api/v1/call-lists/LISTE_ID/activate \  -H "Authorization: Bearer bgl_..."

5. Sonuçları oku.

Terminal
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.

YolNe yapar
GET/meAnahtar ve işletme bilgisi
GET/call-lists/metaSı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.

YolNe yapar
GET/call-lists/metaSaat sınırları, durumlar, toplu işlem sınırları
GET/call-listsListeleri sayfalı döner
POST/call-listsListe oluşturur (201)
GET/call-lists/:idListe detayı: ayarlar, sayılar, numaralar, durum
PUT/call-lists/:idVerilen alanları günceller; gönderilmeyenler korunur
DELETE/call-lists/:idListeyi ve üyeliklerini siler
POST/call-lists/:id/activateListeyi aktif yapar
POST/call-lists/:id/deactivateListeyi pasif yapar

Liste oluşturma

POST https://app.bagla.ai/api/v1/call-lists
{  "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}
AlanAçıklama
nameZorunlu. İşletmede benzersiz (aynı ad 409)
purposeZorunlu, en az 10 karakter. Asistanın görüşmede ne anlatacağı; %ad%, %firma% gibi yer tutucular kullanılabilir
consentBasisZorunlu. b2b işletmelere yönelik arama, consent aranma izni alınmış kişiler
greetingAçılış cümlesi şablonu; boşsa işletmenin giden arama selamlaması
schedule, timezone, activeFrom, activeUntilÇalışma saatleri bölümünde
numberIdsNumaralar bölümünde
maxAttemptsAçılmazsa en fazla kaç kez aranır (1–5, varsayılan 2)
retryMinutesYeniden deneme aralığı (5–10080 dk, varsayılan 120)
concurrencyAynı anda aranan kişi sayısı (1–10)
contactIds, contactsOluştururken listeye eklenecek mevcut ya da yeni kişiler

Liste oluşturma yanıtı ve GET /call-lists/:id aynı biçimdedir:

JSON
{  "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

statusAnlamı
readyHazır: henüz başlatılmadı
runningAktif. Çalışma aralıklarında kişileri arar
pausedPasif. Hiç aranmaz; kalan kişiler bekler
doneAranacak 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

HTTP
GET https://app.bagla.ai/api/v1/call-lists?status=active&q=fiyat&page=1&pageSize=25

status: 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.

PUT https://app.bagla.ai/api/v1/call-lists/:id/schedule
{  "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:DD biç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:

JSON
{  "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.

YolNe yapar
GET/call-numbersHavuzdaki numaralar
PUT/call-numbers/:idNumarayı etkinleştirir ya da devre dışı bırakır ({ "active": false }), etiketi değiştirir
GET/call-lists/:id/numbersListeye atanmış numaralar
PUT/call-lists/:id/numbersNumaraları değiştirir ({ "numberIds": [...] })
GET https://app.bagla.ai/api/v1/call-numbers
[  { "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ıtlardaki number E.164 (+90…), numberDisplay Tü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.blockedReason alanı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.

YolNe yapar
GET/do-not-callListe (en yeni önce). Süzgeç: q (numara ya da neden), source, since, page, pageSize
POST/do-not-callNumara ekler. Tek: { "phone": "...", "reason": "..." } · toplu: { "phones": [...], "reason": "..." } ya da { "numbers": [{ "phone", "reason" }] } (en fazla 5.000)
POST/do-not-call/checkNumaralardan hangileri listede? { "phones": [...] } (en fazla 1.000)
GET/do-not-call/:phoneTek numara: listede mi, kaydı nedir
DELETE/do-not-call/:phoneNumarayı listeden çıkarır; listede değilse 404
Terminal
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" }'
JSON
{ "phone": "+905321110001", "added": true, "affectedContacts": 2 }
  • phone hangi 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ıtlarda phoneDisplay okunur biçimi verir.
  • added: false numaranın zaten listede olduğunu gösterir (200); yeni eklemede 201 döner. İşlem tekrarlanabilir, zararsızdır.
  • affectedContacts, numaranın bekleyen liste kişilerinden kaç tanesinin opted_out yapı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, invalid ve geçersiz satırların errors listesi. Geçersiz bir satır diğerlerini engellemez.
  • Listeden çıkarılan numara tekrar aranabilir hâle gelir; daha önce opted_out yapılmış liste kişileri olduğu gibi kalır.
AlanAçıklama
reasonEklenme nedeni (en fazla 200 karakter)
sourceassistant (görüşmede müşteri istediği için asistan ekledi), panel (elle), api
createdAtEklendiği an (ISO 8601, UTC). since süzgeci bu değere bakar
POST https://app.bagla.ai/api/v1/do-not-call/check
{ "phones": ["0532 111 00 01", "0532 000 00 00", "abc"] }
JSON
{  "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.

YolNe yapar
GET/contactsTüm kişiler. Süzgeç: q, listId, notInList, sort (name, oldest, newest), page, pageSize
POST/contactsTek kişi ekler (201) ya da { "contacts": [...] } ile toplu ekleme
GET/contacts/:idKişi ve üye olduğu listeler
PUT/contacts/:idKişiyi günceller; aranmamış tüm üyeliklere yansır
DELETE/contacts/:idKişiyi ve tüm üyeliklerini siler
GET/call-lists/:id/contactsListe üyeleri ve arama durumları. Süzgeç: status, q, updatedSince, page, pageSize
POST/call-lists/:id/contactsListeye 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/:contactIdTek kişiyi listeden çıkarır
POST https://app.bagla.ai/api/v1/contacts
{  "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"]}
AlanAçıklama
phoneZorunlu. 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)
fieldsSerbest alanlar (en fazla 20); açılış cümlesinde ve amaçta %sehir% biçiminde kullanılır
externalIdKendi sistemindeki kimlik (en fazla 100 karakter)
listIdsKişinin ekleneceği listeler (zorunlu)

Kurallar

  • Aynı telefon seçilen listelerin hepsinde zaten kayıtlıysa 409 döner. Var olanı güncellemek için PUT /contacts/:id ya da toplu uçta update: true kullan.
  • 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; externalId ve 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ı

statusAnlamı
pendingSırada
callingŞu an aranıyor
answeredGörüşüldü
meetingGörüşmede randevu ya da görüşme ayarlandı
no_answerAçılmadı; deneme hakkı varsa yeniden denenir
skippedUlaşılamadı, deneme hakkı bitti
failedArama 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.

JSON
{  "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}
AlanAçıklama
id, targetIdKişi/üyelik kimliği; contactIds alanlarında bunu kullanırsın. call_list.contact.status_changed olayındaki targetId ile aynı
status, outcome, outcomeCodeListedeki arama durumu; outcome Türkçe serbest metin, outcomeCode sabit kod (Kişi durumları)
attempts, lastAttemptAt, nextAttemptAt, finishedAtDeneme sayısı ve zamanlar; nextAttemptAt yalnızca yeniden denenecek kişide doludur
callIdSon aramanın görüşme kimliği (GET /calls/:id); henüz aranmadıysa null
updatedAtKiş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.

Terminal
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

POST https://app.bagla.ai/api/v1/contacts/assign
{ "contactIds": ["673a…", "673b…"], "listIds": ["672f…", "6730…"] }
POST https://app.bagla.ai/api/v1/contacts/unassign
{ "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.

YolNe yapar
POST/contacts{ "contacts": [...] } ile toplu ekleme. update ve listIds desteklenir
PATCH/contacts/bulkToplu güncelleme
POST/contacts/bulk-delete{ "ids": [...] } toplu silme
POST/contacts/importCSV içe aktarma (listIds ile)
POST/call-lists/:id/importCSV’yi içe aktarır ve doğrudan bu listeye ekler

Toplu ekleme

POST https://app.bagla.ai/api/v1/contacts
{  "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 ve existing olarak 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.

PATCH https://app.bagla.ai/api/v1/contacts/bulk
{  "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.

Terminal
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, phonephone
ad, isim, ad soyad, musteri, namename
firma, sirket, isletme, unvan, companycompany
not, aciklama, notenote
id, kimlik, musteri no, referans, external idexternalId
diğer sütunlarserbest 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

JSON
{  "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 }]}
AlanAnlamı
createdYeni oluşan kişi
updatedGüncellenen kayıtlı kişi
existingKayıtlı olduğu için değiştirilmeyen kişi
assignedListelere eklenen üyelik
removedSilinen kişi (bulk-delete)
skippedAynı istekte tekrar eden numara ya da aranmayacaklardaki numara
failedDoğrulamadan geçemeyen satır
errorsHer 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:

JSON
{ "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:

Terminal
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.csv

Aynı 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.

  1. Panelde Geliştirici sayfasında geliştirici ayarlarını etkinleştirip bir API anahtarı oluşturmuş olmalısın.
  2. Arama ayarları sekmesinde açılış cümlesini, arama yönergesini, arama saatlerini, günleri ve aramalar arası beklemeyi ayarla.
  3. Sisteminden POST /outbound/contacts ile kişileri gönder. Her kişi kuyruğun sonuna eklenir.
  4. POST /outbound/campaign/start ile kuyruğu başlat. Asistan, arama saatleri içinde aynı anda en fazla concurrency (1–3) kişiyi arar; bir görüşme bitince gapSeconds kadar bekleyip boşalan hatta sıradakini alır.
  5. GET /outbound/contacts ve GET /outbound/report ile 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": [...] }.

JSON
{  "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" }    }  ]}
AlanZorunluSınır
phoneevetBoşluk ve tire temizlenir; 0532…, +90 532…, 90532… yazımları E.164’e çevrilir. Yurt dışı numaraları + ile yaz
namehayıren fazla 80 karakter
companyhayıren fazla 120 karakter
notehayıren fazla 300 karakter
externalIdhayıren fazla 100 karakter; kendi sistemindeki kimlik
fieldshayıren 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:

JSON
{  "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_out olarak girer, aranmaz ve skipped içinde görünür.
  • Geçersiz numaralar skipped içinde nedeniyle birlikte döner; diğer kişiler yine eklenir.
  • status kuyruğun durumudur (running ya da paused). 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.

Metin
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 fields anahtarı 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

YolNe yapar
GET/outbound/campaignAyarları, durumu ve kişi sayılarını verir
POST/outbound/campaign/startKuyruğu başlatır
POST/outbound/campaign/pauseKuyruğ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ı:

JSON
{  "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 pending kalır.
  • maxAttempts (1–3): bir kişiye en fazla kaç kez ulaşılmaya çalışılır. Cevap yoksa retryMinutes (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.

Terminal
curl -X POST "https://app.bagla.ai/api/v1/outbound/contacts" \  -H "Authorization: Bearer $BAGLA_KEY" -H "Content-Type: text/csv" \  --data-binary @kisiler.csv
CSV
telefon;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 skipped iç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.

DurumAnlamı
pendingSırada, henüz aranmadı
callingŞu an aranıyor
answeredTelefon açıldı, görüşme yapıldı
meetingGörüşme sonunda randevu / toplantı alındı
no_answerAçılmadı ya da çok kısa sürdü; deneme hakkı varsa yeniden denenecek
opted_outAranmak istemedi (görüşmede ya da listede); bir daha aranmaz
failedArama başlatılamadı
skippedDeneme 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.

outcomeCodeNe zamanstatus
callingŞu an aranıyorcalling
answeredTelefon açıldı, görüşüldüanswered
meetingGörüşmede randevu / toplantı alındımeeting
no_answerAçılmadı ya da çok kısa sürdüno_answer
unreachableUlaşılamadı (operatör hatası: kapalı, meşgul vb.)no_answer
retries_exhaustedDeneme hakkı bittiskipped
automatedKarşıdaki sesli menü ya da telesekreter; asistan kapattıskipped
opted_outAranmak istemediopted_out
start_failedArama başlatılamadıfailed

Kişi nesnesi:

JSON
{  "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

YolNe yapar
GET/outbound/contactsKişi listesi. Süzgeçler: status, externalId, limit (1–500, varsayılan 100), offset
GET/outbound/contacts/:idTek kişi, görüşme özeti ve dökümüyle
DELETE/outbound/contacts/:idKiş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/reportKuyruk raporu (from, to; varsayılan son 30 gün)

Rapor yanıtı:

JSON
{  "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:

YolNe yapar
GET/callsGö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/recordingSes kaydı (Ses kayıtları)
GET/reports/summaryToplam, 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.

Terminal
# 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"
DurumYanıt
Kayıt var200, ses dosyası (audio/wav ya da audio/ogg)
Görüşme senin işletmenin değil ya da yok404
Kayıt tutulmamış ya da saklama süresi dolup silinmiş404
Kayıt henüz işlenmedi404 (“Kayıt henüz hazır değil”); call.recording_ready olayını bekle
Dakikada 20’den fazla indirme429 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 gelirSorduğun andaOlay olduğu an
İstek sayısıÇoğu boş dönerYalnızca olay olunca
Kaçan olay riskiAralığı yanlış seçersenYeniden deneme ve olay geçmişi korur
SınırDakikada 120 istekYok
Metin
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 denenir

Webhook’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.

Terminal
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"]  }'
JSON
{  "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.

Terminal
curl -X POST https://app.bagla.ai/api/v1/webhooks/WEBHOOK_ID/test \  -H "Authorization: Bearer bgl_..."
JSON
{ "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.

YolNe yapar
GET/webhooksWebhook’lar, olay kataloğu ve teslimat kuralları
POST/webhooksWebhook ekler (201). Yanıtta secret bir kez görünür
GET/webhooks/:idTek webhook
PATCH/webhooks/:idurl, description, events, active, includeTranscript alanlarından gönderdiğini günceller
DELETE/webhooks/:idWebhook’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/deliveriesTeslimat geçmişi: limit (en fazla 100), before, status (pending, retrying, delivered, failed)
POST/webhook-deliveries/:id/retryBaşarısız ya da bekleyen teslimatı hemen yeniden dener
GET/webhook-eventsOlay geçmişi (son 7 gün): type, limit, before
GET/webhook-events/:idTek olay (gönderilen zarfın aynısı)
POST/webhook-events/:id/resendOlayı seçilen webhook’a yeniden gönderir: { "webhookId": "..." }
AlanAçıklama
urlZorunlu. 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
eventsAbone 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
activefalse yapınca olay gönderilmez; yeniden true yapınca hata sayaçları sıfırlanır
includeTranscriptVarsayı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:

Terminal
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": true bulunur. Örnek olayları CRM’ine kaydetme: bu alana bakıp atla.
  • Webhook o türe abone olmasa da gönderilir. includeTranscript: false ise örnek call.completed’de de döküm olmaz.
  • Örnek olaylar olay geçmişinde listelenmez; eventType verilmezse webhook.test gider.
  • 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.

JSON
{  "id": "6740c1d2e3f4a5b6c7d8e9f0",  "type": "call.completed",  "apiVersion": "1",  "createdAt": "2026-10-30T09:16:31.402Z",  "businessId": "6701a2b3c4d5e6f7a8b9c0d1",  "data": { }}
AlanAçıklama
idOlayın kimliği. Aynı olay yeniden denemelerde aynı id ile gelir; tekilleştirmek için bunu kullan
typeOlay türü, örn. call.completed
apiVersionZarf 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
createdAtOlayın oluştuğu an (ISO 8601, UTC)
businessIdOlayın ait olduğu işletme
sampleYalnızca örnek olaylarda bulunur ve truedur. Gerçek olaylarda bu alan yoktur
dataOlaya özgü veri; her olay için Olay türleri’nde

İstek başlıkları

BaşlıkDeğer
Content-Typeapplication/json
User-AgentBagla-Webhooks/1.0
Bagla-Event-IdZarftaki id ile aynı
Bagla-Event-TypeZarftaki type ile aynı. Gövdeyi okumadan yönlendirmek için kullan
Bagla-Delivery-IdBu teslimatın kimliği
Bagla-AttemptKaçıncı deneme: 1, 2, …
Bagla-Timestampİmzanın üretildiği an (Unix saniye)
Bagla-Signaturet=<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:

  1. Bagla-Signature başlığından t (Unix saniye) ve tüm v1 (imza) değerlerini al. Normalde bir v1 vardır; anahtar yenileme geçiş süresinde iki tane gelir (önce yenisi, sonra eskisi).
  2. İmzalanan metin: t + . + ham gövde (gelen baytlar, hiç değiştirmeden).
  3. Beklenen imza: bu metnin, webhook’un whsec_... anahtarıyla HMAC-SHA256 özeti, küçük harfli onaltılık.
  4. v1 değerlerinden biri beklenen imzayla tutuyorsa istek geçerlidir; karşılaştırmayı sabit zamanlı yap.
  5. t değ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)

JavaScript
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)

Python
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 "", 200

Bağ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.

DenemeBir önceki başarısız denemeden sonra bekleme
1hemen
21 dakika
35 dakika
415 dakika
51 saat
63 saat
76 saat
812 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 200 dön; işi sonra yap. Yavaş yanıt zaman aşımına, tekrara ve sonunda kapanmaya yol açar.
  • Tekilleştir. İşlediğin id değerlerini sakla. Aynı id tekrar gelirse 200 dö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 createdAt ya 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 200 dö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-secret ile 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ürNe zaman
call.completedBir görüşme bitti (gelen/giden; açılmayan ve başarısız aramalar dahil)
call.recording_readyGörüşmenin ses kaydı hazır oldu; kayıt olaydaki yoldan API anahtarınla indirilir (Ses kayıtları)
appointment.createdYeni bir randevu oluşturuldu
appointment.cancelledRandevu iptal edildi (panelden, müşterinin mesajıyla ya da asistan tarafından)
appointment.confirmedMüşteri randevusunu onayladı
order.createdAsistan yeni bir sipariş aldı
order.status_changedSiparişin durumu değişti
customer.createdYeni müşteri ya da potansiyel müşteri kaydı açıldı
customer.updatedMüşteri kaydı güncellendi (durum, ad, e-posta, etiket…)
message.sentSMS, WhatsApp ya da e-posta gönderildi
message.failedBir mesaj gönderilemedi
message.receivedMüşteriden SMS ya da WhatsApp yanıtı geldi
call_list.contact.status_changedArama listesindeki bir kişinin durumu değişti
call_list.status_changedBir arama listesi (ya da kuyruk) başladı, durdu ya da tamamlandı
do_not_call.addedNumara aranmayacaklar listesine eklendi (panelden ya da asistan tarafından)
do_not_call.removedNumara aranmayacaklar listesinden çıkarıldı (panelden)
webhook.testDeneme 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.

JSON
{  "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 }    ]  }}
AlanAçıklama
idGörüşme kimliği (GET /calls/:id ile aynı)
directioninbound (gelen) ya da outbound (giden)
durationSecs, answeredKonuşma süresi ve açılıp açılmadığı
resultserved, greeted_only, no_answer, busy, voicemail, escalated, system_fault
campaignGö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
transcriptKonuş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.

JSON
{  "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"  }}
AlanAçıklama
codeMüşterinin bildiği randevu kodu (işletme içinde benzersiz)
statuspending, confirmed, cancelled, completed, no_show
staffPersonel atanmadıysa null
notifyChannelMüşterinin bilgilendirme tercihi: sms, whatsapp, email ya da null
customerConfirmedMüşteri mesajla onayladıysa true
callIdRandevunun alındığı görüşme; panelden açıldıysa null

order.created, order.status_changed

JSON
{  "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

JSON
{  "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

JSON
{  "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"  }}
AlanAçıklama
channelsms, whatsapp, email
directionout (gönderilen) ya da in (müşteriden gelen)
kindappointment_confirm, appointment_reminder, appointment_cancel, order_confirm, order_status, info, auto_reply…
statussent, delivered, simulated (test ortamı; gerçek gönderim yok), failed, received
errorYalnı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ç).

JSON
{  "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çin callId ile GET /calls/:id çağır ya da call.completed olayını bekle.
  • summary arama sonuçlandığında dolar; kişi calling iken ya da özet henüz yoksa nulldır.
  • outcome Türkçe serbest metindir ve değişebilir; koda status ve outcomeCode ile bağlan.
  • Yeniden deneme: açılmayan kişi pending’e dönmez. Deneme hakkı varken no_answer kalır ve nextAttemptAt bir sonraki deneme zamanını gösterir; hak bitince skipped olur.

call_list.status_changed

JSON
{  "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

JSON
{  "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

JSON
{ "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:

Terminal
curl "https://app.bagla.ai/api/v1/webhooks/WEBHOOK_ID/deliveries?status=failed" \  -H "Authorization: Bearer bgl_..."
JSON
{  "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:

Terminal
# 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

BelirtiOlası 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/403Gü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ümlenmediAlan 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 tutmuyorHam 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ç geliyorGörüşme, ses altyapısı kaydı işleyince tamamlanır (genellikle 1–3 dakika). Daha uzun sürerse destek merkezinden yaz
Hiç olay gelmiyorWebhook 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 geldiBeklenen 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.

JSON
{ "error": "İstek doğrulanamadı.", "code": "validation", "details": [{ "path": "schedule.0.start", "message": "Saat SS:DD biçiminde olmalı" }] }
KodNe zaman
400Gö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
401Anahtar geçersiz ya da iptal edilmiş
402Plan giden aramayı kapsamıyor
403Geliştirici ayarları kapalı ya da giden arama onaylı değil
404Liste, kişi, numara, görüşme ya da kuyruk kişisi bulunamadı (başka işletmenin kaydı da 404 döner)
409Aynı 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ış
413Gövde izin verilen boyutu aşıyor
422Aynı Idempotency-Key farklı bir gövdeyle kullanılmış
429Dakikada 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; fields iç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

Terminal
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+)

JavaScript
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

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)

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)

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ı

JSON
[  {    "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 externalId ver. Aynı kişiyi tekrar göndermek güvenlidir; çift arama olmaz.
  • fields içine yalnızca açılış cümlesinde ya da görüşmede işe yarayacak bilgiyi koy. Kişisel veri gerektirmeyen alanları ekleme.
  • skipped boş değilse nedenini kullanıcıya bildir; aynı numarayı yeniden gönderip durma.
  • 429 gelirse Retry-After kadar bekle. Sonuçları saniyede bir sorgulama; görüşmeler dakikalar sürer. Birkaç dakikada bir GET /outbound/contacts?status=... yeterlidir.
  • opted_out olan kişiyi bir daha gönderme.
  • 402, 403, 409 hataları ajanın çözebileceği şeyler değildir (plan, onay, ayar). Hatayı olduğu gibi kullanıcıya ilet.

Önerilen akış

  1. İsteğe bağlı: GET /outbound/campaign ile kuyruğun hazır olduğunu ve durumunu kontrol et.
  2. Kişileri 500’erli gruplar hâlinde POST /outbound/contacts ile gönder.
  3. Kullanıcı onayıyla (ya da panelden) kuyruğu başlat.
  4. Birkaç dakikada bir GET /outbound/contacts?status=meeting ve status=answered ile sonuçları topla. outcome alanı görüşmenin tek cümlelik sonucudur; ayrıntı için GET /outbound/contacts/:id ile özet ve dökümü al.
  5. Günün sonunda GET /outbound/report ile özet çıkar.

Takıldığın bir yer mi var? Panelde Destek merkezinden yaz ya da app.bagla.ai üzerinden bize ulaş.