ZesleeAPI dokümantasyonu
Giriş yapHesap oluştur

Başlangıç

Genel bakış
Başlamadan önce
Kimlik doğrulama
Hızlı başlangıç

Arama listeleri

Arama listeleri
Çalışma saatleri
Numaralar
Aranmayacaklar listesi

Kişiler

Kişiler
Toplu işlemler ve içe aktarma
Sayfalama ve tekrarlanan istekler

Otomatik arama kuyruğu

Kuyruk nasıl çalışır
Kişi gönderme
Açılış cümlesi ve şablonlar
Kuyruğu yönetme
CSV ile ve panel listelerine aktarma
Kişi durumları
Okuma ve silme

Webhook'lar

Ses kayıtları
OpenAPI tanımı
Webhook'lara giriş
Webhook hızlı başlangıç
Webhook yönetimi
Olay zarfı
İmza doğrulama
Teslimat ve yeniden denemeler
Olay türleri
Olay geçmişi ve kaçan olaylar
Sorun giderme

Referans

Hatalar
Ücretlendirme
Yasal uyum

Örnekler

Örnekler
Yapay zekâ ajanları için

Zeslee API

Zeslee asistanını kendi sisteminden yönet: arama listeleri, alıcılar (kişiler), çalışma saatleri, hat numaraları ve otomatik arama kuyruğu. Asistan aramaları senin belirlediğin saatlerde yapar; sonuçları API'den okursun ya da **webhook'larla** olduğu an kendi sistemine alırsın (görüşme raporları, randevular, siparişler, mesajlar).

Temel adreshttps://api.zeslee.com/api/v1
Hızlı başlangıçBeş istekte liste kur, kişileri ekle, aramayı başlat.Arama listeleriKategori, çalışma saatleri ve numaralarla bağımsız listeler.Kişiler ve içe aktarmaAlıcı havuzu, toplu işlemler ve CSV yükleme.

Genel bakış#

Zeslee API'si iki bölümden oluşur. İkisi de aynı anahtarı ve aynı temel adresi kullanır.

Arama listeleri ve kişilerOtomatik arama kuyruğu
Ne içinBirden çok liste, alıcı havuzu, kategori, liste bazlı saat ve numaraTek bir kuyruğa kişi basıp sırayla aratmak
KişiListelerden bağımsız; bir kişi birden çok listede olurKuyruğa özel
Çalışma saatleriGün başına birden çok aralık, saat dilimi, etkin tarihTek aralık ve günler
NumaraHavuzdan seçim, round-robinİşletmenin numarası
BaşlangıçHızlı başlangıçKuyruk nasıl çalışır

Görüşme raporlarını, randevuları ve diğer olayları sürekli sorgulamak yerine [webhook'lara](#webhook-lara-giris) abone ol: olay olunca Zeslee sana imzalı bir istek gönderir.

Yeni bir arama entegrasyonu için arama listelerini öneririz: alıcı listelerini kategorilere ayırabilir, her listeye ayrı saat ve numara verebilir, tüm işlemleri toplu yapabilirsin.

Başlamadan önce#

Aramanın çalışması için şunlar gerekir. Biri eksikse API aşağıdaki hatayı döner.

GerekenEksikse
Geliştirici ayarları açık403 Bu işletmede geliştirici ayarları kapalı
Planda giden arama (Pro ya da Partner)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
Panelde otomatik arama ayarı (yalnızca otomatik arama kuyruğu için)409 Otomatik arama henüz ayarlanmadı

Panelde Geliştirici sayfasını aç, Geliştirici ayarlarını etkinleştir anahtarını aç ve bir API anahtarı oluştur.

Kimlik doğrulama#

Her isteğe API anahtarını ekle. Anahtar zsl_ ile başlar ve oluşturulurken yalnızca bir kez gösterilir.

Metin
Authorization: Bearer zsl_...

X-API-Key: zsl_... başlığı da kabul edilir. Anahtarı kaynak koduna yazma; ortam değişkeninde tut. Sızdığından şüphelenirsen panelden iptal et, yenisini oluştur.

  • Temel adres: https://zeslee.arcuslabs.co/api/v1
  • Sınır: anahtar başına dakikada 120 istek. Aşınca 429 ve Retry-After (saniye) başlığı gelir.
  • Geçersiz ya da iptal edilmiş anahtar: 401.
  • Anahtar raporları okuyabilir; arama listelerini, kişileri, 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.

Hızlı başlangıç#

Beş adımda bir liste kur, kişileri ekle ve aramaya başlat. zsl_... yerine kendi anahtarını yaz.

1. Hat numaralarını gör. Dönen id değerini listeye atayacaksın.

Terminal
curl https://zeslee.arcuslabs.co/api/v1/call-numbers \
  -H "Authorization: Bearer zsl_..."

2. Listeyi oluştur. Kategori, amaç ve çalışma saatlerini ver.

Terminal
curl -X POST https://zeslee.arcuslabs.co/api/v1/call-lists \
  -H "Authorization: Bearer zsl_..." -H "Content-Type: application/json" \
  -d '{
    "name": "Fiyat soranlar",
    "category": "hot",
    "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": [3]
  }'

3. Kişileri ekle. Aynı telefon ikinci kez eklenmez; update açıksa kayıtlı kişi dolu alanlarla güncellenir.

Terminal
curl -X POST https://zeslee.arcuslabs.co/api/v1/call-lists/12/contacts \
  -H "Authorization: Bearer zsl_..." -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://zeslee.arcuslabs.co/api/v1/call-lists/12/activate \
  -H "Authorization: Bearer zsl_..."

5. Sonuçları oku.

Terminal
curl "https://zeslee.arcuslabs.co/api/v1/call-lists/12/contacts?status=answered" \
  -H "Authorization: Bearer zsl_..."
Liste yanıtındaki state.openNow listenin şu an aramaya uygun olup olmadığını, state.nextOpenAt bir sonraki açılışı söyler.

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.

