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).
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şiler | Otomatik arama kuyruğu | |
|---|---|---|
| Ne için | Birden çok liste, alıcı havuzu, kategori, liste bazlı saat ve numara | Tek bir kuyruğa kişi basıp sırayla aratmak |
| Kişi | Listelerden bağımsız; bir kişi birden çok listede olur | Kuyruğa özel |
| Çalışma saatleri | Gün başına birden çok aralık, saat dilimi, etkin tarih | Tek aralık ve günler |
| Numara | Havuzdan 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.
| Gereken | Eksikse |
|---|---|
| Geliştirici ayarları açık | 403 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.
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
429veRetry-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.
curl https://zeslee.arcuslabs.co/api/v1/call-numbers \
-H "Authorization: Bearer zsl_..."2. Listeyi oluştur. Kategori, amaç ve çalışma saatlerini ver.
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.
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.
curl -X POST https://zeslee.arcuslabs.co/api/v1/call-lists/12/activate \
-H "Authorization: Bearer zsl_..."5. Sonuçları oku.
curl "https://zeslee.arcuslabs.co/api/v1/call-lists/12/contacts?status=answered" \
-H "Authorization: Bearer zsl_..."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/meta | Kategoriler, numara stratejileri, saat sınırları, toplu işlem sınırları |
GET/call-lists | Listeleri sayfalı döner |
POST/call-lists | Liste oluşturur (201) |
GET/call-lists/:id | Liste detayı: ayarlar, sayılar, numaralar, durum |
PUT/call-lists/:id | Verilen alanları günceller; gönderilmeyenler korunur |
DELETE/call-lists/:id | Listeyi siler; kişiler silinmez |
POST/call-lists/:id/activate | Listeyi aktif yapar |
POST/call-lists/:id/deactivate | Listeyi pasif yapar |
Liste oluşturma#
/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
}| Alan | Açıklama |
|---|---|
name | Zorunlu. İşletmede benzersiz (aynı ad 409) |
purpose | Zorunlu, en az 10 karakter. Asistanın görüşmede ne anlatacağı. %ad%, %firma% gibi yer tutucular kullanılabilir |
consentBasis | Zorunlu. b2b işletmelere yönelik, consent aranma izni alınmış kişiler |
category | hot 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 |
greeting | Açılış cümlesi şablonu; boşsa işletmenin giden arama selamlaması |
schedule, timezone, activeFrom, activeUntil | Çalışma saatleri bölümünde |
numberIds, numberStrategy | Numaralar bölümünde |
maxAttempts | Açılmazsa en fazla kaç kez aranır (1–3, varsayılan 2) |
retryMinutes | Yeniden deneme aralığı (30–1440 dk, varsayılan 120) |
gapSeconds | Bir görüşme bitince sıradaki aramaya kadar bekleme (0–3600 sn) |
concurrency | Aynı anda aranan kişi sayısı (1–3) |
contactIds, contacts | Oluştururken listeye eklenecek mevcut ya da yeni kişiler |
Liste oluşturma yanıtı ve GET/call-lists/:id aynı biçimdedir:
{
"id": 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#
status | Anlamı |
|---|---|
draft | Taslak: henüz başlatılmadı |
running | Aktif. Çalışma aralıklarında kişileri arar |
paused | Pasif. Hiç aranmaz; kalan kişiler bekler |
done | Aranacak kimse kalmadı |
Listeyi aktifleştirmek için asistan ve telefon hattı bağlı olmalı ve listede aranacak kişi bulunmalı, yoksa 400. Çalışan liste silinemez (409); önce pasife al.
Listeleri süzme#
/api/v1/call-lists?category=hot&status=active&q=fiyat&page=1&pageSize=25status: 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.
/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:DDbiçiminde ve 09:00–20:00 arasında olmalı. Başlangıç bitişten önce gelmeli. - Aynı günde çakışan aralıklar reddedilir (
400). Bitişik aralıklar (10:00–11:00 ve 11:00–12:00) tek aralığa birleşir. - Kısayol:
{ "days": [1, 2, 3], "ranges": [["10:00", "11:45"], ["13:30", "16:30"]] }bu günlerin hepsine aynı aralıkları uygular. timezone: IANA adı (örn.Europe/Istanbul). Boşsa işletmenin saat dilimi. Saatler bu dilimde uygulanır.activeFrom,activeUntil: listenin arama yapabileceği tarihler (YYYY-AA-GG, ikisi de dahil). 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:
{
"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-numbers | Havuzdaki numaralar |
PUT/call-numbers/:id | Numarayı etkinleştirir ya da devre dışı bırakır ({ "active": false }), etiketi değiştirir |
GET/call-lists/:id/numbers | Listeye atanmış numaralar ve strateji |
PUT/call-lists/:id/numbers | Numaraları ve stratejiyi değiştirir |
POST/call-lists/:id/numbers/:numberId | Listeye tek numara ekler |
DELETE/call-lists/:id/numbers/:numberId | Listeden tek numara çıkarır |
/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 }
]/api/v1/call-lists/12/numbers{ "numberIds": [3, 4], "strategy": "round_robin" }Seçim stratejisi (strategy):
| Değer | Davranış |
|---|---|
round_robin | Etkin numaralar arasında sırayla döner (varsayılan) |
random | Her aramada rastgele bir etkin numara |
first | Her 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.blockedReasonalanı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-call | Liste (en yeni önce). Süzgeç: q (numara ya da neden), source, since, page, pageSize |
POST/do-not-call | Numara ekler. Tek: { "phone": "...", "reason": "..." } · toplu: { "phones": [...], "reason": "..." } ya da { "numbers": [{ "phone": "...", "reason": "..." }] } (en fazla 5.000) |
POST/do-not-call/check | Numaralardan hangileri listede? { "phones": [...] } (en fazla 1.000). Kişi göndermeden önce topluca sormak için |
GET/do-not-call/:phone | Tek numara: listede mi, kaydı nedir |
DELETE/do-not-call/:phone | Numarayı listeden çıkarır; listede değilse 404 |
# 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" }'{ "phone": "05321110001", "added": true, "affectedContacts": 2 }phonehangi biçimde yazılırsa yazılsın (0532 111 00 01,+90 532 111 00 01,5321110001)05321110001olarak saklanır. Yanıtlardakiphonebu biçimdedir.added: false, numaranın zaten listede olduğunu gösterir (200); yeni eklemede201döner. İşlem tekrarlanabilir, zararsızdır.affectedContacts, numaranın bekleyen liste kişilerinden kaç tanesininopted_outyapıldığıdır (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),invalidve geçersiz satırlarınerrorslistesi (row,phone,reason). Geçersiz bir satır diğerlerini engellemez. - Listeden çıkarılan numara tekrar aranabilir hâle gelir; ama daha önce
opted_outyapılmış liste kişileri olduğu gibi kalır, onları listelere yeniden ekleyebilirsin.
Liste öğesi:
{ "phone": "05321110001", "reason": "Görüşmede istedi", "source": "agent", "createdAt": "2026-10-30T09:16:12Z" }| Alan | Açıklama |
|---|---|
reason | Eklenme nedeni (en fazla 200 karakter) |
source | agent (asistan görüşmede müşteri istediği için ekledi), panel (elle eklendi), api (API ile eklendi) |
createdAt | Eklendiği an (ISO 8601, UTC). since süzgeci bu değere bakar |
Toplu sorgu örneği:
/api/v1/do-not-call/check{ "phones": ["0532 111 00 01", "0532 000 00 00", "abc"] }{
"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/contacts | Tüm kişiler. Süzgeç: q, listId, notInList, sort (name, oldest), page, pageSize |
POST/contacts | Tek kişi ekler (201) |
GET/contacts/:id | Kişi ve üye olduğu listeler |
PUT/contacts/:id | Kişiyi günceller |
DELETE/contacts/:id | Kişiyi ve tüm üyeliklerini siler |
GET/call-lists/:id/contacts | Liste üyeleri ve arama durumları. Süzgeç: status, q, updatedSince, page, pageSize (yanıt biçimi) |
POST/call-lists/:id/contacts | Listeye kişi ekler: { "contactIds": [...] } ya da { "contacts": [...] } |
DELETE/call-lists/:id/contacts | { "contactIds": [...] } kişileri listeden çıkarır |
DELETE/call-lists/:id/contacts/:contactId | Tek kişiyi listeden çıkarır |
/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]
}| Alan | Açıklama |
|---|---|
phone | Zorunlu. 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) |
fields | Serbest alanlar; açılış cümlesinde ve amaçta %sehir% biçiminde kullanılır |
externalId | Kendi sistemindeki kimlik |
listIds | Oluşturulurken eklenecek listeler |
Kurallar
- Aynı telefonla ikinci kişi
409döner. Var olanı güncellemek için PUT/contacts/:idya da toplu uçtaupdatekullan. - 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ı#
status | Anlamı |
|---|---|
pending | Sırada |
calling | Şu an aranıyor |
answered | Görüşüldü |
meeting | Görüşmede randevu ya da görüşme ayarlandı |
no_answer | Açılmadı; deneme hakkı varsa yeniden denenir |
skipped | Ulaşılamadı, deneme hakkı bitti |
failed | Arama başlatılamadı |
opted_out | "Beni aramayın" dedi ya da aranmayacaklar listesinde |
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.
{
"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
}| Alan | Açıklama |
|---|---|
id | Kişi kimliği (listelerden bağımsız). contactIds alanlarında bunu kullanırsın |
targetId | Bu kişinin bu listedeki üyelik kimliği; call_list.contact.status_changed olayındaki targetId ile aynı |
externalId, phone, name, company, note, fields | Kişi bilgileri (phone 05321110001 biçiminde) |
status | Listedeki arama durumu: pending, calling, answered, no_answer, meeting, opted_out, failed, skipped |
outcome, outcomeCode | Sonuç metni (serbest) ve sabit sonuç kodu (Kişi durumları) |
attempts, lastAttemptAt, nextAttemptAt, finishedAt | Deneme sayısı ve zamanlar; nextAttemptAt yalnızca yeniden denenecek kişide doludur |
callId | Son aramanın görüşme kimliği (GET/calls/{id}); henüz aranmadıysa null |
updatedAt | Kişi bilgisi ya da 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.
curl "https://zeslee.arcuslabs.co/api/v1/call-lists/12/contacts?updatedSince=2026-10-30T09:00:00Z&pageSize=100" \
-H "Authorization: Bearer zsl_..."updatedSince kullan.Kişileri listelere bağlama#
/api/v1/contacts/assign{ "contactIds": [1, 2, 3], "listIds": [12, 13] }/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/bulk | Toplu güncelleme |
POST/contacts/bulk-delete | { "ids": [...] } toplu silme |
POST/contacts/import | CSV içe aktarma |
POST/call-lists/:id/import | CSV'yi içe aktarır ve doğrudan bu listeye ekler |
Toplu ekleme#
/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 veexistingolarak 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.
/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.
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, phone | phone |
ad, isim, ad soyad, musteri, name | name |
firma, sirket, isletme, unvan, company | company |
not, aciklama, note | note |
id, kimlik, musteri no, referans | externalId |
| diğer sütunlar | serbest alan (fields) |
Başlık yoksa sıra telefon, ad, firma, not olarak alınır. Yüklemeden önce { "text": "...", "preview": true } göndererek sütun eşlemesini ve kaç kişi okunacağını görebilirsin; önizleme hiçbir şey yazmaz.
Rapor#
{
"total": 5, "created": 2, "updated": 0, "existing": 1, "assigned": 3, "skipped": 1, "failed": 1,
"errors": [{ "row": 4, "phone": "12", "reason": "Geçerli bir telefon numarası gerekli" }]
}| Alan | Anlamı |
|---|---|
created | Yeni oluşan kişi |
updated | Güncellenen kayıtlı kişi |
existing | Kayıtlı olduğu için değiştirilmeyen kişi |
assigned | Listelere eklenen üyelik |
removed | Silinen kişi (bulk-delete) |
skipped | Aynı istekte tekrar eden numara |
failed | Doğrulamadan geçemeyen satır |
errors | Her 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:
{ "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:
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.csvAynı anahtarla gelen ikinci istek işlenmez: ilk yanıt döner ve yanıtta Idempotent-Replay: true başlığı bulunur. Ağ hatasında isteği güvenle yeniden gönderebilirsin. Anahtarlar 24 saat saklanır; 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.
- Panelde Geliştirici sayfasında geliştirici ayarlarını etkinleştirip bir API anahtarı oluşturmuş olmalısın.
- Otomatik arama sekmesinde açılış cümlesini, arama amacını, arama saatlerini, günleri ve aramalar arası beklemeyi ayarla.
- Sisteminden POST
/outbound/contactsile kişileri gönder. Her kişi kuyruğun sonuna eklenir. - POST
/outbound/campaign/startile kuyruğu başlat. Asistan, arama saatleri içinde aynı anda en fazlaconcurrency(1–3) kişiyi arar; bir görüşme bitincegapSecondskadar bekleyip boşalan hatta sıradakini alır. - GET
/outbound/contactsve GET/outbound/reportile 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": [...] }.
{
"contacts": [
{
"phone": "0532 123 45 67",
"name": "Ahmet Yılmaz",
"company": "Yılmaz Ticaret",
"externalId": "crm-10234",
"note": "Geçen ay demo istemişti",
"fields": { "yas": 21, "paket": "Pro", "son_alisveris": "12 Ekim" }
}
]
}| Alan | Zorunlu | Sınır |
|---|---|---|
phone | evet | 10–13 karakter; boşluk ve tire temizlenir, +90… ve 90… yazımları 05XXXXXXXXX biçimine çevrilir. Yurt dışı numaraları + ile yaz |
name | hayır | en fazla 80 karakter |
company | hayır | en fazla 120 karakter |
note | hayır | en fazla 300 karakter |
externalId | hayır | en fazla 100 karakter; kendi sistemindeki kimlik |
fields | hayır | en fazla 20 alan; anahtar harf, rakam ve _ (en fazla 40 karakter); değer metin 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:
{
"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_outolarak girer, aranmaz veskippediçinde görünür. - Geçersiz numaralar
skippediçinde nedeniyle birlikte döner; diğer kişiler yine eklenir. statuskuyruğun durumudur (runningya dapaused). Kişi eklemek kuyruğu başlatmaz.
Açılış cümlesi ve şablonlar#
Panelde yazdığın açılış cümlesi ve arama amacı, her kişi için doldurulur. Yer tutucu %alan% ya da {{alan}} biçimindedir.
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
fieldsanahtarı da kullanılabilir:%yas%,%paket%,%son_alisveris%. - Büyük/küçük harf ve Türkçe karakter farkı gözetilmez (
%Şirket%,%sirket%ve%firma%aynı alanı bulur). - Karşılığı olmayan yer tutucu boş kalır; 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#
| İstek | Ne yapar |
|---|---|
GET/outbound/campaign | Ayarları, durumu ve kişi sayılarını verir |
POST/outbound/campaign/start | Kuyruğu başlatır |
POST/outbound/campaign/pause | Kuyruğu duraklatır; süren görüşme kesilmez |
Ayarlar (açılış cümlesi, amaç, saatler, günler, bekleme) panelde yapılır. GET/outbound/campaign yanıtı:
{
"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
pendingkalır. maxAttempts(1–3): bir kişiye en fazla kaç kez ulaşılmaya çalışılır. Cevap yoksaretryMinutes(30–1440) sonra yeniden denenir; öncelik hiç aranmamış kişilerdedir.- 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.
curl -X POST "$BASE/outbound/contacts" \
-H "Authorization: Bearer $ZESLEE_KEY" -H "Content-Type: text/csv" \
--data-binary @kisiler.csvtelefon;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
skippediçinde sayıları görünür.
Panelden elle açtığın bir arama listesine de aynı şekilde aktarabilirsin:
| İstek | Ne yapar |
|---|---|
POST/campaigns/{id}/contacts | Listeye 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.
| Durum | Anlamı |
|---|---|
pending | Sırada, henüz aranmadı |
calling | Şu an aranıyor |
answered | Telefon açıldı, görüşme yapıldı (15 saniyeden uzun) |
meeting | Görüşme sonunda randevu / toplantı alındı |
no_answer | Açılmadı ya da çok kısa sürdü; deneme hakkı varsa yeniden denenecek |
opted_out | Aranmak istemedi (görüşmede ya da listede); bir daha aranmaz |
failed | Arama başlatılamadı |
skipped | Deneme hakkı bitti, ulaşılamadı |
Sonuç kodu (`outcomeCode`). outcome serbest metindir; entegrasyonlar bunun yerine aşağıdaki sabit koda bağlanmalıdır. Liste üyeleri (GET/call-lists/:id/contacts) ve call_list.contact.status_changed olayında bulunur; pending kişide null'dır.
outcomeCode | Ne zaman | status |
|---|---|---|
calling | Şu an aranıyor | calling |
answered | Telefon açıldı, görüşüldü | answered |
meeting | Görüşmede randevu / toplantı alındı | meeting |
no_answer | Açılmadı ya da çok kısa sürdü; deneme hakkı varsa yeniden denenir | no_answer |
unreachable | Ulaşılamadı (operatör hatası: kapalı, meşgul vb.); deneme hakkı varsa yeniden denenir | no_answer |
retries_exhausted | Deneme hakkı bitti | skipped |
automated | Karşıdaki sesli menü ya da telesekreter; asistan kapattı, kişi yeniden aranmaz | skipped |
opted_out | Aranmak istemedi | opted_out |
start_failed | Arama başlatılamadı | failed |
Kişi nesnesi:
{
"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#
| İstek | Ne yapar |
|---|---|
GET/outbound/contacts | Kişi listesi. Süzgeçler: status, externalId, limit (1–500, varsayılan 100), offset |
GET/outbound/contacts/{id} | Tek kişi, görüşme özeti ve dökümüyle |
DELETE/outbound/contacts/{id} | Kişiyi kuyruktan çıkarır. Yalnızca pending olanlar silinir, diğerleri için 409 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-31 | Kuyruk raporu (varsayılan son 30 gün) |
Rapor yanıtı:
{
"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:
| İstek | Ne yapar |
|---|---|
GET/calls | Gö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/summary | Toplam, 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.
# 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"| Durum | Yanıt |
|---|---|
| Kayıt var | 200, ses dosyası (audio/mpeg) |
| Görüşme senin işletmenin değil ya da yok | 404 |
| Kayıt tutulmamış ya da saklama süresi dolup silinmiş | 404 |
| Kayıt henüz işlenmedi | 404 ("henüz hazır değil"); call.recording_ready olayını bekle |
| Dakikada 20'den fazla indirme | 429 ve Retry-After |
Güvenlik: yalnızca kendi işletmenin görüşmelerine erişilir; yanıt private, no-store ile gelir (ö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 gelir | Sorduğun anda | Olay olduğu an |
| İstek sayısı | Çoğu boş döner | Yalnızca olay olunca |
| Kaçan olay riski | Aralığı yanlış seçersen | Yeniden deneme ve olay geçmişi korur |
| Sınır | Dakikada 120 istek | Yok |
Müşteri asistanı arar ──► görüşme biter ──► 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 denenirWebhook'lar işletmeye özeldir: yalnızca kendi işletmenin olayları gelir. Panelde Geliştirici → Webhook'lar sekmesinden ya da bu sayfadaki API'den yönetilir. Çalışması için geliştirici ayarlarının açık olması yeterlidir; giden arama onayı ya da özel bir plan gerekmez. Geliştirici ayarlarını kapatırsan yeni olay ü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.
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"]
}'{
"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.
curl -X POST https://zeslee.arcuslabs.co/api/v1/webhooks/12/test \
-H "Authorization: Bearer zsl_..."{ "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/webhooks | Webhook'lar, olay kataloğu ve teslimat kuralları |
POST/webhooks | Webhook ekler (201). Yanıtta secret bir kez görünür |
GET/webhooks/:id | Tek webhook |
PATCH/webhooks/:id | url, description, events, active alanlarından gönderdiğini günceller |
DELETE/webhooks/:id | Webhook'u ve teslimat geçmişini siler |
POST/webhooks/:id/rotate-secret | İmza anahtarını yeniler. Gövde: { "graceHours": 24 } (isteğe bağlı, 0–168; varsayılan 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/deliveries | Teslimat geçmişi: limit (en fazla 100), before, status (pending, success, failed) |
POST/webhook-deliveries/:id/retry | Başarısız ya da bekleyen teslimatı hemen yeniden dener |
GET/webhook-events | Olay geçmişi (son 7 gün): type, limit, before |
GET/webhook-events/:id | Tek olay (gönderilen zarfın aynısı) |
POST/webhook-events/:id/resend | Olayı seçilen webhook'a yeniden gönderir: { "webhookId": 12 } |
| Alan | Açıklama |
|---|---|
url | Zorunlu. https:// ile başlamalı, en fazla 500 karakter. Kullanıcı adı/parola ve # parçası içeremez. Herkese açık bir alan adı ya da IP'ye işaret etmeli |
events | Abone olunan olaylar. ["*"] ya da boş: hepsi. ["call.*"]: önek. ["call.completed", "order.created"]: tek tek. Bilinmeyen tür 400 döner |
description | İsteğe bağlı, en fazla 200 karakter |
active | false yapınca olay gönderilmez; yeniden true yapınca sayaçlar sıfırlanır |
includeTranscript | Varsayılan true. false yapınca call.completed olayında konuşma dökümü hiç gönderilmez, yalnızca özet gider (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.xve 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.
v1 değerine bakıyorsa (yeni anahtarı henüz bilmiyorsa) geçiş süresinde doğrulama tutmaz. Alıcını önce tüm v1 değerlerine bakacak şekilde güncelle.Örnek olay gönderme#
POST/webhooks/:id/test ile seçtiğin olay türünden örnek yük gönderebilirsin; böylece gerçek bir görüşme ya da randevu beklemeden alıcı kodunu denersin:
curl -X POST https://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": truebulunur. Örnek olayları CRM'ine kaydetme: bu alana bakıp atla. - Webhook o türe abone olmasa da gönderilir.
includeTranscript: falseise örnekcall.completed'de de döküm olmaz. - Örnek olaylar olay geçmişinde listelenmez.
eventTypeverilmezsewebhook.testgider.
- Kapalı bir webhook'a olay gönderilmez, ama olay geçmişine yazılır. Webhook'u yeniden açınca kaçanları Olay geçmişi'nden gönderebilirsin. 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.
{
"id": "evt_9f2c41b7a0d3e58c6b1f2a90",
"type": "call.completed",
"apiVersion": "1",
"createdAt": "2026-10-30T09:16:31.402Z",
"businessId": 3,
"data": { }
}| Alan | Açıklama |
|---|---|
id | Olayın kimliği. Aynı olay yeniden denemelerde aynı `id` ile gelir; tekilleştirmek için bunu kullan |
type | Olay türü, örn. call.completed |
apiVersion | Zarf sürümü. Bugün "1". Geriye uyumsuz değişiklik olursa yeni sürüm numarası verilir; yeni alan eklemek uyumsuz sayılmaz, bu yüzden bilmediğin alanları yok say |
createdAt | Olayın oluştuğu an (ISO 8601, UTC) |
businessId | Olayın ait olduğu işletme |
sample | Yalnızca örnek olaylarda bulunur ve true'dur (Örnek olay gönderme). Gerçek olaylarda bu alan yoktur |
data | Olaya özgü veri; her olay için Olay türleri'nde |
İstek başlıkları
| Başlık | Değer |
|---|---|
Content-Type | application/json |
User-Agent | Zeslee-Webhooks/1.0 |
Zeslee-Event-Id | Zarftaki id ile aynı |
Zeslee-Event-Type | Zarftaki type ile aynı. Gövdeyi okumadan yönlendirmek için kullan |
Zeslee-Delivery-Id | Bu teslimatın kimliği |
Zeslee-Attempt | Kaçıncı deneme: 1, 2, ... |
Zeslee-Timestamp | İmzanın üretildiği an (Unix saniye) |
Zeslee-Signature | t=<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:
Zeslee-Signaturebaşlığındant(Unix saniye) ve tümv1(imza) değerlerini al. Normalde birv1vardır; anahtar yenileme geçiş süresinde iki tane gelir (önce yenisi, sonra eskisi).- İmzalanan metin:
t+.+ ham gövde (gelen baytlar, hiç değiştirmeden). - Beklenen imza: bu metnin, webhook'un
whsec_...anahtarıyla HMAC-SHA256 özeti, küçük harfli onaltılık. v1değerlerinden biri beklenen imzayla tutuyorsa istek geçerlidir; karşılaştırmayı sabit zamanlı yap.tdeğeri şimdiden 5 dakikadan eskiyse isteği reddet (kaydedilmiş bir isteğin sonradan tekrar gönderilmesine karşı).
Yeniden denemelerde her deneme için yeni t ve imza üretilir; imzayı saklayıp karşılaştırma.
Node.js (Express)
import crypto from "node:crypto"
import express from "express"
const SECRET = process.env.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)
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 "", 200PHP
<?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)
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.
| Deneme | Bir önceki başarısız denemeden sonra bekleme |
|---|---|
| 1 | hemen |
| 2 | 30 saniye |
| 3 | 2 dakika |
| 4 | 10 dakika |
| 5 | 30 dakika |
| 6 | 2 saat |
| 7 | 6 saat |
| 8 | 12 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
200dön; işi sonra yap. Yavaş yanıt zaman aşımına, tekrara ve sonunda kapanmaya yol açar. - Tekilleştir. Işlediğin
iddeğerlerini sakla. Aynıidtekrar gelirse200dön ama yeniden işleme. - Sıraya güvenme. Olaylar üretildikleri sırayla gitmeyebilir (bir olay yeniden denenirken sonraki teslim edilir). Sıra gerekiyorsa zarftaki
createdAtya da nesnenin kendi zamanını (startedAt,updatedAt...) kullan. - Bilinmeyen alanları ve olay türlerini yok say. Zamanla yeni alan ve olay eklenir. Tanımadığın türe
200dön. - IP izin listesine güvenme. Gönderim IP'leri değişebilir. Doğrulama için imzayı kullan.
- Anahtarı korumak senin işin. Sızdığından şüphelenirsen
rotate-secretile yenile.
Veri saklama. Gönderilen olaylar 7 gün 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ür | Ne zaman |
|---|---|
call.completed | Bir görüşme bitti (gelen/giden; açılmayan ve başarısız aramalar dahil) |
call.recording_ready | Görüşmenin ses kaydı hazır oldu; kayıt olaydaki yoldan API anahtarınla indirilir (Ses kayıtları) |
appointment.created | Asistan yeni bir randevu oluşturdu |
appointment.cancelled | Randevu iptal edildi (panelden, müşterinin mesajıyla ya da asistan tarafından) |
appointment.confirmed | Müşteri randevusunu mesajla onayladı |
order.created | Asistan yeni bir sipariş aldı |
order.status_changed | Siparişin durumu değişti |
customer.created | Yeni müşteri ya da potansiyel müşteri kaydı açıldı |
customer.updated | Müşteri kaydı güncellendi (durum, ad, e-posta, etiket...) |
message.sent | SMS, WhatsApp ya da e-posta gönderildi |
message.failed | Bir mesaj gönderilemedi |
message.received | Müşteriden SMS ya da WhatsApp yanıtı geldi |
call_list.contact.status_changed | Arama listesindeki bir kişinin durumu değişti |
call_list.status_changed | Bir arama listesi başladı, durdu ya da tamamlandı |
do_not_call.added | Numara aranmayacaklar listesine eklendi (panelden ya da müşteri görüşmede istediği için asistan ekledi) |
do_not_call.removed | Numara aranmayacaklar listesinden çıkarıldı (panelden) |
webhook.test | Deneme olayı (yalnızca sen istediğinde) |
Aşağıdaki örneklerde yalnızca data gösterilir; zarf Olay zarfı'ndaki gibidir. Zamanlar ISO 8601 UTC'dir; randevu tarihi ve saati işletmenin saat dilimindedir (timezone).
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.
{
"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 }
]
}
}| Alan | Açıklama |
|---|---|
id | Görüşme kimliği (GET/calls/{id} ile aynı) |
startedAt | Görüşmenin başladığı an |
direction | inbound (gelen) ya da outbound (giden); bilinmiyorsa null |
phone | Karşı tarafın numarası (05321234567 biçiminde); gizli numara ise null |
customerName | Müşteri kaydındaki ad; yoksa null |
durationSecs | Konuşma süresi (saniye) |
answered | Süre 15 saniye ve üstü ise true |
result | answered (açıldı), no_answer, busy, voicemail (telesekreter ya da sesli menü), failed |
status | Görüşmenin durumu (done, failed) |
success | Asistanın görüşme değerlendirmesi: success, failure, unknown |
title | Kısa başlık |
terminationReason | Görüşmenin neden bittiği (ses altyapısının verdiği metin) |
summary | Görüşme özeti; yoksa null |
campaign | Gö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ı) |
transcript | Konuş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.
{
"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" }]
}
}
}| Alan | Açıklama |
|---|---|
callId | Randevunun 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 |
contact | Randevudaki 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 |
code | Müşterinin bildiği randevu kodu (işletme içinde benzersiz) |
status | confirmed ya da cancelled |
staff | Personel atanmadıysa null |
notifyChannel | Müşterinin bilgilendirme tercihi: sms, whatsapp ya da null |
customerConfirmed | Müşteri mesajla onayladıysa true |
order.created, order.status_changed#
{
"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#
{
"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#
{
"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"
}
}| Alan | Açıklama |
|---|---|
channel | sms, whatsapp, email |
direction | out (gönderilen) ya da in (müşteriden gelen) |
kind | confirmation (onay), reminder (hatırlatma), cancellation (iptal), order, inbound (müşteri yanıtı), reply (otomatik yanıt)... |
status | sent, simulated (test ortamı; gerçek gönderim yok), failed, received |
error | Yalnı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ç).
{
"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
callingiken ya da özet henüz yoksanull'dır. Özet sonradan oluşursa bu olay yeniden gelmez; özeticall.completedolayı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ı). Kodastatusve `outcomeCode` ile bağlan;outcomeCodesabit bir listedir (Kişi durumları). - Yeniden deneme: açılmayan kişi
pending'e dönmez. Deneme hakkı varkenstatusno_answerkalır venextAttemptAtbir sonraki deneme zamanını gösterir; hak bitinceskippedolur.
call_list.status_changed#
{
"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#
{
"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#
{ "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:
curl "https://zeslee.arcuslabs.co/api/v1/webhooks/12/deliveries?status=failed" \
-H "Authorization: Bearer zsl_..."{
"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:
# 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.
Sorun giderme#
| Belirti | Olası neden ve çözüm |
|---|---|
Webhook eklerken 400 url https:// ile başlamalı | Yalnızca https desteklenir. Geliştirme için ngrok/Cloudflare Tunnel gibi bir https tüneli kullan |
400 url herkese açık bir adres olmalı | localhost, özel ağ ve iç adresler reddedilir. Adres internetten erişilebilir olmalı |
Deneme olayında Beklenen 2xx, gelen 401/403 | Güvenlik duvarı ya da kimlik doğrulama 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ümlenmedi | Alan adı özel bir IP'ye işaret ediyor. DNS kaydını herkese açık IP'ye çevir |
| TLS ya da sertifika hatası | Sertifika geçerli, süresi dolmamış ve zinciri tam olmalı. Kendi imzaladığın sertifika kabul edilmez |
| İmza hep tutmuyor | Ham gövdeyi değil çözülüp yeniden yazılmış JSON'u imzalıyorsun; ya da yanlış (eski) whsec_ anahtarı. Anahtar yenilendiyse yenisini kullan |
| Webhook kendiliğinden kapandı | Art arda 5 olay teslim edilemedi. Adresi düzelt, active: true yap, kaçanları geçmişten yeniden gönder |
call.completed geç geliyor | Görüşme, ses altyapısı kaydı işleyince tamamlanır (genellikle 1–3 dakika). Daha uzun sürerse destek merkezinden yaz |
| Hiç olay gelmiyor | Webhook açık mı, olay seçili mi (events), geliştirici ayarları açık mı? Deneme olayı gelip gerçek olay gelmiyorsa abonelikleri kontrol et |
| Aynı olay iki kez geldi | Beklenen davranış (en az bir kez). id ile tekilleştir |
Hatalar#
Hata yanıtı her zaman { "error": "..." } biçimindedir; error Türkçe, kullanıcıya gösterilebilir bir cümledir.
| Kod | Ne zaman |
|---|---|
400 | Gö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 |
401 | Anahtar geçersiz ya da iptal edilmiş |
402 | Plan giden aramayı kapsamıyor |
403 | Geliştirici ayarları kapalı ya da giden arama onaylı değil |
404 | Liste, kişi, numara ya da kuyruk kişisi bulunamadı (başka işletmenin kaydı da 404 döner) |
409 | Aynı ad ya da telefon zaten var; en fazla 10 webhook; zaten teslim edilmiş olay yeniden gönderilemez; çalışan liste silinemez; kişi şu an aranıyor ya da aranmış; otomatik arama ayarlanmamış |
429 | Dakikada 120 istek aşıldı; Retry-After kadar bekle |
Ücretlendirme#
Giden aramalar da diğer görüşmeler gibi dakika başına ücretlendirilir; sabit ücret yoktur. İşletmenin aylık tavanı dolunca yeni arama başlatılmaz ve kuyruk bekler. 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#
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+)#
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#
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)#
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)#
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ı#
[
{
"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
externalIdver. Aynı kişiyi tekrar göndermek güvenlidir; çift arama olmaz. fieldsiçine yalnızca açılış cümlesinde ya da görüşmede işe yarayacak bilgiyi koy. Kişisel veri gerektirmeyen alanları ekleme.skippedboş değilse nedenini kullanıcıya bildir; aynı numarayı yeniden gönderip durma.429gelirseRetry-Afterkadar bekle. Sonuçları saniyede bir sorgulama; görüşmeler dakikalar sürer. Birkaç dakikada bir GET/outbound/contacts?status=...yeterlidir.opted_outolan kişiyi bir daha gönderme.402,403,409hataları ajanın çözebileceği şeyler değildir (plan, onay, ayar). Hatayı olduğu gibi kullanıcıya ilet.
Önerilen akış#
- İsteğe bağlı: GET
/outbound/campaignile kuyruğun hazır olduğunu ve durumunu kontrol et. - Kişileri 500'erli gruplar halinde POST
/outbound/contactsile gönder. - Kullanıcı onayıyla (ya da panelden) kuyruğu başlat.
- Birkaç dakikada bir GET
/outbound/contacts?status=meetingvestatus=answeredile sonuçları topla.outcomealanı görüşmenin tek cümlelik sonucudur; ayrıntı için GET/outbound/contacts/{id}ile özet ve dökümü al. - Günün sonunda GET
/outbound/reportile özet çıkar.