UçNe yapar
GET/call-lists/metaKategoriler, numara stratejileri, saat sınırları, 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 siler; kişiler silinmez
POST/call-lists/:id/activateListeyi aktif yapar
POST/call-lists/:id/deactivateListeyi pasif yapar

Liste oluşturma#

POST/api/v1/call-lists
{
  "name": "Fiyat soranlar",
  "description": "Web formundan gelenler",
  "category": "hot",
  "purpose": "Fiyat teklifimizi anlatıp randevu önermek için arıyoruz.",
  "greeting": "Merhaba %ad%, ben Zeslee asistanıyım.",
  "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": [3, 4],
  "numberStrategy": "round_robin",
  "maxAttempts": 2,
  "retryMinutes": 120,
  "gapSeconds": 0,
  "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, consent aranma izni alınmış kişiler
categoryhot sıcak lead, warm ılık lead, cold soğuk lead, prospect potansiyel müşteri, customer mevcut müşteri, other (varsayılan)
descriptionİsteğe bağlı, en fazla 300 karakter
greetingAçılış cümlesi şablonu; boşsa işletmenin giden arama selamlaması
schedule, timezone, activeFrom, activeUntilÇalışma saatleri bölümünde
numberIds, numberStrategyNumaralar bölümünde
maxAttemptsAçılmazsa en fazla kaç kez aranır (1–3, varsayılan 2)
retryMinutesYeniden deneme aralığı (30–1440 dk, varsayılan 120)
gapSecondsBir görüşme bitince sıradaki aramaya kadar bekleme (0–3600 sn)
concurrencyAynı anda aranan kişi sayısı (1–3)
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": 12,
  "name": "Fiyat soranlar",
  "category": "hot",
  "status": "draft",
  "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": 3, "number": "908502551712", "active": true }] },
  "state": {
    "active": false,
    "openNow": false,
    "nextOpenAt": "2026-11-02T07:00:00.000Z",
    "timezone": "Europe/Istanbul",
    "expired": false,
    "summary": "Pzt 10:00–11:45, 13:30–16:30",
    "blockedReason": null
  }
}

Durumlar#

statusAnlamı
draftTaslak: 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#

GET/api/v1/call-lists?category=hot&status=active&q=fiyat&page=1&pageSize=25

status: active (çalışanlar), passive (çalışmayanlar), ya da draft, running, paused, done.

Ç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/api/v1/call-lists/12/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" },
    { "weekday": 2, "start": "14:00", "end": "15:00" },
    { "weekday": 2, "start": "17:00", "end": "18: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). Dışında liste aramaz. İkisi de boş bırakılırsa 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 ise 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-02T07:00:00.000Z"
}

nextOpenAt UTC'dir. Eski tek aralıklı biçim (windowStart, windowEnd, days) liste oluştururken ve güncellerken hâlâ kabul edilir ve programa dönüştürülür.

Numaralar#

İşletmeye atanmış 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.

UçNe 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 ve strateji
PUT/call-lists/:id/numbersNumaraları ve stratejiyi değiştirir
POST/call-lists/:id/numbers/:numberIdListeye tek numara ekler
DELETE/call-lists/:id/numbers/:numberIdListeden tek numara çıkarır
GET/api/v1/call-numbers
[
  { "id": 3, "phoneNumberId": "phnum_...", "number": "908502551712", "label": "Satış", "active": true, "isDefault": true, "lists": 2 },
  { "id": 4, "phoneNumberId": "phnum_...", "number": "908502551713", "label": "Destek", "active": true, "isDefault": false, "lists": 0 }
]
PUT/api/v1/call-lists/12/numbers
{ "numberIds": [3, 4], "strategy": "round_robin" }

Seçim stratejisi (strategy):

DeğerDavranış
round_robinEtkin numaralar arasında sırayla döner (varsayılan)
randomHer aramada rastgele bir etkin numara
firstHer zaman ilk etkin numara
  • Listeye numara atamazsan işletmenin varsayılan numarası kullanılır.
  • Havuzda olmayan bir numara atanamaz (400). Havuza yeni hat eklemeyi sistem yöneticisi yapar.
  • Devre dışı bırakılan numara hiçbir listede kullanılmaz.
  • Listeye atanan numaraların hepsi devre dışıysa liste arama yapmaz ve nedeni yanıttaki state.blockedReason alanında görünür. Numara etkinleşince liste kendiliğinden devam eder.

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.

UçNe 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). Kişi göndermeden önce topluca sormak için
GET/do-not-call/:phoneTek numara: listede mi, kaydı nedir
DELETE/do-not-call/:phoneNumarayı listeden çıkarır; listede değilse 404
Terminal
# Tek numara ekle
curl -X POST https://zeslee.arcuslabs.co/api/v1/do-not-call \
  -H "Authorization: Bearer zsl_..." -H "Content-Type: application/json" \
  -d '{ "phone": "0532 111 00 01", "reason": "CRM: müşteri aranmak istemiyor" }'
JSON
{ "phone": "05321110001", "added": true, "affectedContacts": 2 }
  • phone hangi biçimde yazılırsa yazılsın (0532 111 00 01, +90 532 111 00 01, 5321110001) 05321110001 olarak saklanır. Yanıtlardaki phone bu biçimdedir.
  • 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 (outcome: "Aranmak istemiyor"). Daha önce aranmış ya da görüşülmüş kişiler değişmez.
  • Toplu ekleme tek istekte ve tekrarsız çalışır; yanıt: received, added, existing (zaten listede), invalid ve geçersiz satırların errors listesi (row, phone, reason). Geçersiz bir satır diğerlerini engellemez.
  • Listeden çıkarılan numara tekrar aranabilir hâle gelir; ama daha önce opted_out yapılmış liste kişileri olduğu gibi kalır, onları listelere yeniden ekleyebilirsin.

Liste öğesi:

JSON
{ "phone": "05321110001", "reason": "Görüşmede istedi", "source": "agent", "createdAt": "2026-10-30T09:16:12Z" }
AlanAçıklama
reasonEklenme nedeni (en fazla 200 karakter)
sourceagent (asistan görüşmede müşteri istediği için ekledi), panel (elle eklendi), api (API ile eklendi)
createdAtEklendiği an (ISO 8601, UTC). since süzgeci bu değere bakar

Toplu sorgu örneği:

POST/api/v1/do-not-call/check
{ "phones": ["0532 111 00 01", "0532 000 00 00", "abc"] }
JSON
{
  "items": [
    { "phone": "0532 111 00 01", "normalized": "05321110001", "valid": true, "doNotCall": true },
    { "phone": "0532 000 00 00", "normalized": "05320000000", "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 (Olay türleri). Böylece CRM'in bu numarayı kendi tarafında da "aranmasın" olarak işaretleyebilir. API ile yaptığın ekleme ve silmeler için olay üretilmez; o değişikliği zaten sen yaptın.

Kişiler#

Kişi (alıcı) listelerden bağımsız tutulur: işletme başına telefon numarası benzersizdir ve bir kişi birden çok listede olabilir. Listeye özel arama durumu (sırada, açılmadı, görüşüldü…) üyelikte tutulur. Kişiyi bir listeden çıkarmak kişiyi ve diğer listelerdeki yerini silmez.

UçNe yapar
GET/contactsTüm kişiler. Süzgeç: q, listId, notInList, sort (name, oldest), page, pageSize
POST/contactsTek kişi ekler (201)
GET/contacts/:idKişi ve üye olduğu listeler
PUT/contacts/:idKişiyi günceller
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 (yanıt biçimi)
POST/call-lists/:id/contactsListeye kişi ekler: { "contactIds": [...] } ya da { "contacts": [...] }
DELETE/call-lists/:id/contacts{ "contactIds": [...] } kişileri listeden çıkarır
DELETE/call-lists/:id/contacts/:contactIdTek kişiyi listeden çıkarır
POST/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": [12]
}
AlanAçıklama
phoneZorunlu. 0532 111 00 01, +90 532 111 00 01, 5321110001 aynı kişidir; 05321110001 olarak saklanır
name, company, noteİsteğe bağlı (ad 80, firma 120, not 300 karakter)
fieldsSerbest alanlar; açılış cümlesinde ve amaçta %sehir% biçiminde kullanılır
externalIdKendi sistemindeki kimlik
listIdsOluşturulurken eklenecek listeler

Kurallar

  • Aynı telefonla ikinci kişi 409 döner. Var olanı güncellemek için PUT/contacts/:id ya da toplu uçta update kullan.
  • Kişi güncellenince değişiklik, henüz aranmamış (ya da cevapsız) tüm liste üyeliklerine yansır. Tamamlanmış aramaların kaydı değişmez.
  • Ş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

Bir kişi aynı anda iki listede aranmaz: biri başka listede aranırken ikinci listedeki arama beklemeye alınır.

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": 5521,
      "targetId": 9014,
      "externalId": "crm-1042",
      "phone": "05321110001",
      "name": "Ayşe Yılmaz",
      "company": "Yılmaz Ltd",
      "note": "Geçen ay demo istedi",
      "fields": { "sehir": "İzmir" },
      "createdAt": "2026-10-21 08:40:12",
      "updatedAt": "2026-10-30 10:04:30",
      "status": "answered",
      "outcome": "Görüşüldü",
      "outcomeCode": "answered",
      "attempts": 1,
      "lastAttemptAt": "2026-10-30 10:02:11",
      "nextAttemptAt": null,
      "finishedAt": "2026-10-30 10:04:30",
      "callId": "conv_0701k8x4q2p3fz9w"
    }
  ],
  "total": 1,
  "page": 1,
  "pageSize": 25,
  "pages": 1
}
AlanAçıklama
idKişi kimliği (listelerden bağımsız). contactIds alanlarında bunu kullanırsın
targetIdBu kişinin bu listedeki üyelik kimliği; call_list.contact.status_changed olayındaki targetId ile aynı
externalId, phone, name, company, note, fieldsKişi bilgileri (phone 05321110001 biçiminde)
statusListedeki arama durumu: pending, calling, answered, no_answer, meeting, opted_out, failed, skipped
outcome, outcomeCodeSonuç metni (serbest) ve sabit sonuç kodu (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 listedeki arama durumu en son ne zaman değişti

Bu uçtaki zamanlar UTC ve YYYY-AA-GG SS:DD:ss biçimindedir (saat dilimi eki yoktur).

Artımlı senkron: `updatedSince`. Yalnızca son senkrondan beri değişen üyeleri alırsın. Değer ISO 8601 ya da yanıttaki biçim olabilir; saat dilimi yazılmazsa UTC sayılır. Karşılaştırma dahildir (>=), aynı saniyedeki değişiklik kaçmasın diye; aynı üyeyi iki kez almak zararsızdır (targetId ile tekilleştir). updatedSince verildiğinde sıra eskiden yeniye updatedAt iledir; böylece sayfaları sırayla gezip son öğenin updatedAt değerini bir sonraki senkronun başlangıcı yaparsın.

Terminal
curl "https://zeslee.arcuslabs.co/api/v1/call-lists/12/contacts?updatedSince=2026-10-30T09:00:00Z&pageSize=100" \
  -H "Authorization: Bearer zsl_..."
Webhook'lar bu uca ihtiyacı büyük ölçüde azaltır; ilk yüklemede ve olası kesintilerden sonra boşluğu kapatmak için updatedSince kullan.

Kişileri listelere bağlama#

POST/api/v1/contacts/assign
{ "contactIds": [1, 2, 3], "listIds": [12, 13] }
POST/api/v1/contacts/unassign
{ "contactIds": [1], "listIds": [13] }

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, geçerli satırları tek işlemde yazar ve satır satır rapor döner.

UçNe 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
POST/call-lists/:id/importCSV'yi içe aktarır ve doğrudan bu listeye ekler

Toplu ekleme#

POST/api/v1/contacts
{
  "update": true,
  "listIds": [12],
  "contacts": [
    { "phone": "05321110002", "name": "Mehmet Kaya", "company": "Kaya A.Ş." },
    { "phone": "05321110003", "name": "Zeynep Çelik", "fields": { "sehir": "Ankara" } }
  ]
}
  • update: true: kayıtlı kişi dosyadaki 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 de 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/api/v1/contacts/bulk
{
  "updates": [
    { "id": 5, "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://zeslee.arcuslabs.co/api/v1/call-lists/12/import \
  -H "Authorization: Bearer zsl_..." -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, referansexternalId
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": 3, "skipped": 1, "failed": 1,
  "errors": [{ "row": 4, "phone": "12", "reason": "Geçerli bir telefon numarası gerekli" }]
}
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
failedDoğrulamadan geçemeyen satır
errorsHer hatalı satırın numarası, telefonu ve nedeni (en fazla 200)

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) alır:

JSON
{ "items": [ ... ], "total": 120, "page": 1, "pageSize": 25, "pages": 5 }

Tekrarlanan istekler (Idempotency-Key)#

Liste oluşturma, kişi ekleme, içe aktarma ve atama uçları Idempotency-Key başlığını destekler:

Terminal
curl -X POST https://zeslee.arcuslabs.co/api/v1/call-lists/12/import \
  -H "Authorization: Bearer zsl_..." -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; ilk istek sürerken gelen tekrar 409 alır.

Anahtarsız istekler de mükerrer kayıt oluşturmaz: aynı telefon tek kişidir, aynı ad tek listedir (409).

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, kategori 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. Otomatik arama sekmesinde açılış cümlesini, arama amacını, 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. Panelden elle açılan arama listelerinden bağımsızdı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
phoneevet10–13 karakter; boşluk ve tire temizlenir, +90… ve 90… yazımları 05XXXXXXXXX biçimine ç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 ya da sayı, en fazla 200 karakter. İç içe nesne ve dizi yok sayılır

Tek istekte en fazla 500 kişi, kuyrukta toplam en fazla 2000 kişi olabilir.

Yanıt 201:

JSON
{
  "added": 1,
  "updated": 0,
  "skipped": [],
  "contacts": [{ "id": 812, "phone": "05321234567", "updated": false }],
  "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 amacı, her kişi için doldurulur. Yer tutucu %alan% ya da {{alan}} biçimindedir.

Metin
Merhaba %ad%, ben Zeslee asistanı. %firma% adına %paket% paketiyle ilgili arıyorum; 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; cümleyi buna göre kur ("Merhaba %ad%" yerine 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 ayar ekranının altındaki önizleme, gerçek bir kişiyle cümlenin nasıl okunacağını gösterir.

Kuyruğu yönetme#

İstekNe 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, amaç, saatler, günler, bekleme) panelde yapılır. GET/outbound/campaign yanıtı:

JSON
{
  "id": 7,
  "status": "running",
  "greeting": "Merhaba %ad%, ...",
  "purpose": "...",
  "windowStart": "10:00",
  "windowEnd": "18:00",
  "days": [1, 2, 3, 4, 5],
  "gapSeconds": 60,
  "concurrency": 2,
  "maxAttempts": 2,
  "retryMinutes": 120,
  "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ı numaradan aynı anda kaç kişi aranır. Aramalar paralel yürür; aynı turda başlayan aramaların arasına bir saniyelik fark konur. gapSeconds, biten bir görüşmeden sonra sıradakine geçmeden önceki bekleme süresidir; ilk turda tüm hatlar birlikte açılır. Her görüşme ayrı ücretlendirilir ve ses altyapısı hesabının eşzamanlı konuşma sınırı da geçerlidir.
  • 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.
  • 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 2000 kişi.

Terminal
curl -X POST "$BASE/outbound/contacts" \
  -H "Authorization: Bearer $ZESLEE_KEY" -H "Content-Type: text/csv" \
  --data-binary @kisiler.csv
CSV
telefon;ad;firma;not;musteri_no;paket
0532 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 sayıları görünür.

Panelden elle açtığın bir arama listesine de aynı şekilde aktarabilirsin:

İstekNe yapar
POST/campaigns/{id}/contactsListeye kişi ekler (JSON ya da CSV). Aranmamış kişi tekrar gelirse güncellenir. Tamamlanmış liste için 409, olmayan liste için 404 döner

Panelde Giden aramalar → İçe aktar aynı çözümleyiciyi kullanır: dosyayı yükler ya da yapıştırırsın, sütun eşleşmelerini ve ilk satırları önizleyip içe aktarırsın. Pencerenin API ile sekmesi seçili listenin adresini ve hazır örnekleri verir.

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ı (15 saniyeden uzun)
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 üyeleri (GET/call-lists/:id/contacts) ve call_list.contact.status_changed olayında bulunur; pending kişide null'dı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ü; deneme hakkı varsa yeniden denenirno_answer
unreachableUlaşılamadı (operatör hatası: kapalı, meşgul vb.); deneme hakkı varsa yeniden denenirno_answer
retries_exhaustedDeneme hakkı bittiskipped
automatedKarşıdaki sesli menü ya da telesekreter; asistan kapattı, kişi yeniden aranmazskipped
opted_outAranmak istemediopted_out
start_failedArama başlatılamadıfailed

Kişi nesnesi:

JSON
{
  "id": 812,
  "externalId": "crm-10234",
  "phone": "05321234567",
  "name": "Ahmet Yılmaz",
  "company": "Yılmaz Ticaret",
  "note": "",
  "fields": { "yas": "21" },
  "status": "answered",
  "outcome": "İlgilendi, fiyat bilgisi istedi",
  "attempts": 1,
  "lastAttemptAt": "2026-10-21T09:12:44.000Z",
  "finishedAt": "2026-10-21T09:14:02.000Z",
  "order": 3,
  "callId": "CA..."
}

GET/outbound/contacts/{id} ayrıca call alanını verir: süre, özet ve konuşma dökümü. {id} olarak Zeslee kimliğini ya da kendi externalId değerini kullanabilirsin.

Okuma ve silme#

İstekNe yapar
GET/outbound/contactsKiş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 döner
GET/outbound/liveŞu an aranan kişiler: durum (ringing çalıyor / talking konuşuyor), geçen süre, deneme. Panelde Geliştirici → Canlı sekmesinde döküm de akar
GET/outbound/report?from=2026-10-01&to=2026-10-31Kuyruk raporu (varsayılan son 30 gün)

Rapor yanıtı:

JSON
{
  "business": { "id": 3, "name": "Örnek Ltd." },
  "from": "2026-10-01",
  "to": "2026-10-31",
  "status": "running",
  "contacts": { "total": 40, "pending": 31, "answered": 5, "meeting": 1, "no_answer": 2, "opted_out": 1, "failed": 0, "skipped": 0, "calling": 0 },
  "calls": { "attempted": 9, "answered": 6, "meetings": 1, "noAnswer": 2, "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:

İstekNe yapar
GET/callsGörüşme listesi: from, to, direction (inbound / outbound), limit (en fazla 200), before
GET/calls/{id}Özet ve konuşma dökümü
GET/reports/summaryToplam, gelen, giden, yanıtlanan, dakika ve randevu özeti
GET/campaigns, GET/campaigns/{id}Panelden açılan arama listeleri

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 bir adresle herkese açılmaz ve webhook gövdesinde dolaşmaz.

Terminal
# 1) Olayda gelen yol
#    "recording": { "available": true, "path": "/api/v1/calls/conv_123/recording" }

# 2) Dosyayı indir
curl -H "Authorization: Bearer $ZESLEE_KEY" -o gorusme.mp3 \
  "https://zeslee.arcuslabs.co/api/v1/calls/conv_123/recording"
DurumYanıt
Kayıt var200, ses dosyası (audio/mpeg)
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 ("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 (önbelleğe alınmaz) ve her indirme denetim kaydına yazılır (panelde Güvenlik → Denetim). 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 `/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.

Webhook'lara giriş#

Webhook'ta sen Zeslee'yi sormazsın, Zeslee seni arar. Bir görüşme bitince, randevu alınınca ya da bir mesaj gidince Zeslee, 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 ──► Zeslee olayı kaydeder
                                                 │
                              POST https://sistemin.com/zeslee/webhook
                              Zeslee-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 üretilmez.

Webhook hızlı başlangıç#

Dört adımda ilk olayını al. zsl_... 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://zeslee.arcuslabs.co/api/v1/webhooks \
  -H "Authorization: Bearer zsl_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://sistemin.com/zeslee/webhook",
    "description": "CRM senkronu",
    "events": ["call.completed"]
  }'
JSON
{
  "id": 12,
  "url": "https://sistemin.com/zeslee/webhook",
  "description": "CRM senkronu",
  "events": ["call.completed"],
  "active": true,
  "includeTranscript": true,
  "secretHint": "whsec_aB3d…",
  "previousSecretExpiresAt": null,
  "failureStreak": 0,
  "disabledReason": null,
  "lastSuccessAt": null,
  "lastFailureAt": null,
  "createdAt": "2026-10-30T09:12:44Z",
  "secret": "whsec_aB3dE5gH7jK9mN1pQ3sT5vW7yZ9bC2dF"
}

secret yalnızca bu yanıtta tam görünür. Gelen isteklerin Zeslee'den 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://zeslee.arcuslabs.co/api/v1/webhooks/12/test \
  -H "Authorization: Bearer zsl_..."
JSON
{ "delivery": { "id": 481, "eventType": "webhook.test", "status": "success", "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.

UçNe 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 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 0). Geçiş süresi verilirse eski anahtar o süre boyunca geçerli kalır (Anahtar yenileme). 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). Örnek yük gerçek biçimdedir (Örnek olay gönderme)
GET/webhooks/:id/deliveriesTeslimat geçmişi: limit (en fazla 100), before, status (pending, success, 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": 12 }
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 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 (KVKK gereği dökümü saklamak istemeyenler için). Webhook başına ayarlanır

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 istekte 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. Geçiş süresince her istek iki anahtarla imzalanır ve başlıkta iki v1 bulunur: Zeslee-Signature: t=1761815000,v1=<yeni anahtarla>,v1=<eski anahtarla>. Alıcın bildiği anahtarla doğrulayan imzayı bulur (İmza doğrulama örnekleri 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://zeslee.arcuslabs.co/api/v1/webhooks/12/test \
  -H "Authorization: Bearer zsl_..." -H "Content-Type: application/json" \
  -d '{ "eventType": "appointment.created" }'
  • Yük, gerçek olayla aynı alanlara sahiptir; değerler belirgin biçimde örnektir (id: 0, conv_ornek0001, crm-ornek-1, 05320000000).
  • 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. Geçmiş, işletmenin en az bir webhook'u olduğu ve geliştirici ayarları açık olduğu sürece tüm olayları tutar.

Olay zarfı#

Her istek aynı biçimdedir: gövde bir JSON zarfı, olaya özgü veri data içindedir.

JSON
{
  "id": "evt_9f2c41b7a0d3e58c6b1f2a90",
  "type": "call.completed",
  "apiVersion": "1",
  "createdAt": "2026-10-30T09:16:31.402Z",
  "businessId": 3,
  "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, bu yüzden 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 true'dur (Örnek olay gönderme). 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-AgentZeslee-Webhooks/1.0
Zeslee-Event-IdZarftaki id ile aynı
Zeslee-Event-TypeZarftaki type ile aynı. Gövdeyi okumadan yönlendirmek için kullan
Zeslee-Delivery-IdBu teslimatın kimliği
Zeslee-AttemptKaçıncı deneme: 1, 2, ...
Zeslee-Timestampİmzanın üretildiği an (Unix saniye)
Zeslee-Signaturet=<zaman>,v1=<imza> (İmza doğrulama)

İmza doğrulama#

Adresin herkese açıktır; bu yüzden gelen isteğin gerçekten Zeslee'den 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. Zeslee-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.ZESLEE_WEBHOOK_SECRET // whsec_...
const app = express()

// Ham gövde için express.json() yerine express.raw() kullan
app.post("/zeslee/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("zeslee-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) => 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, time
from flask import Flask, abort, request

SECRET = os.environ["ZESLEE_WEBHOOK_SECRET"].encode()  # whsec_...
app = Flask(__name__)

@app.post("/zeslee/webhook")
def zeslee_webhook():
    header = request.headers.get("Zeslee-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

PHP

php
<?php
$secret = getenv('ZESLEE_WEBHOOK_SECRET'); // whsec_...
$body   = file_get_contents('php://input'); // ham gövde
$header = $_SERVER['HTTP_ZESLEE_SIGNATURE'] ?? '';

if (!preg_match('/t=(\d+)/', $header, $tm) || !preg_match_all('/v1=([0-9a-f]{64})/', $header, $sm)) { http_response_code(400); exit; }
$t = $tm[1]; $signatures = $sm[1]; // anahtar geçişinde birden çok imza olabilir
if (abs(time() - (int) $t) > 300) { http_response_code(400); exit; }

$expected = hash_hmac('sha256', $t . '.' . $body, $secret);
$ok = false;
foreach ($signatures as $v1) { if (hash_equals($expected, $v1)) { $ok = true; } }
if (!$ok) { http_response_code(400); exit; }

$event = json_decode($body, true);
// ... olayı işle
http_response_code(200);

C# (ASP.NET minimal API)

csharp
app.MapPost("/zeslee/webhook", async (HttpRequest req) =>
{
    using var ms = new MemoryStream();
    await req.Body.CopyToAsync(ms);
    var body = ms.ToArray();

    var header = req.Headers["Zeslee-Signature"].ToString();
    var tm = Regex.Match(header, @"t=(\d+)");
    var signatures = Regex.Matches(header, @"v1=([0-9a-f]{64})").Select(x => x.Groups[1].Value).ToList(); // anahtar geçişinde birden çok olabilir
    if (!tm.Success || signatures.Count == 0) return Results.BadRequest();
    var t = tm.Groups[1].Value;
    if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(t)) > 300) return Results.BadRequest();

    using var h = new HMACSHA256(Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("ZESLEE_WEBHOOK_SECRET")!));
    var signed = Encoding.UTF8.GetBytes(t + ".").Concat(body).ToArray();
    var expected = Convert.ToHexString(h.ComputeHash(signed)).ToLowerInvariant();
    if (!signatures.Any(v1 => CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(expected), Encoding.ASCII.GetBytes(v1))))
        return Results.BadRequest();

    var evt = JsonSerializer.Deserialize<JsonElement>(body);
    return Results.Ok();
});

Teslimat ve yeniden denemeler#

Zeslee 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 (±%10 sapmayla) yeniden denenir. Toplam 8 deneme, yaklaşık 21 saat.

DenemeBir önceki başarısız denemeden sonra bekleme
1hemen
230 saniye
32 dakika
410 dakika
530 dakika
62 saat
76 saat
812 saat

Sekiz deneme de başarısız olursa teslimat failed olur. Panelde Geçmiş'ten 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. Iş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ı (startedAt, updatedAt...) 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 saklanır, sonra silinir (içerikleri diskte şifrelidir). 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.createdAsistan yeni bir randevu oluşturdu
appointment.cancelledRandevu iptal edildi (panelden, müşterinin mesajıyla ya da asistan tarafından)
appointment.confirmedMüşteri randevusunu mesajla 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 başladı, durdu ya da tamamlandı
do_not_call.addedNumara aranmayacaklar listesine eklendi (panelden ya da müşteri görüşmede istediği için asistan ekledi)
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).

call.completed#

Bir görüşme bitip kaydı işlendiğinde gelir. Asistan görüşmeyi işledikten sonra, çoğunlukla 1–3 dakika içinde ulaşır (bazı altyapı seçeneklerinde görüşme biter bitmez). data.call, GET/calls/{id} ile aynı nesnedir ve konuşma dökümünü içerir.

JSON
{
  "call": {
    "id": "conv_0701k8x4q2p3fz9w",
    "startedAt": "2026-10-30T09:14:02Z",
    "direction": "inbound",
    "phone": "05321234567",
    "customerName": "Ayşe Yılmaz",
    "durationSecs": 142,
    "answered": true,
    "result": "answered",
    "status": "done",
    "success": "success",
    "title": "Cumartesi için randevu",
    "terminationReason": "end_call tool was called.",
    "summary": "Müşteri cumartesi 14:00 için saç kesimi randevusu aldı.",
    "campaign": null,
    "transcript": [
      { "role": "asistan", "text": "Merhaba, Örnek Kuaför, 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ı)
startedAtGörüşmenin başladığı an
directioninbound (gelen) ya da outbound (giden); bilinmiyorsa null
phoneKarşı tarafın numarası (05321234567 biçiminde); gizli numara ise null
customerNameMüşteri kaydındaki ad; yoksa null
durationSecsKonuşma süresi (saniye)
answeredSüre 15 saniye ve üstü ise true
resultanswered (açıldı), no_answer, busy, voicemail (telesekreter ya da sesli menü), failed
statusGörüşmenin durumu (done, failed)
successAsistanın görüşme değerlendirmesi: success, failure, unknown
titleKısa başlık
terminationReasonGörüşmenin neden bittiği (ses altyapısının verdiği metin)
summaryGörüşme özeti; yoksa null
campaignGörüşme bir arama listesinden yapıldıysa { id, name, targetStatus }, değilse null
recording{ available, path }. Ses kaydı varsa available: true ve indirme yolu; yoksa false/null (Ses kayıtları)
transcriptKonuşma dökümü: role (asistan / musteri), text, secs (görüşmenin kaçıncı saniyesi). 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": 212,
    "code": "4821",
    "status": "confirmed",
    "customerName": "Ayşe Yılmaz",
    "phone": "05321234567",
    "service": { "id": "sac-kesim", "name": "Saç kesimi" },
    "staff": { "id": "elif", "name": "Elif" },
    "date": "2026-11-07",
    "startTime": "14:00",
    "endTime": "14:30",
    "timezone": "Europe/Istanbul",
    "note": null,
    "notifyChannel": "sms",
    "customerConfirmed": false,
    "createdAt": "2026-10-30T09:16:12Z",
    "callId": "conv_0701k8x4q2p3fz9w",
    "contact": {
      "id": 5521,
      "externalId": "crm-1042",
      "phone": "05321234567",
      "name": "Ayşe Yılmaz",
      "company": "Yılmaz Ltd",
      "lists": [{ "id": 12, "name": "Sıcak leadler", "targetId": 9014, "status": "meeting" }]
    }
  }
}
AlanAçıklama
callIdRandevunun alındığı görüşme (call.completed olayındaki call.id ve GET/calls/{id} ile aynı). Asistan görüşme kimliğini aracı çağırırken iletir; iletmediyse o numaranın o anda aranmakta olan liste görüşmesi kullanılır. Bulunamazsa null
contactRandevudaki telefona ait kişi kaydı: kendi sistemindeki externalId, ad, firma ve üye olduğu listeler (lists[]: liste kimliği ve adı, targetId üyelik kimliği, listedeki güncel arama status'u). Bu telefonla kayıtlı kişi yoksa null
codeMüşterinin bildiği randevu kodu (işletme içinde benzersiz)
statusconfirmed ya da cancelled
staffPersonel atanmadıysa null
notifyChannelMüşterinin bilgilendirme tercihi: sms, whatsapp ya da null
customerConfirmedMüşteri mesajla onayladıysa true

order.created, order.status_changed#

JSON
{
  "order": {
    "id": 87,
    "code": "K7M2QX",
    "status": "new",
    "type": "teslimat",
    "customerName": "Mehmet Demir",
    "phone": "05329876543",
    "address": "Moda Cad. 12/4 Kadıköy",
    "items": [{ "name": "Karışık pizza", "qty": 2, "unit": 280 }],
    "subtotal": 560,
    "deliveryFee": 30,
    "discount": 0,
    "total": 590,
    "note": null,
    "notifyChannel": null,
    "createdAt": "2026-10-30T11:02:40Z"
  },
  "previousStatus": "new"
}

previousStatus yalnızca order.status_changed olayında bulunur. status: new, preparing, done, cancelled. type: gel-al ya da teslimat (address yalnızca teslimatta dolu). Tutarlar işletmenin para biriminde (TL) sayıdır.

customer.created, customer.updated#

JSON
{
  "customer": {
    "id": 341,
    "phone": "05321234567",
    "name": "Ayşe Yılmaz",
    "email": "[email protected]",
    "status": "customer",
    "source": "agent",
    "tags": ["düzenli"],
    "memberNo": null,
    "memberPlan": null,
    "createdAt": "2026-10-30T09:14:05Z",
    "updatedAt": "2026-10-30T09:16:12Z"
  }
}

status: lead (potansiyel), customer, member (üye). Durum yalnızca yukarı çıkar. source: kaydın nereden geldiği (call, agent...). Müşteri notları ve özel alanlar webhook'ta gönderilmez; gerekirse API'den oku.

message.sent, message.failed, message.received#

JSON
{
  "message": {
    "id": 1290,
    "channel": "sms",
    "direction": "out",
    "kind": "confirmation",
    "status": "sent",
    "phone": "05321234567",
    "email": null,
    "body": "Örnek Kuaför: Randevunuz onaylandı. Cumartesi saat 14:00. Kod: 4821.",
    "error": null,
    "appointmentId": 212,
    "orderId": null,
    "attempts": 1,
    "createdAt": "2026-10-30T09:16:12Z",
    "sentAt": "2026-10-30T09:16:14Z"
  }
}
AlanAçıklama
channelsms, whatsapp, email
directionout (gönderilen) ya da in (müşteriden gelen)
kindconfirmation (onay), reminder (hatırlatma), cancellation (iptal), order, inbound (müşteri yanıtı), reply (otomatik yanıt)...
statussent, simulated (test ortamı; gerçek gönderim yok), failed, received
errorYalnızca message.failed olayında doludur

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": 5521,
    "targetId": 9014,
    "externalId": "crm-1042",
    "phone": "05321110001",
    "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:11Z",
    "finishedAt": "2026-10-30T10:04:30Z",
    "callId": "conv_0701k8x4q2p3fz9w"
  },
  "list": { "id": 12, "name": "Sıcak leadler" }
}

status: calling (aranıyor), answered (görüşüldü), meeting (randevu alındı), no_answer (açılmadı; deneme hakkı varsa yeniden denenir), skipped (deneme hakkı bitti), opted_out (aranmak istemiyor), failed (arama başlatılamadı). Görüşmenin ayrıntısı için callId ile GET/calls/{id} çağır ya da call.completed olayını bekle. contact.id listelerden bağımsız kişi kimliğidir; targetId listedeki üyelik satırıdır.

  • `summary` arama sonuçlandığında dolar (görüşme özeti). Kişi calling iken ya da özet henüz yoksa null'dır. Özet sonradan oluşursa bu olay yeniden gelmez; özeti call.completed olayından ya da GET/calls/{id}'den al.
  • `outcome` Türkçe, kullanıcıya gösterilebilir serbest metindir ve değişebilir (örn. Görüşüldü, Ulaşılamadı: Telefon kapalı). Koda status ve `outcomeCode` ile bağlan; outcomeCode sabit bir listedir (Kişi durumları).
  • Yeniden deneme: açılmayan kişi pending'e dönmez. Deneme hakkı varken status no_answer kalır ve nextAttemptAt bir sonraki deneme zamanını gösterir; hak bitince skipped olur.

call_list.status_changed#

JSON
{
  "list": {
    "id": 12,
    "name": "Sıcak leadler",
    "category": "hot",
    "status": "done",
    "previousStatus": "running",
    "counts": { "answered": 31, "meeting": 6, "no_answer": 2, "skipped": 4, "opted_out": 1 },
    "updatedAt": "2026-10-30T14:40:02Z"
  }
}

status: draft, running, paused, done. 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": "05321110001",
    "reason": "Görüşmede istedi",
    "source": "agent",
    "createdAt": "2026-10-30T09:16:12Z"
  }
}

source: agent (asistan ekledi) ya da panel. do_not_call.removed olayında reason, source ve createdAt null'dı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 ve her biri için ayrıca call_list.contact.status_changed olayı gelir.

webhook.test#

JSON
{ "message": "Bu bir deneme olayıdır. Webhook adresin çalışıyor.", "webhookId": 12 }

Olay geçmişi ve kaçan olaylar#

Adresin bir süre kapalı kaldıysa ya da bir olayı yanlışlıkla işlemediysen, Zeslee 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://zeslee.arcuslabs.co/api/v1/webhooks/12/deliveries?status=failed" \
  -H "Authorization: Bearer zsl_..."
JSON
{
  "items": [
    {
      "id": 478,
      "eventId": "evt_9f2c41b7a0d3e58c6b1f2a90",
      "eventType": "call.completed",
      "status": "failed",
      "attempts": 8,
      "nextAttemptAt": null,
      "lastAttemptAt": "2026-10-31T06:14:45Z",
      "deliveredAt": null,
      "responseCode": 503,
      "responseBody": "Service Unavailable",
      "error": "Beklenen 2xx, gelen 503",
      "durationMs": 212,
      "createdAt": "2026-10-30T09:16:31Z"
    }
  ],
  "next": null
}

Yeniden gönder. Adresini düzelttikten sonra:

Terminal
# Tek teslimatı yeniden dene
curl -X POST https://zeslee.arcuslabs.co/api/v1/webhook-deliveries/478/retry \
  -H "Authorization: Bearer zsl_..."

# Bir olayı (aboneliği olmasa bile) başka bir webhook'a gönder
curl -X POST https://zeslee.arcuslabs.co/api/v1/webhook-events/evt_9f2c41b7a0d3e58c6b1f2a90/resend \
  -H "Authorization: Bearer zsl_..." -H "Content-Type: application/json" \
  -d '{ "webhookId": 13 }'

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 Zeslee'yi 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

Hatalar#

Hata yanıtı her zaman { "error": "..." } biçimindedir; error Türkçe, kullanıcıya gösterilebilir bir cümledir.

KodNe zaman
400Gövde boş ya da geçersiz; doğrulama hatası (geçersiz saat, çakışan aralık, geçersiz kategori ya da 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 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ış
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. Partner planında kullanım izlenir ama ücret çıkmaz. Ayrıntılar panelde Faturalar & Ödeme sayfasındadır.

Yasal uyum#

Aramalar yalnızca işletmeler (B2B) ya da aranma izni alınmış kişiler için yapılabilir; hangisi olduğunu panelde ayarlarken 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.


Örnekler#

curl#

Terminal
export ZESLEE_KEY="zsl_..."
BASE="https://zeslee.arcuslabs.co/api/v1"

# Kişi ekle
curl -X POST "$BASE/outbound/contacts" \
  -H "Authorization: Bearer $ZESLEE_KEY" -H "Content-Type: application/json" \
  -d '{"contacts":[{"phone":"05321234567","name":"Ahmet","company":"Yılmaz Ticaret","externalId":"crm-1","fields":{"paket":"Pro"}}]}'

# Başlat
curl -X POST "$BASE/outbound/campaign/start" -H "Authorization: Bearer $ZESLEE_KEY"

# Sonuçlar
curl "$BASE/outbound/contacts?status=answered" -H "Authorization: Bearer $ZESLEE_KEY"
curl "$BASE/outbound/report?from=2026-10-01" -H "Authorization: Bearer $ZESLEE_KEY"

JavaScript (Node 18+)#

JavaScript
const BASE = "https://zeslee.arcuslabs.co/api/v1";
const headers = { Authorization: `Bearer ${process.env.ZESLEE_KEY}`, "Content-Type": "application/json" };

async function zeslee(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 zeslee(path, init);
  }
  const body = await res.json();
  if (!res.ok) throw new Error(`${res.status}: ${body.error}`);
  return body;
}

// 500'erli gruplar halinde gönder
async function push(contacts) {
  for (let i = 0; i < contacts.length; i += 500) {
    const r = await zeslee("/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, requests

BASE = "https://zeslee.arcuslabs.co/api/v1"
S = requests.Session()
S.headers["Authorization"] = "Bearer " + os.environ["ZESLEE_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://zeslee.arcuslabs.co/api/v1";
const headers = { Authorization: "Bearer " + process.env.ZESLEE_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", category: "hot", 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, requests

base = "https://zeslee.arcuslabs.co/api/v1"
s = requests.Session()
s.headers["Authorization"] = "Bearer " + os.environ["ZESLEE_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", "category": "hot", "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ı Zeslee asistanı yapar.

Araç tanımları#

JSON
[
  {
    "name": "zeslee_kisi_ekle",
    "description": "Zeslee 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": "zeslee_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: zeslee_kisi_ekle → POST/outbound/contacts, zeslee_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 halinde 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? [email protected] ya da +90 543 313 35 26 numarasından bize ulaş.

© 2026 Arcus Labs CorporationKullanım koşullarıGizlilikVeri işleme sözleşmesi

Bu bölümde

Genel bakış

Kısayollar

Webhook’lara girişSes kayıtlarıOpenAPI tanımıDestek