Kime: DoWaba panelini kendi uygulamasından (headless) kullanan geliştirici partner'lar. Özellikle Trusted Partner OAuth ile bağlanıp kullanıcı adına panel API'larını çağıranlar için.
Bu doküman neden var: API Dokümantasyonu (
/api-docs/) endpoint'leri API grubuna göre dizer — eksiksiz referans ama "panelde gördüğüm şeyin API karşılığı ne?" sorusuna doğrudan cevap vermez. Bu rehber panelin sol menüsünü takip eder: her menü için → ne işe yarar · paneldeki form/alanlar · hangi endpoint · örnek istek · örnek yanıt. İkisi birbirini tamamlar:
İhtiyaç Kaynak "Hangi menü hangi endpoint, akış nasıl?" Bu doküman "Bu endpoint'in tam validasyon alanları neler?" API Dokümantasyonu — her endpoint'te alan listesi Postman / OpenAPI 3 /api-docs/collection.json·/api-docs/openapi.yamlÇalışan örnek projeler Örnek Projeler Toplu gönderim / pazarlama uyum kuralları § 0.6 — opt-out + İYS + KVKK (ZORUNLU) Görsel/video için otomatik sosyal yayın kuyruğu § 5.4 — tek bundle, kanal metinleri, döngü, Threads/X, SSS Olay push'u + teslimat takibi (webhook) § 10 — message.received·message.status·appointment.created·appointment.status_changed· imza ·site_idkapsamı ·full_payloadAPI'lerde ne değişti? (yeni endpoint / kural değişikliği) Panelde: Geliştirici → Sürüm Notları (canlı, sürümlü) — bkz. § 13
Entegrasyona başlamadan önce şu tek soruya cevap ver: müşterin DoWaba'yla hiç yüz yüze gelecek mi?
| Senaryon | Doğru model | Kullanacağın anahtar |
|---|---|---|
| Müşterilerin DoWaba'yı hiç görmeyecek; kaydı, botu, içeriği kendi sisteminden arka planda sen yöneteceksin | Tek hesap + müşteri başına SİTE — her müşteri için POST /api/sites (§ 1) |
Kişisel API anahtarı (panel → Geliştirici → API Anahtarları, § 10) |
| Müşterilere ayrı DoWaba hesabı vereceksin (istersen kendi markanla panel girişi de) — yetki/fatura ayrımı istiyorsun | Bayi modeli: müşteri başına ALT HESAP — POST /api/reseller/customers + /{user}/sites (§ 12) |
Yine kişisel API anahtarın |
| Kullanıcılar SENİN uygulamana kendi DoWaba hesaplarıyla giriş yapacak ("Login with DoWaba") | Trusted Partner OAuth (§ 0) | OAuth token yanıtındaki sanctum_token |
Karar verirken bil:
client_id bir token DEĞİLDİR — yalnız OAuth giriş akışında uygulamanı tanıtır, Authorization
header'ına asla yazılmaz. İlk iki senaryoda OAuth Uygulamaları sekmesiyle hiç işin olmaz.POST /api/reseller/customers/{user}/login-link (§ 12). Müşteri konuşmalarını kendi sisteminden
okuman gerekiyorsa siteleri kendi hesabında aç (ilk satır) — kendi sitende kısıt yok.PUT /api/sites/{id}
(settings.system_prompt), bilgi tabanı POST /api/sites/{id}/faqs/batch, müşteri sitesine chat
için tek satır widget script'i (§ 1).Senin uygulaman kendi users tablosunu tutmuyor; kullanıcılar DoWaba'nın user havuzunda.
Uygulaman, kullanıcı adına DoWaba'nın panel API'larını (sites, conversations, contacts…) çağırıyor —
sanki kullanıcı DoWaba'nın resmi mobil uygulamasındaymış gibi. Bunu sağlayan şey sanctum_token.
issues_sanctum_token açık olmalı. İki yol var:
(a) Bayiysen kendin açarsın — Geliştirici → OAuth uygulamaları → Güvenilir Ortak (§ 10);
kapsam daima tenant'tır, yani yalnız senin evrenindeki kullanıcılar giriş yapabilir.
(b) platform kapsamı (evren dışı kullanıcılar da girebilsin) yalnız DoWaba ekibi
tarafından, sözleşmeyle açılır.POST /api/oauth/token çağırır (grant_type=authorization_code).{
"access_token": "doat_...",
"id_token": "eyJ...",
"refresh_token": "dort_...",
"expires_in": 3600,
"scope": "openid profile email",
"sanctum_token": "12|abc...", // ← panel API'ları için BU
"sanctum_token_type": "Bearer"
}
sanctum_tokengelmiyorsa dört sebep olabilir:
- client trusted partner değil (
issues_sanctum_tokenkapalı);- kullanıcı superadmin (superadmin trusted partner app'lere giremez — onay ekranında bloklanır);
- uygulama
tenantkapsamlı ve kullanıcı sahibin evreninde değil (onay ekranıout_of_scope_reason: "tenant_scope"döner);- uygulama sahibinin kapısı düştü — bayilik / ödeme / güncel sözleşme koşullarından biri bozulunca token üretimi durur (
out_of_scope_reason: "owner_gate", § 10).⚠️ (3) ve (4) her authorize'da yeniden değerlendirilir: dün token alan kullanıcı bugün alamayabilir.
curl https://dowaba.com/api/sites \
-H "Authorization: Bearer 12|abc..." \
-H "Accept: application/json"
https://dowaba.com/apiAccept: application/json ŞART — yoksa auth hatası HTML/redirect dönebilir (302/500 görürsün). Her zaman gönder.reseller_id dolu), sanctum_token ile konuşma/mesaj endpoint'leri
çalışmaz — inbox/unified, {kanal}/conversations, {kanal}/messages/..., inbox/media,
voice transkriptleri, widget sohbetleri boş liste veya 403 döner. Fallback YOKTUR. Gerekçe:
bu token senin (partner'ın) elindedir; bayi müşterisinin konuşmaları yalnız kendi panel
oturumuna açıktır. Yönetim endpoint'lerinin çoğu (site/profil/entegrasyon/ayarlar/şablon/
kampanya oluşturma) çalışmaya devam eder — ama mesajlaşma kapsamı üzerinden çalışan birkaç uç
bu hesaplarda da kapalıdır: message-opt-outs (liste boş, ekleme 403), contacts/quick-add
(site bağlamı verilirse 403), contacts/lookup-by-channel ({ "contact": null }). Bu hesaplarda
red (opt-out) kaydını kendi tarafında tut ve gönderim listesini oradan süz; DoWaba yine de
gönderim anında filtreler (§ 0.6). Bağımsız (bayiye bağlı olmayan) kullanıcılar için kısıt yok.sanctum_token'ın süresi dolmaz; yalnızca OAuth revoke veya refresh rotation ile silinir
(mobil "logout" = POST /api/oauth/revoke).GET /api/auth/user # oturum açan kullanıcı: id, name, email, role, reseller_id,
# allowed_modules, assigned_site_id, is_agent_only ...
GET /api/user # profil detayları
allowed_modules alanı kritik: kullanıcı kısıtlıysa (sub-user / bayi müşterisi / agent) bu bir whitelist
(["whatsapp","mail",...]). null ise tüm modüller açık. Bir menü/endpoint'i göstermeden önce buna bak.
{ "success": true, "data": ... } döner; bazı liste endpoint'leri data / profiles
anahtarı altında, bazı konuşma listeleri kök array döner. Her endpoint'in şeklini ilk çağrıda doğrula.401 (token yok/geçersiz), 403 (yetki/scope dışı), 402 (slot/abonelik gerekiyor — site açma),
422 (validasyon), 429 (rate limit).10/dk, outbound çağrı 5/dk, AI iyileştir 20/dk).
Her bölümde belirtildi.Geliştirici anahtarıyla (panelde Geliştirici > API Anahtarları'ndan üretilen dev: token'lar ve
Trusted Partner oauth- token'ları) yapılan tüm istekler kullanıcı başına tek dakikalık sayaçta
toplanır — genel sayaç uç başına ayrışmaz. Bazı uçlarda ayrıca kendine ait adlı bir ek limit vardır
(genel sayaca ek olarak işler; hangi uçta ne olduğu ilgili bölümünde yazar).
Panel/mobil oturum trafiği bu sayaca girmez.
| Hesap | Limit (istek/dk) |
|---|---|
| Business | 60 |
| Professional | 120 |
| Premium | 300 |
| Ultimate | 600 |
| Bayi | 300 + aktif müşteri × 30 (tavan 1.500) |
| Bayi müşterisi | 60 |
X-RateLimit-Limit + X-RateLimit-Remaining header'ları döner; aşımda
429 { "error": "rate_limited", "limit": ..., "retry_after": <sn> } + Retry-After header'ı.
Retry-After kadar bekleyip yeniden dene — agresif retry döngüsü kurma.GET /api/me/rate-limit
# → 200 { "unlimited": false, "limit": 120, "used": 34, "remaining": 86,
# "window_seconds": 60, "source": "plan" }
# source: plan | reseller | reseller_customer | override | default
# | exempt (superadmin) | disabled (limit kapalı)
⚠️ Bu uç
developermodülü gerektirir: kısıtlı hesaplarda (bayi müşterisi / alt kullanıcı / agent —allowed_modulesiçindedeveloperyoksa)403döner. O hesaplarda limiti her yanıttakiX-RateLimit-Limit/X-RateLimit-Remainingheader'larından oku. Son ikisourcedeğerindeunlimited: true+limit: nullgelir — istemci bilinmeyen birsourcegördüğünde "limitsiz" varsaymak yerineunlimitedalanına bakmalı.
API erişimin var ama kullanıcıyı tarayıcıda panele sokmak istiyorsan (örn. kendi arayüzünde "Panele Git" butonu):
POST /api/oauth/login-link
# Bearer: sanctum_token (yalnız 'oauth-' Trusted Partner PAT; panel/'api-token' → 403)
# → 200 { "success": true, "url": "https://<marka-host>/admin/impersonate?token=...",
# "expires_at": "...", "expires_in_seconds": 300 }
# → 403 token Trusted Partner değilse · 422 pasif/askıdaki hesap
url'i kullanıcının tarayıcısında aç. Tek-kullanımlık + 5 dk TTL; POST /api/auth/impersonate-redeem
ile redeem edilir (değişmedi). Reseller paneldeki customers/{user}/login-link'in OAuth karşılığıdır.url host'u markaya göre döner (msg724.com / bayi özel domain / dowaba.com). Mimari: OAUTH_PROVIDER.md § 6.1.inbox/unified, {kanal}/messages/..., inbox/media, contacts gibi endpoint'ler WhatsApp / Instagram / Messenger
mesaj içeriği + telefon + isim + medya (Meta Verisi) döndürür. Bu veriyi çekmek = Meta Verisi'nin 3. tarafça işlenmesi →
Meta Platform Terms + WhatsApp Business Solution Terms'e tabidir. Kurallar (DoWaba Partner Sözleşmesi ile bağlayıcı):
me/developer-terms), (2) son kullanıcının açık onayı (OAuth consent). Onaylanmayan veri kategorisine erişme.Özet: "İşletme kendi DoWaba verisi için senin aracını kullanıyor" = uygun. "Sen DoWaba verisiyle kendi ürününü besliyorsun" = ihlal.
scheduled-jobs, contacts/groups/{id}/send-template, mail-campaigns gibi toplu / kampanya gönderim
endpoint'lerini çağırıyorsan Türkiye mevzuatı (ETK 6563 + İYS + KVKK) senin entegrasyonun için de geçerlidir —
panelden manuel gönderim ile API üzerinden gönderim aynı hukuki yükümlülüğe tabidir. Üç kural:
1. Red (opt-out) listesi — DAİMA uygula. Alıcı "DUR / STOP / İPTAL" yazdıysa veya elle çıkarıldıysa kanal-bağımsız
suppression listesine (message_opt_outs) düşer. Bu kişilere gönderim yapılamaz.
scheduled-jobs (zamanlı) hem send-template (anlık toplu) yolları opt-out + İYS reddini otomatik filtreler
(red'li alıcı gönderilmez, kotadan düşmez). Sonucu nerede göreceğin yola göre değişir:
send-template (anlık) yanıtında skipped sayısı + skipped_samples (ilk 10 örnek) + skipped_reasons döner.
scheduled-jobs (zamanlı) asenkron çalışır → oluşturma yanıtında atlananlar YOKTUR; işi
GET /api/scheduled-jobs ile izle: atlananlar iş kaydındaki total_skipped alanında birikir,
sebep kırılımı çalışma log'una yazılır.call): alıcı kampanya araması sırasında "beni bir daha aramayın" derse
sesli bot bunu otomatik kaydeder (channel=call opt-out) ve çağrıyı kibarca kapatır; voice-campaigns o numarayı
sonraki aramalarda arama anında atlar (alıcı skipped, skip_reason: opt_out — § 5).GET /api/message-opt-outs?site_id=12&channel=whatsapp
ile listeyi çekebilirsin.2. İYS onayı — pazarlamada ön onay zorunlu. Ticari/pazarlama içerikli toplu mesaj için alıcının önceden onayı
iys/consents ile yükle/sorgula (§ 4). Onaysız liste =
hukuka aykırı + Meta ban riski. (Mevcut müşteriye onaysız kampanya da yasaktır — istisna yalnız değişiklik/bakım bildirimidir.)3. KVKK aydınlatması — botu kapatırsan SEN sorumlusun. Bir konuşmada toggle-bot ile AI'yı kapatıp kendi
uygulamandan cevaplıyorsan, DoWaba'nın otomatik KVKK aydınlatması (son alıcının ilk mesajında gönderilen) devreye
girmez — aydınlatma AI cevabına bağlıdır. Bu durumda son alıcıya KVKK aydınlatmasını sen sağlamak zorundasın
(veri sorumlusu = DoWaba hesap sahibi; sen onun adına işleyensin → Partner Sözleşmesi).
Red (opt-out) listesi endpoint'leri:
GET /api/message-opt-outs?site_id=12&channel=whatsapp # red listesi — channel: whatsapp | sms | mail | call
POST /api/message-opt-outs { "site_id": 12, "channel": "call", "identifier": "+905551112233", "reason": "telefonla beni aramayın dedi" }
DELETE /api/message-opt-outs/{id}
identifier= telefon (whatsapp/sms/call, E.164) veya e-posta (mail) — sistem normalize eder. Yanıt sayfalıdır (data, total, current_page, last_page).channel=callsesli arama kampanyalarının (§ 5) red listesidir; kaydı silmek numarayı tekrar aranabilir yapar.⚠️ Bayi müşterisi hesabında bu uçlar kapalıdır (partner token'ı ile): liste boş döner,
POST403verir (§ 0.2). O hesaplarda red listesini kendi tarafında tutmak senin sorumluluğundadır.
DoWaba sol menüsündeki sırayla:
| Sol menü | Bölüm | Ana endpoint(ler) |
|---|---|---|
| Siteler | §1 | sites, sites/{site}/faqs, sites/{site}/functions |
| Mesaj Kutusu (Inbox) | §2 | inbox/unified, inbox/mark-read, inbox/media/... |
| Kanallar (WhatsApp · Telegram · Messenger · Instagram · X · TikTok · Mail · Widget · Google Yorumlar) | §3 | {kanal}/conversations, {kanal}/messages/..., {kanal}/send |
| Rehber & WP Kampanya | §4 | contacts/groups, contacts/..., scheduled-jobs |
| Şablonlar / Kampanyalar (+ Çağrı Kampanyası) | §5 | whatsapp/templates, mail-templates, mail-campaigns, voice-campaigns |
| Paylaşım (çok kanallı gönderi + otomatik kuyruk) | §5.4 | social-publishing/capabilities, social-publish-queues, threads-profiles |
| Reklam Yönetimi (Meta Ads) | §5.5 | ads/accounts, ads/studio/*, ads/campaigns, ads/insights, ads/guard |
| Çağrılar (Voice) + Outbound + Santral | §6 | sites/{site}/voice-conversations, voice/call, sites/{site}/transfer-targets, outbound-intents, outbound-messages |
| Müşteri Talepleri (Callback) | §7 | callback-requests |
| Potansiyel Müşteriler (Leads / CRM) | §7.5 | leads, leads/scan, leads/{id}/stage, leads/{id}/assign, leads/bulk-assign, leads/{id}/activities |
| Bilgi Tabanı (site içi: SSS + SSS Stüdyosu) | §8 | sites/{site}/faqs, sites/{site}/faq-universe/*, sites/{site}/playground/* |
| Kullanım & Krediler / Abonelik | §9 | me/usage, credits, subscriptions/*, ai-credit/* |
| Geliştirici (kendi token/webhook/oauth + ChatGPT MCP) | §10 | me/tokens, webhook-endpoints, me/oauth/clients, https://dowaba.com/mcp |
| Profil & Ayarlar | §11 | auth/user, user, settings |
İşletme Sayfası (dowaba.com/isletme/{slug}) |
§8.5 | sites/{site}/business-page, .../chat, .../publish, .../checkout |
| İş Ortakları ("Uzman Bul") | §12 | experts, experts/{slug}/inquiries, reseller/expert-profile |
| Faturalarım / Bayi (+ Referans Programı) | §12 | me/billing, reseller/*, reseller/referral/* |
⚠️ İşletme Sayfası ve İş Ortakları kısıtlı hesaplarda yoktur: bayi müşterisi / alt kullanıcı / agent bunları menüde görmez ve API'da
403 restricted_accountalır (modül anahtarı YOK — kural controller'da; menü gizlemek tek başına yeterli değildir).Sol menüde ayrı maddesi olmayan ama API'sı olan iki aile: Sipariş Bildirimleri (Siteler → Entegrasyonlar altında — §5.6) ve Cevapsız çağrı → WhatsApp takibi (Çağrılar altında — §6).
Randevu — Dış API (§7.6, 2026-08-08 YENİ): panel randevu uçları hâlâ kapsam dışıdır, ama server-to-server entegrasyon için siteye kilitli
dse_anahtarla çalışan ayrı bir yüzey vardır:/api/external/appointments/*. Bearer PAT gerektirmez, yalnız randevu alanını açar.Atlananlar / kısmi (2026-08-03 güncellendi): Randevu PANELİ (ayrı modül, site'ta
appointmentmodülü gerektirir), Trendyol Q&A (trendyol/*), e-ticaret mağaza bağlama akışları (ikas · Shopify · İdeaSoft OAuth — panelden yapılır; sipariş bildirimi tarafı §5.6'da). İhtiyaç olursa aynı şablonla eklenir.
Ne işe yarar: Bir "site" = bir AI asistan yapılandırması (sistem promptu, dil, widget ayarı, bağlı kanallar).
Her şeyin merkezi; çoğu endpoint sites/{site}/... altındadır.
Paneldeki form (Yeni Site): ad, açıklama, sistem promptu, dil(ler), (opsiyonel) domain.
GET /api/sites
{ "success": true, "sites": [ { "id": 12, "name": "Örnek Mağaza", "is_active": true,
"api_key": "...", "is_owner": true, ... } ] }
⚠️ Liste
sitesanahtarındadır (datadeğil) —data'ya bakan istemci boş liste görür, JSON hatası bile almaz.?messaging_scope=1eklenirse bayi müşterisine ait siteler listeden çıkar.
GET /api/sites/{site}
POST /api/sites
Content-Type: application/json
{ "name": "Örnek Mağaza", "languages": ["tr"], "domain": "magaza.com",
"bot_name": "Asistan", "welcome_message": "Merhaba! Nasıl yardımcı olabilirim?",
"widget_color": "#4f46e5" }
# → 201 { "success": true, "site": { ... }, "api_key": "..." }
402
{ "success": false, "requires_paid_site": true, "price": 3999, "period": "yearly", ... } →
aynı isteği "confirm_paid_site": true ile tekrarla, yıllık lisanslı site açılır.403 requires_upgrade.description ve settings oluşturmada YOK SAYILIR — sistem promptu otomatik üretilir.
Özelleştirmek için siteyi açtıktan sonra PUT /api/sites/{site} ile settings.system_prompt
gönder (max 50.000 karakter — bu sınır yalnız PUT için geçerlidir).languages (settings.languages değil; 44 dil whitelist).domain verirsen site_domains satırı doğrudan doğrulanmış (verified) yaratılır —
ayrıca POST /api/domains/{domain}/verify çağırmana gerek kalmaz.Bayi, müşteri adına site açar:
POST /api/reseller/customers/{user}/sites(§12).
PUT /api/sites/{site} # ad, sistem promptu, ayarlar (settings.widget_theme dahil)
DELETE /api/sites/{site} # KVKK: anonymize + soft delete (30 gün geri alınabilir)
{ "cleanup_channels": false } # opsiyonel gövde — aşağıdaki uyarıya bak
POST /api/sites/{site}/restore # 30 gün içinde geri al
POST /api/sites/{site}/regenerate-key # API anahtarını yeniden üret (widget vb.)
POST /api/sites/{site}/widget-theme/ai # { "prompt": "koyu lacivert, altın vurgulu" } → AI widget tema ÖNERİSİ
POST /api/sites/{site}/auto-messages/beautify # { "field": "ai_error", "text": "..." } → AI ile güzellenmiş metin ÖNERİSİ
⚠️ Geri alma KISMİDİR — "nasıl olsa restore ederim" diye silme. Silme yanıtı
restore_deadlineverir; 30 gün dolduysa restore404. Geri gelen: yalnız site ayarları (ad, widget yapılandırması — audit snapshot'tan). Geri gelmeyen: SSS ve belgeler (silme anında kalıcı silinir) + mesaj içerikleri (KVKK kanıtı olarak anonim kalır). Site silmeden önce SSS/belgeleri kendi tarafında yedekle.
cleanup_channels: truegönderirsen siteye özel kanal bağlantıları da KALICI silinir (Evolution instance, WABA aboneliği, webhook'lar) — restore bunları geri getirmez. Varsayılanfalse.
widget-theme/aiyalnız öneri döner ({theme, summary}); kalıcı kayıt için dönenthemeobjesiniPUT /api/sites/{site}ilesettings.widget_themealanına yaz (10 renk#RRGGBB+launcher_icon).
Otomatik mesajlar (2026-06-12): AI cevap veremediğinde müşteriye giden sistem metinleri site
bazında özelleştirilebilir: PUT /api/sites/{id} ile
settings.auto_messages.{unpaid_notice | ai_error | off_hours_notice}.
Alan boş/yok ise sitenin birincil dilinde hazır metin gönderilir. off_hours_notice çalışma saatleri
dışında yazan müşteriye konuşma başına bir kez gider; metindeki {next_open} yer tutucusunu sistem
doldurur (aynen korunmalı, silme). auto-messages/beautify (field: bu üçünden biri; text opsiyonel —
boşsa varsayılan baz alınır) yalnız öneri döner ({success, text}), kaydetmez.
GET /api/sites/{site}/domains
POST /api/sites/{site}/domains { "domain": "magaza.com" }
POST /api/domains/{domain}/verify
DELETE /api/domains/{domain}
Ne işe yarar: 8+ kanalın tek birleşik listesi. Panelin "Mesaj Kutusu"nun aynısı. Tek istekte tüm kanalların konuşmalarını + okunmamış sayaçlarını verir (yoksa kanal başına ~35 ayrı istek atman gerekir).
GET /api/inbox/unified
GET /api/inbox/unified?filter=unread # sadece okunmamışlar
GET /api/inbox/unified?channel=whatsapp # tek kanal (sayfa boyu 500'e çıkar)
GET /api/inbox/unified?offset=50 # sonraki sayfa (2026-07-14, aşağıya bak)
GET /api/inbox/unified?fresh=1 # cache (15s) bypass
{
"success": true,
"conversations": [
{ "channel": "whatsapp", "identifier": "905551112233", "profileId": 4, "siteId": 12,
"displayName": "Örnek Müşteri", "lastMessage": "Tamam, görüşürüz",
"lastMessageAt": "2026-05-20T18:25:00Z", "unreadCount": 2,
"isBotActive": true, "isBlocked": false, "sourceLabel": "Örnek Mağaza" }
],
"counts": {
"whatsapp": { "total": 140, "unread": 3 }, "instagram": { "total": 37, "unread": 0 },
"telegram": { "total": 5, "unread": 0 }, "messenger": { "total": 2, "unread": 0 },
"tiktok": { "total": 0, "unread": 0 }, "x": { "total": 0, "unread": 0 },
"mail": { "total": 249, "unread": 0 }, "voice": { "total": 40, "unread": 18 },
"widget": { "total": 30, "unread": 0 }
},
"totals": { "total": 503, "unread": 21 },
"hasMore": true,
"pageSize": 50,
"offset": 0,
"cached": true,
"ttl_seconds": 12
}
⚠️ Konuşma alanları camelCase'dir (
displayName,lastMessage,lastMessageAt,unreadCount,isBotActive,isBlocked,profileId,siteId,sourceLabel) — snake_case karşılıkları YOKTUR.countsdüz sayı değil, kanal başına{total, unread}nesnesidir (9 kanal).
Sayfalama (2026-07-14): liste kanal başına pageSize konuşma döner (varsayılan 50, channel=X
seçiliyken 500). hasMore=true ise sonraki sayfayı offset += pageSize ile iste; offset>0
yanıtlarında counts/totals null gelir (ilk sayfadaki değerler geçerlidir).
Sayaç birimi (2026-07-14): counts.*.unread ve totals.unread okunmamış konuşma sayısıdır
(mesaj adedi değil) — totals.total ile aynı birim. Konuşma başına mesaj adedi tile'daki unreadCount
alanında kalır. Tek istisna TikTok yorumlarıdır: orada birim yorum adedidir (her yorum ayrı satır).
channel + identifier (+ gerekiyorsa profile_id) ikilisi, kanal bölümlerindeki (§3) mesaj/gönder
endpoint'lerine girdi olur.
POST /api/inbox/mark-read
{ "channel": "whatsapp", "identifier": "+905551112233", "profile_id": 4 }
Desteklenen
channel: whatsapp · instagram · telegram · messenger · tiktok · mail · widget.xvevoicebu uçta YOK (422) — birleşik listede görünseler bile.widgetkabul edilir ama okunmamış sayacı olmadığı için çağrı no-op'tur (updated: 0).
Bot/operatör cevabı dış API'ye anlık reddedilirse (Meta 5xx/429/bağlantı hatası) mesaj failed
kalır — kaybolmaz. Arka planda kademeli otomatik retry (5/15/45dk, maks 3) çalışır; ek olarak
panelden "Tekrar dene" bu endpoint'i tetikler:
POST /api/inbox/messages/{channel}/{id}/retry # channel: whatsapp | instagram | messenger | telegram
# → 200 { "success": true, "delivery_status": "sent", "message_id": "..." }
# → 422 mesajlaşma penceresi (24 saat) kapalıysa (TG'de pencere yok)
GET /api/inbox/media/{channel}/{messageId} # channel: whatsapp | instagram | telegram | messenger | tiktok
GET /api/inbox/media/{channel}/{messageId}?download=1
GET /api/inbox/media-signed/{channel}/{messageId} # imzalı varyant — Authorization header GEREKMEZ
# (aynı 5 kanal)
Mail ekleri ayrı uçtan iner:
GET /api/mail/messages/{id}/attachments/{index}(auth'lu) ·GET /api/mail/attachments-signed/{messageId}/{index}(imzalı, 24 saat) — mail'de mesaj başına N ek olduğu için index'li ayrı bir aile.
İmzalı varyantın URL'ini kendin üretme: kanal mesaj listesi yanıtlarındaki media_signed_url alanı hazır
imzalı gelir (24 saat geçerli, sonra 403). Medyayı kendi arayüzünde <img>/<video>/<audio> src'ına
doğrudan koyacaksan bunu kullan — auth'lu /inbox/media/... tarayıcı media etiketlerinde header
gönderilemediği için 401 alır. Sunucu tarafı indirme için Bearer'lı /inbox/media/... kullanmaya devam et.
Tüm mesajlaşma kanalları aynı 4 adımlı pattern'i izler:
GET {kanal}/conversationsGET {kanal}/messages/{identifier}POST {kanal}/send (Instagram/X/TikTok/Mail'de farklı isim)POST {kanal}/toggle-botBot mantığı:
toggle-botile bir konuşmada AI'yı kapatırsan, o konuşmaya sen (operatör) cevap yazarsın. Açık bırakırsan gelen mesaja AI otomatik cevap verir.toggle-blockile konuşmayı engellersin.⚠️ Mail istisnası:
mail/toggle-botgövdesi{ account_id, site_id? }'dir ve konuşma bazlı değildir —site_idverirsen o site-hesap eşleşmesini, vermezsen hesabın tamamını kapatır. Tek bir yazışmayı susturmak içinPOST /api/mail/toggle-block(gönderen e-posta bazlı) kullan.
Sayfalama (mesaj listesi) — 2026-06-07:
{kanal}/messages/...varsayılanda konuşmanın tüm geçmişini döndürür (geriye dönük uyumlu). Performans için opt-in sayfalama var:?limit=50→ en yeni 50 mesaj +{ messages, has_more, oldest_id }zarfı; daha eski sayfa için?before_id={oldest_id}&limit=50(yukarı kaydırma cursor'ı,oldest_id'yi zincirle).limit/before_idvermezsen eski davranış (tüm mesajlar, düz dizi) aynen korunur — mevcut entegrasyonların bozulmaz. Destekleyen kanallar: WhatsApp · Telegram · Messenger · TikTok DM · X. (Instagram/Mail konuşma-gruplu yanıt verir, Widget/Voice tekil konuşmadır → bunlarda sayfalama yoktur.)limit1–200 clamp.
Paneldeki form (profil ekleme): bağlantı tipi (Evolution QR / Meta Cloud API / Coexistence) + ilgili kimlik bilgileri. Profil = bir WhatsApp numarası.
GET /api/whatsapp/profiles # bağlı numaralar (profiller)
GET /api/whatsapp/conversations # ?profile_id=X ile filtrele
GET /api/whatsapp/messages/{phone} # mesajlar (?limit=50&before_id=... ile sayfala — opsiyonel)
POST /api/whatsapp/send # yanıt gönder
GET /api/whatsapp/conversation-state # ?phone=...&site_id=... → { bot_active, is_blocked }
POST /api/whatsapp/toggle-bot
POST /api/whatsapp/toggle-block
DELETE /api/whatsapp/conversations/{phone}
Gönderme:
POST /api/whatsapp/send
{
"phone": "+905551112233",
"profile_id": 4,
"message": "Merhaba, nasıl yardımcı olabilirim?",
"media_file_id": null // opsiyonel — önce POST /api/media/upload
}
Yanıt bağlantı tipine göre iki dallıdır (data zarfı yoktur):
// Meta Cloud (meta_business / meta_coexistence)
{ "success": true, "message_id": "wamid.HBg..." }
// Evolution (QR)
{ "success": true }
Teslim edildi mi? Giden mesajın durumu (
sent → delivered → read/failed) push olarakmessage.statuswebhook'uyla, alternatif olarakGET whatsapp/messages/{phone}yanıtındakidelivery_statusalanıyla izlenir — ikisinin kuralları ve polling'in okundu işaretleme yan etkisi § 10 → Outbound webhook'ta. ⚠️wamidyalnız Meta bağlantısında döner; QR (Evolution) yolunda gönderim yanıtında mesaj kimliği yoktur.
Tek alıcıya onaylı ŞABLON (wamid döner) — 2026-08-03:
POST /api/whatsapp/send-template
{ "phone": "+905551112233", "template_name": "toplanti_cagrisi", "template_language": "tr",
"profile_id": 48, "site_id": 192,
"variable_mappings": { "1": { "pull": "ad" } }, "variables": { "ad": "Ayşe Yılmaz" },
"contact_id": 4021 }
# → 200 { "success": true, "status": "sent", "message_id": "wamid.HBg...",
# "message_db_id": 90211, "site_id": 192 }
# → 422 { "success": false, "status": "skipped", "reason": "opt_out", "message_id": null }
Hangisi ne zaman: whatsapp/send = serbest metin, 24 saatlik pencere gerekir, wamid yok ·
whatsapp/send-template = şablon, pencere gerekmez, wamid döner ·
contacts/groups/{id}/send-template = grup bazlı, wamid yok.
Adlı ek limit:
wa-send-template60/dk/kullanıcı (genel plan limitine ek). ⚠️ Varsayılan olarak idempotent değildir — başlıksız aynı istek iki kez atılırsa iki mesaj gider. 20-30 kişiyi aşan gönderimiPOST /api/scheduled-jobsile yap (alıcı bazında idempotent, kaldığı yerden devam, kota iadesi).
Tekilleştirme — opsiyonel Idempotency-Key başlığı (2026-08-07): ağ zaman aşımında isteği tekrarlayan
entegrasyon ikinci mesajı göndermek zorunda kalmasın diye send-template çağrısına kendi ürettiğin bir
anahtarı başlıkla geçebilirsin (maks. 120 karakter, 24 saat geçerli, hesap kapsamında tekil).
POST /api/whatsapp/send-template
Idempotency-Key: aidat-2026-08-daire-12
| Durum | Sonuç |
|---|---|
| Aynı anahtar + aynı gövde | Meta'ya gidilmez; ilk yanıt (wamid dahil) idempotent_replay: true ile aynen döner |
| Aynı anahtar + farklı gövde | 409 idempotency_key_reused — sessizce eski yanıt DÖNMEZ |
| İlk istek hâlâ işleniyor | 409 idempotency_in_progress — ikinci mesaj gönderilmez |
| Gönderim hiç yapılmadan reddedildi (opt-out / kota / şablon kapısı) | Anahtar serbest kalır; düzeltip aynı anahtarla tekrar dene |
| Meta 5xx / zaman aşımı (502) | Anahtar yanar — mesaj gitmiş olabilir; tekrar aynı belirsiz yanıtı alır, körlemesine yeniden gönderim YOK |
Başlığı göndermezsen hiçbir şey değişmez: yanıt gövdesi
idempotent_replayalanını içermez ve davranış bugünküyle birebir aynıdır.idempotency_keybir gövde alanı değildir, yalnız başlık geçerlidir. Replay geçmiş yetkiyi taşımaz: her tekrarda site/profil erişimi güncel authz kümesinden yeniden doğrulanır; erişim kaldırıldıysa saklanan telefon/wamid yanıtı dönmez. Opaque anahtarın kendisi DB'de tutulmaz, yalnız SHA-256 izi saklanır. (Süreç gönderim ortasında ölerse anahtar 24 saat boyunca 409 verir — mesajın gidip gitmediği bilinemediği için bilinçli karar; o gönderime devam etmen gerekiyorsa yeni bir anahtarla ilerle.)
Profil yönetimi (kurulum):
GET /api/whatsapp-profiles
POST /api/whatsapp-profiles # profil oluştur
POST /api/whatsapp-profiles/{id}/link-site { "site_id": 12 } # profili siteye bağla
POST /api/whatsapp-profiles/{id}/unlink-site
GET /api/whatsapp-profiles/{id}/linked-sites
# Evolution (QR): POST whatsapp-profiles/evolution/create → GET .../{id}/evolution/qrcode
# Meta Cloud: GET whatsapp-profiles/platform/phones → POST whatsapp-profiles/platform/connect
İşletme profili, görünen ad ve numara kaydı (2026-08-03):
GET /api/whatsapp-profiles/{id}/business-profile # about/açıklama/adres/e-posta/websites/vertical/foto
POST /api/whatsapp-profiles/{id}/business-profile # güncelle (adlı limit: wa-profile-admin, 20/dk)
POST /api/whatsapp-profiles/{id}/business-profile/photo # multipart foto (maks 5 MB, aynı limiter)
GET /api/whatsapp-profiles/{id}/display-name # mevcut ad + onay durumu + bekleyen talep
POST /api/whatsapp-profiles/{id}/display-name # yeni ad TALEBİ (Meta incelemesi — anında değişmez)
POST /api/whatsapp-profiles/{id}/register # onaylanan adı UYGULA (adlı limit: wa-profile-verify, 10 dk'da 5)
meta_business / meta_coexistence); QR (Evolution) profilinde
yanıt { "state": "unsupported" }.is_platform_managed) profilinde okuma serbest, yazma 409.403 alır.WHATSAPP_PROFILE_ADMIN_ENABLED — kapalıyken 6 uç da 404 (uç yokmuş gibi).register kotası Meta'dadır: 72 saatte numara başına 10 kayıt; onaydan sonra 14 gün içinde
çağrılmazsa ad yeniden incelemeye girer.Endpoint adları kanaldan kanala değişir ama akış aynıdır. Kimlik alanı (identifier) kanala göre farklı:
| Kanal | Konuşmalar | Mesajlar | Gönder | Bot aç/kapa | identifier |
|---|---|---|---|---|---|
GET whatsapp/conversations |
GET whatsapp/messages/{phone} |
POST whatsapp/send |
POST whatsapp/toggle-bot |
phone |
|
| Telegram | GET telegram/conversations |
GET telegram/messages/{chatId} |
POST telegram/send |
POST telegram/toggle-bot |
chat_id |
| Messenger | GET messenger/conversations |
GET messenger/messages/{senderId} |
POST messenger/send |
POST messenger/toggle-bot |
sender_id |
GET instagram/messages |
(mesajlar inline döner) | POST instagram/messages/send |
POST instagram/toggle-conversation-bot |
sender_id |
|
| X (Twitter) | GET x/conversations |
GET x/conversations/{conversationId}/messages |
POST x/reply |
POST x/conversations/toggle-bot |
conversation_id |
| TikTok (yorum) | GET tiktok/conversations |
GET tiktok/conversation-messages?video_id=..&open_id=.. |
POST tiktok/reply |
POST tiktok/toggle-bot |
videoId+openId |
| TikTok (DM) | GET tiktok/dm/conversations |
GET tiktok/dm/messages?open_id=.. |
POST tiktok/dm/reply |
POST tiktok/dm/toggle-bot |
openId |
GET mail/accounts/{id}/messages |
GET mail/messages/{id}/thread |
POST mail/reply |
POST mail/toggle-bot |
hesap + thread | |
| Web Widget | GET widget-chats |
GET sessions/{session} |
POST sessions/{session}/reply |
(yok) | session |
| Google Yorumlar | GET google-reviews |
GET google-reviews/{id} |
POST google-reviews/{id}/reply |
POST google-reviews/{id}/toggle-bot |
review id |
TikTok path formları DEPRECATED:
.../{videoId}/{openId}/messagesve.../dm/conversations/{openId}/messagesyalnız geri uyumluluk içindir —open_idstandart base64'tür,/içerdiğinde route ıskalanır (404). Query-param formunu kullan. Aynı ikilik silmede de var:DELETE tiktok/conversation?video_id=..&open_id=..·DELETE tiktok/dm/conversation?open_id=...Instagram kimliği yönlere göre farklı ad alır: listede/mark-read'de
sender_id, gönderimde aynı IGSIDrecipient_idalanıyla gider.
Gönderme gövdesi kanala göre kimlik alanını değiştirir, gerisi aynı kalır:
// Telegram
POST /api/telegram/send { "chat_id": "123456789", "profile_id": 7, "message": "..." }
// Messenger
POST /api/messenger/send { "sender_id": "...", "profile_id": 3, "message": "..." }
// Instagram — kimlik alanı recipient_id (sender_id DEĞİL → 422)
POST /api/instagram/messages/send { "recipient_id": "<IGSID>", "message": "...", "media_file_id": null }
// X — metin alanı text (maks 280); conversation_id hem DB id'sini hem conversation_key'i kabul eder
POST /api/x/reply { "conversation_id": "...", "text": "... (maks 280)", "in_reply_to_x_message_id": null }
// TikTok DM
POST /api/tiktok/dm/reply { "open_id": "...", "message": "...", "profile_id": 12 }
// Mail — account_id + to ZORUNLU; "message_id" diye bir istek alanı YOKTUR
POST /api/mail/reply { "account_id": 4, "to": "musteri@ornek.com", "subject": "Re: ...",
"body": "...", "in_reply_to": "<msgid@...>" }
// → { "success": true, "data": { ...MailMessage } }
Profil kurulumu çoğu kanalda benzer:
GET/POST {kanal}/profiles+.../link-site+ OAuth (Messenger/TikTok/X/Google →{kanal}/oauth/config|save). İki istisna: Instagram'dainstagram/profilesdiye bir uç YOKTUR (bağlantıGET instagram/account+POST instagram/connect/instagram/manual-connect/instagram/oauth/saveüzerinden), Mail'de kaynak adımail/accounts'tır (GET/POST mail/accounts,POST mail/accounts/{id}/link-site). Bunlar genelde panel UI'ından yapılır. Tam alanlar API Dokümantasyonu'nda.
# kanal-aware — metin alanı "draft", "channel" ZORUNLU ("text" alanı YOK → 422)
POST /api/ai-improve-message
{ "channel": "whatsapp", "draft": "taslak metin", "profile_id": 4, "identifier": "+9055..." }
# site-explicit — burada da "channel" ZORUNLU
POST /api/sites/{site}/ai-improve-message { "channel": "whatsapp", "draft": "..." }
# → taslağı kanala uygun, kibar versiyona çevirir (20/dk)
POST /api/media/upload (multipart)
# → { "file": { "id": 555, "url": "...", "type": "image", "mime": "image/jpeg" }, "deduped": false }
# send'de kullanacağın değer: media_file_id = file.id (yanıtın kökünde "id" YOKTUR)
GET /api/media # → { "files": [ ... ] }
DELETE /api/media/{id}
Site-explicit varyant sahipliği
site.user_id === oturum kullanıcısıile kontrol eder → bayi müşterisi / alt kullanıcı / agent403alır. O hesaplarda kanal-aware ucu kullan.
Ne işe yarar: Kişi/grup yönetimi (CRM) + WhatsApp şablonuyla toplu/zamanlı gönderim.
GET /api/contacts/groups
POST /api/contacts/groups { "name": "VIP Müşteriler" }
PUT /api/contacts/groups/{id}
DELETE /api/contacts/groups/{id}
GET /api/contacts/groups/{id}/contacts
POST /api/contacts/groups/{id}/contacts { "name": "...", "phone": "+90..." }
PUT /api/contacts/{id} # kişi güncelle (phone ZORUNLU; name/email/phone_type/metadata opsiyonel)
POST /api/contacts/delete { "ids": [1,2,3] }
DELETE /api/contacts/groups/{id}/contacts # grubu boşalt — TÜM kişileri tek istekte siler (grup kalır, geri alınamaz)
POST /api/contacts/groups/{id}/import # dosya yükle: csv/txt/xlsx/xls, maks 5 MB. Başlıkta telefon kolonu ZORUNLU
# (Telefon / Cep / Mobile / WhatsApp No / Phone) — telefonsuz satır kişi üretmez
POST /api/contacts/groups/import-file # bilgisayardan CSV/TXT/JSON yükle → YENİ grup: { "name": "...", file }
# maks 15 MB, import-url'in aynı esnek parser'ı → email-only kişiler de alınır
POST /api/contacts/quick-add # sohbet panelinden hızlı rehbere ekle
GET /api/contacts/lookup-by-channel # ?phone=+90... | ?telegram_chat_id=... | ?messenger_psid=...
# | ?instagram_sender_id=... (+ opsiyonel &site_id=12)
lookup-by-channel: kimlik alanlarından tam olarak biri verilmelidir; hiçbiri verilmezse422 { "contact": null, "error": "Bir identifier gerekli" }döner.channel+identifiersözleşmesi bu uçta DEĞİL,conversation-ai-overridesucunda geçerlidir.
import-urlKendi web siteniz / CRM'inizden kişi listesini DoWaba rehberine çekin.
POST /api/contacts/groups/import-url
{
"name": "Web sitesi lead'leri",
"url": "https://siteniz.com/api/leads.csv", // CSV veya JSON döndüren PUBLIC adres
"api_key": "gizli-anahtar", // opsiyonel
"auth_style": "bearer", // bearer | x-api-key | query
"auto_refresh": false // true → her kampanyada yeniden çekilir
}
email (e-posta/eposta/mail…), ad (name/isim/ad_soyad…), phone (telefon/gsm/cep…). email VEYA telefon en az biri zorunlu.auto_refresh=false (varsayılan): tek-seferlik — bir kez çekilir, statik grup oluşur, API anahtarı saklanmaz.auto_refresh=true: kaynak URL + auth + API anahtarı (şifreli) gruba kaydedilir; mail kampanyası başlatıldığında (POST /api/mail-campaigns/{id}/start) liste bu adresten yeniden çekilir (UPSERT; kaynakta olmayan kişi silinmez). Tazeleme hatası kampanyayı bloklamaz, yalnız loglanır.
⚠️ WhatsApp yolları (scheduled-jobs, send-template) grubu TAZELEMEZ — grup ilk import'taki hâliyle kalır. WhatsApp'ta güncel liste istiyorsan gönderimden önce POST /api/contacts/groups/import-url ile yeniden çek ya da kişileri POST /api/contacts/groups/{id}/contacts ile upsert et.group.id'yi mail/WhatsApp kampanyasında contact_group_id olarak kullanın.Akıllı segment (yalnız superadmin):
POST /api/contacts/segments/{key}/sync. Geçerlikeydeğerleri:owner_reseller(İYS izinli, e-posta) ·owner_reseller_info(izinsiz/bilgilendirme, e-posta) ·owner_reseller_wa(İYS izinli, WhatsApp) ·owner_reseller_wa_info(izinsiz/bilgilendirme, WhatsApp). Kapsam: role ∈ {user, reseller} ∧ bayi müşterisi değil ∧ alt kullanıcı değil ∧ aktif ∧ kanal kimliği (e-posta/telefon) dolu ∧ izin eksenine uygun (marketing_consent_atdolu/boş). Mail segmenti WhatsApp toplu gönderiminde 400 ile reddedilir — WhatsApp için*_waanahtarını kullan. Tazeleme: mail kampanyası start'ında otomatik; WhatsApp yollarında otomatik DEĞİL → gönderimden öncesync'i kendin çağır. Diğer kullanıcılara kapalıdır (403); grup yalnız superadmin'in rehberinde görünür.
Müşteri-bazlı AI talimatı: Bir kişiye özel bot davranışı vermek için
PUT /api/conversation-ai-overrides { site_id, channel, identifier, instructions, is_active }(9 kanalda geçerli; aynı kişi WhatsApp/IG/Mail'den yazsa da aynı talimat uygulanır). ⚠️ Kişiye özel AI talimatıPUT /api/contacts/{id}ucundan yazılamaz — o uçai_instructionsalanını kabul etmez (gönderirsen 200 döner ama talimat kaydedilmez), yalnız bu uç geçerlidir.
POST /api/contacts/groups/{id}/send-template
{
"profile_id": 4, // ZORUNLU — hangi bağlı WhatsApp numarası
"template_name": "kampanya_haziran", // ZORUNLU
"template_language": "tr", // ZORUNLU
"variable_mappings": { "1": {"column": "name"} }, // opsiyonel (bkz. "Alıcıya özel şablon değişkenleri")
"variable_source": { ... }, // opsiyonel — gönderim anında veri çekme (pull tavanı: 50 alıcı)
"header_image_path": "whatsapp-headers/x.jpg" // opsiyonel (veya header_image_url + header_media_type)
}
site_idveparamsalanları YOKTUR (gönderilirse sessizce yok sayılır) — siteprofile_id'den çözülür.
⚠️ Uyum: Bu anlık toplu gönderim de
scheduled-jobsgibi opt-out + İYS reddini otomatik filtreler (red'li alıcı atlanır; yanıttaskipped+skipped_samplesdöner). Yine de pazarlama içerikse alıcıların İYS onayını önceden almış olmalısın (§ 0.6).
GET /api/scheduled-jobs
POST /api/scheduled-jobs # zamanlı toplu gönderim tanımı
PUT /api/scheduled-jobs/{id} # taslak/duraklatılmış kampanyayı düzenle (sil-yeniden-oluştur gerekmez)
POST /api/scheduled-jobs/{id}/toggle
DELETE /api/scheduled-jobs/{id}
GET /api/scheduled-jobs/whatsapp-stats/{contactGroupId}
POST /api/scheduled-jobs
{
"contact_group_id": 42, // ZORUNLU
"whatsapp_profile_id": 4, // ZORUNLU
"template_name": "kampanya_haziran", // ZORUNLU
"template_language": "tr", // ZORUNLU
"schedule_type": "once", // ZORUNLU — once | hourly | daily
"schedule_hour": 9, // daily için (0-23)
"scheduled_at": "2026-06-12 14:00", // yalnız once
"batch_size": 500, "delay_ms": 50, // opsiyonel (delay_ms: mesajlar arası bekleme, 0-5000)
"variable_mappings": { ... }, "variable_source": { ... },
"consent_confirmed": true // ⚠️ ZORUNLU BEYAN — false/eksik → 422
}
consent_confirmed, ETK 6563 / İYS beyanıdır (listenin pazarlama onayı var);consent_confirmed_atdamgası delil olarak kaydedilir. Arka planda otomatik gönderme — kullanıcıya göster, o onaylasın.
⚠️
scheduled-jobsile gönderim opt-out + İYS reddini otomatik filtreler (§ 0.6). Onaysız/red listesindeki alıcı atlanır (skipped, kotadan düşmez).
Tek seferlik kampanyaya tarih (2026-06-11): schedule_type=once iken opsiyonel
scheduled_at ("2026-06-12 14:00", Europe/Istanbul) gönderilebilir — kampanya o anda başlar.
Boş bırakılırsa (veya geçmiş tarihse) hemen başlar. hourly/daily tiplerinde yok sayılır.
Büyük listede tek seferlik gönderim nasıl ilerler (2026-08-18): gönderim hızını WhatsApp
(Meta) API'si belirler; ölçülen değer alıcı başına yaklaşık 1 saniyedir (delay_ms bunu artırır).
Bu yüzden once kampanyası tek koşumda bitmez: her koşum ~4 dakikalık bir bütçeyle çalışır,
süre dolunca durur ve kalan alıcılara hiçbir kayıt yazmadan bir sonraki dakikada kaldığı yerden
kendiliğinden devam eder (gönderilenlerin log satırı olduğu için çift gönderim olmaz). Kabaca:
1.000 alıcı ≈ 20 dakika, 5.000 alıcı ≈ 1,5-2 saat. İlerlemeyi GET /api/scheduled-jobs ile
total_sent / total_skipped üzerinden izleyebilirsin; kampanya bitince is_active false olur.
batch_size süreyi kısaltmaz — bir koşumda seçilecek alıcı sayısı tavanıdır.
metadata / variable_mappings)Şablondaki {{1}}, {{2}} yerlerine alıcı bazında değer basmak için (aidat tutarı, randevu saati, sipariş no):
POST /api/contacts/groups/{id}/contacts
{ "phone": "+90...", "name": "Ayşe", "metadata": { "borc": "1.250", "daire": {"no": "12"} } }
PUT /api/contacts/{id}
{ "phone": "+90...", "metadata": { "borc": "0" } }
metadata serbest JSON; DoWaba'da sektöre özel şema YOKTUR. Alanı hiç göndermezsen mevcut değer korunur
(listeyi her gönderim öncesi yeniden basan dış sistemler için)."variable_mappings": { "1": {"metadata": "borc"}, "2": {"metadata": "daire.no"} }
(nokta notasyonu ile iç içe alan).pull > column > static > metadata. metadata / pull çözülemezse alıcı ATLANIR
(skipped_reasons.missing_variable + missing_variables kırılımı) — "Sayın -, borcunuz - TL" gönderilmez.
Eski column / static kaynakları eski davranışta kalır (yerine - basar).variable_source (Function Gateway fonksiyonu).
⚠️ Senkron uçta (contacts/groups/{id}/send-template) alıcı tavanı 50'dir; aşılırsa
422 { "error": "variable_source_requires_scheduled_job" } → POST /api/scheduled-jobs kullan
(orada gönderim alıcı bazında idempotenttir, yarıda kesilse de kaldığı yerden devam eder).İYS (İleti Yönetim Sistemi) onaylarını yükle/sorgula. Toplu pazarlama göndermeden önce gerekli — § 0.6.
Bu endpoint'ler
iysmodülü gerektirir (allowed_modules). İYS şu an STUB modda olabilir (summary.stub_mode=true) → gerçek İYS API'sine yazma için hesap credential'ı + production mod gerekir.
GET /api/iys/consents?site_id=12 # kayıtlı onaylar (data, total, sayfalı)
GET /api/iys/consents/summary # { stub_mode, counts }
GET /api/iys/consents/meta # { sources, types, recipient_types } — geçerli enum değerleri
POST /api/iys/consents # tekil onay kaydı
POST /api/iys/consents/attest-group # bir kişi grubuna toplu onay beyanı
DELETE /api/iys/consents/{id}
Tekil onay kaydı:
POST /api/iys/consents
{
"site_id": 12,
"recipient": "+905551112233", // telefon (MESAJ/ARAMA) veya e-posta (EPOSTA)
"type": "MESAJ", // MESAJ | EPOSTA | ARAMA
"status": "ONAY", // ONAY | RET (varsayılan ONAY)
"source": "HS_WEB", // izin kaynağı — geçerli değerler /iys/consents/meta'da
"recipient_type": "BIREYSEL", // BIREYSEL | TACIR
"consent_date": "2026-06-01 10:00:00", // iznin alındığı GERÇEK an — aşağıdaki nota bak
"consent_confirmed": true // ZORUNLU — onayın gerçekliğini beyan edersin
}
⚠️
consent_dateboş / geçersiz / gelecek tarih REDDEDİLMEZ — sessizce sunucu saatine (now) kırpılır, istek 201 döner. Doğru tarihi göndermek çağıranın sorumluluğudur (delil zinciri). (Çağrı Kampanyası'nda davranış farklı: orada gelecek tarih422ile reddedilir — § 5.)
İYS hesabı (credential) yönetimi:
GET /api/iys/accounts
POST /api/iys/accounts { "iys_code":"...", "brand_code":"...", "iys_username":"...", "iys_password":"...", "environment":"production" }
PUT /api/iys/accounts/{id}
DELETE /api/iys/accounts/{id}
environment:sandbox|production. Credential'lar şifreli saklanır, yanıtta asla açık dönmez.
GET /api/whatsapp/templates
POST /api/whatsapp/templates # şablon oluştur (Meta'ya gider)
PUT /api/whatsapp/templates/{id}
POST /api/whatsapp/templates/add-optout-button # şablona otomatik vazgeçme QUICK_REPLY butonu ekler (metin şablon diline göre: tr "Tekrar mesaj istemiyorum", en "Unsubscribe", ru "Стоп", id "Berhenti", ar "إيقاف") → Meta re-review (PENDING); alıcı butona basınca opt-out suppression
DELETE /api/whatsapp/templates/{name}
POST /api/whatsapp/upload-header-image
GET /api/whatsapp/db-columns # şablon değişkenleri için sütun listesi
GET /api/mail-templates
POST /api/mail-templates
GET/PUT/DELETE /api/mail-templates/{id}
POST /api/mail-templates/generate # AI ile şablon üret — 20/dk (genel limite EK)
# { "site_id": 12, "prompt": "...", "tone": "samimi" }
# tone: resmi | samimi | kisa | detayli (varsayılan samimi)
POST /api/mail-templates/chat # çok turlu AI düzenleme (sohbet asistanı) — 30/dk
POST /api/mail-templates/{id}/preview
POST /api/mail-templates/{id}/send-test
generate:site_idZORUNLU (AI anahtarı site üzerinden çözülür); sitede Gemini anahtarı yoksa422.
GET /api/mail-campaigns
POST /api/mail-campaigns
GET/PUT/DELETE /api/mail-campaigns/{id}
POST /api/mail-campaigns/{id}/start | /pause | /resume | /retry-failed
GET /api/mail-campaigns/{id}/logs
GET /api/mail-campaigns/{id}/preview-recipients
POST /api/mail-campaigns
{
"site_id": 12, "mail_account_id": 4, "mail_template_id": 7, // hepsi ZORUNLU
"contact_group_id": 42, "name": "Haziran bülteni", // ZORUNLU
"schedule_type": "once", // ZORUNLU — once | hourly | daily
"batch_size": 100, // ZORUNLU — 1-500
"recipient_filter": { "require_email": true, "phone_types": ["mobile"] },
"daily_send_limit_override": 500, // mail hesabının kendi default'unu AŞAMAZ → 422
"consent_confirmed": true // ⚠️ ZORUNLU BEYAN (WhatsApp kampanyasıyla parite)
}
Ne işe yarar: Rehber grubundaki (CSV import dahil — contacts/groups/{id}/import, § 4) numaraları
site'ın sesli botu sırayla arar; sonuç alıcı bazında izlenir. Menü: Kampanyalar → Çağrı Kampanyası.
GET /api/voice-campaigns?status=running # liste — status: draft|running|paused|completed|cancelled
POST /api/voice-campaigns # { site_id, contact_group_id, name, initial_message?, prompt_override?, scheduled_at?, consent_source, consent_date, consent_confirmed: true }
GET /api/voice-campaigns/{id} # detay + sayaçlar (total_completed/no_answer/failed/skipped)
PUT /api/voice-campaigns/{id} # yalnız status=draft
DELETE /api/voice-campaigns/{id} # running/paused silinemez (önce cancel)
POST /api/voice-campaigns/{id}/start | /pause | /resume | /cancel
POST /api/voice-campaigns/{id}/retry-failed # no_answer + failed → tekrar kuyruğa (skipped ASLA yeniden aranmaz)
# yalnız completed|paused|cancelled kampanyada; running'de 422
# (tekrar aranacak alıcı yoksa da 422)
GET /api/voice-campaigns/{id}/recipients # alıcılar — status: pending|calling|completed|no_answer|failed|skipped
422 {"error":"no_trunk"}
(consent akışından bağımsız, ayrı bir hata).422 {"error":"consent_required"} dönerse eksik sözleşmeleri
POST /api/consent/channel/voice_campaign/accept ile kabul et (panel modalının muadili); kabul AuditLog + mail
kopyasıyla damgalanır.consent_source + consent_date + consent_confirmed: true zorunlu — listenin İYS
ARAMA onayına sahip olduğunu beyan edersin (alıcı bazında marketing_consents ARAMA delili otomatik yazılır).
⚠️ Burada gelecek tarihli consent_date 422 ile reddedilir (iys/consents ucundan farklı — orada sessizce
now'a kırpılır, § 4).channel=call opt-out'lu (§ 0.6) veya İYS RET'li numara otomatik
atlanır — alıcı skipped + skip_reason: opt_out | iys_denied, kota harcanmaz.message_opt_outs channel=call) ve kibarca kapatır; numara sonraki tüm kampanyalarda atlanır.outbound_call_hours_* + outbound_blocked_weekdays)
dışında arama yapılmaz — kampanya bekler, pencere açılınca devam eder.module:publishing)Ne işe yarar: Görseli/videoyu ve kanal metinlerini bir kez gönderirsin; DoWaba siteye bağlı uygun hesapları
seçer, içerikleri seçtiğin günlük saatlerde sırayla paylaşır ve istersen tur bitince yeniden başlatır. Paneldeki
karşılığı: /admin/publishing → Otomatik Kuyruk.
Bu uçlar external dsk_/dse_ anahtarıyla değil, panelde Geliştirici → API Anahtarları ekranından üretilen
Sanctum PAT (veya doğrudan panel oturumu) ile çalışır:
Authorization: Bearer <PAT>
Accept: application/json
Content-Type: application/json
Kullanıcıda allowed_modules kısıtı varsa publishing açık olmalıdır. Site ve bağlı hesap kapsamı kullanıcının
messaging privacy/authz zincirinden hesaplanır; başka tenant'ın site/profile ID'si kabul edilmez.
Trusted Partner OAuth PAT'larında § 0.2'deki mesaj-gizlilik kilidi aynen geçerlidir; bayinin müşterisi adına alınan
OAuth PAT bu messaging-scope yayın içeriğini yönetemez.
Medya dosyasını önce POST /api/media/upload ile yükleyip yanıttaki file.id değerini media_file_ids[] içinde kullan. Sonra yazmadan
önce otomatik hedef planını sorgulayabilirsin:
GET /api/social-publishing/capabilities?site_id=42&media_file_ids[]=105&text=Tanıtım
Temel otomatik seçim:
| İçerik | Otomatik seçilebilen kanallar |
|---|---|
| Video (+ metin) | YouTube · Instagram · Facebook Sayfa · TikTok · Threads · X |
| Görsel (+ metin) | Instagram · Facebook Sayfa · Threads · X |
| Yalnız metin | Facebook Sayfa · Threads · X |
Yalnız siteye bağlı, aktif ve kullanıcının erişebildiği profiller döner. YouTube ve TikTok video gerektirir; Instagram medya gerektirir. Platform uygulama izinleri/audit durumu başarıyı ayrıca etkileyebilir. X, resmi v2 medya upload akışıyla en fazla 4 görsel veya tek video/GIF kabul eder; uzun metinde medya ilk tweet'e bağlanır.
Örnek aşağıdaki tek POST ile iki günlük saat, döngü, kanal özel metinler, Instagram feed+hikâye, yorum
kuralları ve SSS kaydı birlikte kurulur. Ağ zaman aşımında güvenli retry için aynı Idempotency-Key değerini kullan:
POST /api/social-publish-queues # 20/dk (genel limite EK)
Idempotency-Key: mba-launch-v1
{
"name": "Meta Business Agent tanıtım döngüsü",
"site_id": 42,
"timezone": "Europe/Istanbul",
"daily_times": ["10:00", "20:00"],
"mode": "loop",
"default_repeat_days": 15,
"cycle_cooldown_days": 15,
"dry_run": false,
"items": [
{
"external_id": "mba-card-01",
"title": "Meta Business Agent DoWaba'da",
"body": "Uygunluğu kontrol edin, SSS'lerinizi senkronlayın.",
"media_file_ids": [105],
"targets": "auto",
"platform_content": {
"youtube": {
"title": "DoWaba Meta Business Agent Konsolu nasıl çalışır?",
"description": "Uygunluk, SSS senkronu ve maliyet simülatörü."
},
"instagram": { "caption": "Meta Business Agent'ı DoWaba'dan yönetin. #dowaba" },
"tiktok": { "caption": "Uygunluğu kontrol et, SSS'leri senkronla." },
"facebook_page": { "message": "Meta Business Agent konsolunu keşfedin." },
"threads": { "text": "Meta Business Agent artık DoWaba'da." },
"x": { "text": "Meta Business Agent konsolu DoWaba'da: uygunluk + SSS + maliyet." }
},
"platform_options": {
"youtube": { "privacy": "public", "category_id": "22", "tags": ["dowaba", "meta"] },
"tiktok": {
"publish_mode": "direct",
"privacy": "PUBLIC_TO_EVERYONE",
"disable_comment": false,
"disable_duet": false,
"disable_stitch": false
}
},
"placements": { "instagram": ["feed", "story"] },
"repeat_after_days": 15,
"automation": {
"comment_reply": {
"enabled": true,
"mode": "text",
"text": "Meta Business Agent hakkında yardımcı olabiliriz."
},
"keywords": ["meta", "mba", "sss"],
"dm": {
"enabled": true,
"text": "Detayları DoWaba panelinizde görebilirsiniz."
}
},
"faq_entries": [
{
"question": "Meta Business Agent Konsolu ne işe yarar?",
"answer": "Uygunluk kontrolü, SSS senkronu ve maliyet simülasyonu sağlar.",
"category": "Meta Business Agent"
}
]
}
],
"rules": [
{
"channel": "instagram",
"keywords": ["meta", "mba"],
"reply_message": "Size yardımcı olabiliriz.",
"reply_via": "both",
"enabled": true
},
{
"channel": "tiktok",
"keywords": ["meta"],
"reply_message": "Detaylar DoWaba'da.",
"reply_via": "comment",
"enabled": true
},
{
"channel": "x",
"keywords": ["meta business agent"],
"reply_message": "Meta Business Agent hakkında yardımcı olabiliriz.",
"reply_via": "comment",
"enabled": true
}
]
}
platform_content alanı yoksa ortak title/body fallback olur; alanı açıkça boş göndermek o kanalın
metnini bastırır. YouTube başlığı 100; Instagram caption 2.200; Threads metni 500 karakter sınırındadır.platform_options.youtube.privacy queue'da varsayılan publictir. TikTok direct|inbox seçilir; Login Kit Direct
Post için privacy zorunludur. inbox yalnız Drafts'a yollar, canlı paylaşım/başarı sayılmaz.placements.instagram=["feed","story"] iki ayrı IG hedefi yaratır. Hikâyede yorum alanı olmadığı için story
hedefi etkileşim kuralı üretmez; görsel/video gerçek Stories container'ıyla yayınlanır ve caption uygulanmaz.rules[], başlangıçta instagram|tiktok|x|youtube|facebook_page veya all için yazılabilir. Bir platformun
desteklemediği DM/yayın davranışını payload açamaz; kanal kısıtı üstündür.faq_entries birleştirilir; tek istekte toplam en fazla 100 SSS, question-bazlı upsert.dry_run=true DB'ye hiçbir şey yazmaz; normalize saatleri ve her item'ın gerçek hedef planını döndürür.201; aynı kullanıcı + aynı Idempotency-Key replay'i 200 ve
idempotent_replay:true döndürür. Aynı anahtar farklı gövdeyle kullanılırsa 409 döner. Anahtar en fazla 120
karakterdir; body'de idempotency_key olarak da verilebilir.daily_times benzersiz HH:MM değerleridir; en fazla 24 slot. Her slot yalnız bir öğe tüketir.10:00/20:00 = ilk tur yaklaşık 15 gün. Kesinti sonrası sistem en fazla DB'deki mevcut due slotu
bir kez işler; diğer kaçırılan saatleri topluca paylaşmadan bir sonraki gelecek saate geçer. Böylece spam patlaması olmaz.repeat_after_days, o öğenin yeniden seçilebileceği en erken gündür; boşsa default_repeat_days.mode=once ilk tur sonunda biter. mode=loop, turun sonunda cycle_cooldown_days kadar (ve gerekiyorsa öğenin
daha uzun repeat kilidi kadar) bekleyip yeni tura geçer. max_publishes bir öğeye sonlu tekrar sayısı verir.repeat_after_days=15; tur bitince ek bekleme istemiyorsan cycle_cooldown_days=0. İkisini de 15 verirsen sistem
daha geç olan kapıyı seçer ve tur tamamlandıktan sonra da 15 gün bekler.next_run_at tutulur ve her slotta
tek gerçek gönderi yaratılır. dispatched_count dış platform teslimi değil, yaratılan yayın occurrence sayısıdır.once total failure
kuyruğu duraklatır; kullanıcı düzelttikten sonra Devam Et aynı öğeyi yeniden dener.paused olur; loop sistemi
kullanıcı incelemesi olmadan aynı içeriği tekrar yüklemez.409 ve manual_review_post_ids döner, kuyruk güvenli biçimde duraklatılır.GET /api/social-publish-queues?site_id=42&status=active&per_page=30
GET /api/social-publish-queues/{queue}?per_page=50
PATCH /api/social-publish-queues/{queue} # name/status/mode/timezone/daily_times/repeat/cooldown/starts_at
DELETE /api/social-publish-queues/{queue}
POST /api/social-publish-queues/{queue}/pause
POST /api/social-publish-queues/{queue}/resume
POST /api/social-publish-queues/{queue}/items # { "items": [...] }, max 500 — 20/dk (genel limite EK)
PATCH /api/social-publish-queue-items/{item}
DELETE /api/social-publish-queue-items/{item}
Liste ve item detayları cursor pagination kullanır; sayfa numarası yerine yanıttaki cursor URL/değerleriyle ilerle. Bir HTTP transaction'ında en fazla 500 item vardır. Milyonluk içerik için tek dev JSON göndermek yerine:
Idempotency-Key ile queue create'te gönder./{queue}/items ucuna gönder; her parçaya ayrı ve stabil Idempotency-Key, her
öğeye kuyruk içinde benzersiz external_id ver.200
idempotent_replay:true döndürür; aynı anahtar farklı gövdeyse 409 verir. Kendi tarafında son onaylı chunk
checkpoint'ini yine tut.Target ve item satırları her parçada toplu insert edilir; dispatch yolu priority_rank, uygunluk ve kuyruk-sırası
bileşik indekslerinden seçim yapar. İstemci yine de aynı kuyruğa ait parçaları sırayla onaylamalıdır.
Threads OAuth kullanıcı consent'i gerektirdiği için ilk bağlantı headless tek bundle'ın parçası olamaz; kullanıcı panelde
bir kez Threads hesabını bağlar. Sonrasında targets:auto ve queue API'si hesabı otomatik kullanır.
GET /api/threads-profiles
GET /api/threads-profiles/{id}
PATCH /api/threads-profiles/{id} # name, is_active
DELETE /api/threads-profiles/{id}
POST /api/threads-profiles/{id}/test
POST /api/threads-profiles/{id}/link-site # { "site_id": 42 }
POST /api/threads-profiles/{id}/unlink-site # { "site_id": 42 }
GET /api/threads-profiles/{id}/linked-sites
OAuth popup'ın threads/oauth/config|pending|save|callback uçları panel içidir ve public Scribe referansında
gösterilmez. Profil tokenı şifreli saklanır; günlük yenileme ve yayın anındaki lazy refresh otomatik çalışır. Threads
metin, tek görsel, tek video ve carousel yayınlarını destekler; kanal özel metin platform_content.threads.text alanıdır.
module:adManagementNe işe yarar: Kullanıcı kendi Meta reklam hesabını bir siteye bağlar; kampanya/görsel oluşturur, performansı izler ve sızıntı koruması kurallarını yönetir. Meta App Review / Advanced Access onayı sonrası 2026-07-15'te tüm kullanıcılara açıldı. Reklam bütçesi DoWaba kredisi değil, kullanıcının Meta hesabından harcanır.
GET /api/ads/oauth/config # browser OAuth URL/config
GET /api/ads/oauth/pending # callback sonrası seçim verisi
GET /api/ads/accounts
POST /api/ads/accounts # seçilen hesap + Sayfa + IG'yi siteye bağla
GET /api/ads/accounts/{id}/edit-options
DELETE /api/ads/accounts/{id}
GET /api/ads/studio/models
POST /api/ads/studio/chat | /generate | /generate-set
GET /api/ads/studio/creatives
DELETE /api/ads/studio/creatives/{id}
GET /api/ads/campaigns?site_id={site}
POST /api/ads/campaigns/sync | /shell
POST /api/ads/campaigns # yeni kampanya; Meta'da PAUSED doğar
POST /api/ads/campaigns/{id}/status | /budget
GET /api/ads/audiences | /interests
POST /api/ads/audiences
GET /api/ads/insights?site_id={site}
GET /api/ads/insights/ads?site_id={site}
POST /api/ads/insights/exclude-region # tek bölgeyi hedeflemeden çıkar (10/dk)
POST /api/ads/insights/exclude-regions # TOPLU bölge çıkarma (5/dk — fan-out)
GET /api/ads/recommendations # Meta'nın webhook'la biriken hazır önerileri
POST /api/ads/recommendations/{id}/dismiss
GET /api/ads/advisor?site_id={site}
POST /api/ads/advisor | /advisor/apply
GET /api/ads/guard | /guard/actions
PUT /api/ads/guard
POST /api/ads/guard/actions/{id}/apply | /dismiss
GET /api/ads/catalogs?site_id={site} # Commerce Manager deep-link
GET /api/ads/pixels?site_id={site}
POST /api/ads/pixels
accessibleSiteIds() içindeyse erişilir.allowed_modules içinde adManagement yoksa menü ve API 403 ile kapanır.ads_beta_enabled ON durumundadır ve yalnız acil global kapatma kill-switch'i olarak korunur.insights/exclude-regions 5/dk; ardından pixels POST ve advisor/apply
10/dk, studio/generate-set 15/dk, campaigns/{id}/budget 20/dk, studio/chat · pixels GET ·
catalogs · accounts/{id}/edit-options 30/dk). İkisinden hangisi önce dolarsa 429 gelir.Tam alan/payload listesi için Scribe'daki Reklam Yönetimi gruplarını kullan; Meta wire kuralları için
meta-ads.md tek otoritedir.
Ne işe yarar: Müşterinin ikas / Shopify mağazasında bir sipariş olayı olunca (kargolandı, teslim edildi…) alıcıya Meta onaylı UTILITY şablonuyla tek bildirim gider. Panelde sol menüde ayrı madde değildir — Siteler → (site) → Entegrasyonlar altındadır.
GET /api/sites/{site}/order-notifications # bağlı mağazalar + ayar + kurallar + WhatsApp profilleri + son 20 kayıt
PUT /api/sites/{site}/order-notifications
{ "connection_type": "ikas", "is_active": true, "whatsapp_profile_id": 7, "consent_confirmed": true }
PUT /api/sites/{site}/order-notifications/rules
{ "connection_type": "ikas", "rules": [ { "event": "shipped", "is_enabled": true,
"template_name": "kargo_bilgisi", "template_language": "tr",
"variable_mappings": { "1": { "field": "order_number" } },
"button_vars": { "0": { "field": "tracking_url" } } } ] } # maks 20 kural
POST /api/sites/{site}/order-notifications/test
{ "connection_type": "ikas", "event": "shipped", "phone": "05551234567" }
POST /api/sites/{site}/order-notifications/register { "connection_type": "ikas" } # webhook'u platformda yeniden kur
connection_type her uçta ZORUNLU: ikas | shopify. Siteye bağlı mağaza yoksa 422.422): (1) şablon gönderebilen bir WhatsApp profili —
listedeki whatsapp_profiles[].template_capable=true olanlar, yani Meta Cloud + WABA (QR/Evolution
şablon gönderemez); (2) alıcı izni beyanı consent_confirmed: true. Seçilen profil siteye bağlı
değilse 422.webhook_last_error alanında görünür, saatlik reconcile cron'u tamamlar; beklemek istemezsen
register ucuyla elle tetiklersin.order_created · shipped · delivered · cancelled ·
refunded. ⚠️ Kaydederken her açık kuralın şablon kategorisi Meta'dan sorgulanır: UTILITY
değilse — ya da okunamıyorsa — istek 422 ile tamamen reddedilir (kısmi kayıt yok). MARKETING
şablon kategorik olarak yasaktır: sipariş bildirimi ETK 6563 m.6/1 istisnasına dayanır ve tanıtım
içeren tek satır istisnayı düşürür.test GERÇEK gönderimdir — kotadan düşer ve numaraya mesaj gider; yalnız deftere (logs)
yazılmaz. Adlı ek limit: order-notif-test 5/dk (genel plan limitine ek).logs[].phone null gelir (satırın status/skip_reason alanları görünür
kalır, sorun gidermeyi bozmaz). Modül anahtarı YOKTUR (allowed_modules bu uçlarda aranmaz).ORDER_NOTIFICATIONS_ENABLED: kapalıyken panel uçları okunur kalır, gönderim durur.Platform webhook'u (public):
POST /api/ecommerce/{platform}/order-webhook/{webhookToken} # platform: ikas | shopify
# → 200 { "success": true } (doğrulama geçti, iş kuyruğa alındı)
# → 503 özellik kapalı · 404 bilinmeyen platform · 403 token eşleşmedi / ayar pasif / imza geçersiz
Bu senin çağıracağın bir uç değildir: URL'i (
webhook_url) DoWaba üretir ve platforma kendisi kurar.Authorizationbaşlığı YOKTUR — kimlik opakwebhookToken+ platform doğrulamasıyla kurulur (ShopifyX-Shopify-Hmac-SHA256; app secret tanımsızsa fail-closed reddedilir · ikas gövdedekimerchantId↔ bağlantı eşleşmesi). Rate limit bilinçli olarak yoktur: webhook'a429dönmek platformun retry mantığını bozar (ikas 3 başarısız denemede aboneliği tamamen durdurur).
GET /api/sites/{site}/sip-trunk
PUT /api/sites/{site}/sip-trunk
DELETE /api/sites/{site}/sip-trunk
POST /api/sites/{site}/sip-trunk/move # numarayı başka siteden BU siteye taşı (atomik)
POST /api/sites/{site}/sip-trunk/test
PUTsırasında numara başka bir sitede kayıtlıysa422döner ve yanıtta birconflictobjesi bulunur ({ phone_number, other_site_id, other_site_name, can_move }).conflict.can_move=trueise (o siteye de erişimin var demektir) numarayı buraya taşımak içinmoveucunu çağır. Erişimin yoksa site adı/ID'si sızdırılmaz (null) vecan_move=falsegelir.
GET yanıtındaki privacy alanı trunk henüz kurulmamışken de site politikasını
döndürür. PUT trunk alanlarına ek olarak şu site-bazlı alanları kabul eder:
{
"voice_privacy_mode": "recorded",
"voice_legal_basis": "explicit_consent",
"voice_notice_profile": "short",
"voice_controller_name": "Örnek İşletme AŞ",
"voice_privacy_contact": "kvkk@example.com",
"voice_retention_days": 90,
"voice_controller_attested": true
}
recorded, transcript_only, no_retention. Aydınlatmayı
kapatan bir alan veya değer yoktur; voice_notice_required daima true döner.explicit_consent'tir. Diğer seçenekler:
contract_performance, legal_claims, legitimate_interest,
legal_obligation. Seçimin hukuki doğruluğu veri sorumlusu işletmeye aittir.voice_notice_profile: açılış anonsu profili — short (platform varsayılanı, kısa ilk
katman) veya standard (tam metin). Arayanın DUYDUĞU metni değiştirdiği için mod/hukuki
sebep ile aynı sınıftadır: değiştirmek voice_controller_attested: true ister ve GET
yanıtında da döner. Anonsu kapatmaz (voice_notice_required daima true).voice_privacy_can_manage bu durumu gösterir.voice_legal_actor_is_owner ve
voice_legal_can_accept kabul POST yetkisini; voice_legal_ready,
voice_legal_missing, voice_legal_acceptance_owner_id ve
voice_legal_owner_action_required owner kanıtının durumunu verir. Bayi,
superadmin veya başka bir aktör kendi legal kabul endpoint'lerini site sahibi
adına çağırmaz; owner kanıtı hazırsa operasyonel trunk ayarını kaydedebilir,
hazır değilse aktivasyon 422 + missing_acceptances döner (devre dışı bırakma
yine mümkündür).voice_controller_attested: true zorunludur ve değişiklik denetim kaydına alınır.voice_controller_name 2-100 karakterlik güvenli işletme-unvanı formatındadır;
voice_privacy_contact yalnız geçerli e-posta veya mutlak https:// adresidir.
Aktif trunk için iki alan da site satırında açıkça bulunmalıdır. Legacy
satırlarda panel site adı/sahip e-postasını yalnız ön-doldurabilir;
voice_privacy_materialization_required=true iken yetkili aktör bu değerleri
voice_controller_attested: true ile kalıcılaştırmadan aktivasyon 422 döner.
sites.voice_retention_days tek otoritedir; trunk üzerindeki aynı isimli alan
yalnız eski istemci aynasıdır.GET /api/sites/{site}/voice-privacy
PUT /api/sites/{site}/voice-privacy
SIP trunk'ı olmayan sitelerde (ör. yalnız WhatsApp üzerinden sesli hizmet) veri sorumlusu kimliği / mod /
hukuki sebep buradan kaydedilir — alanlar ve kurallar PUT sip-trunk ile aynıdır
(voice_privacy_mode, voice_legal_basis, voice_notice_profile, voice_retention_days,
voice_controller_name, voice_privacy_contact, voice_controller_attested), fakat bu uç SIP trunk'ına
dokunmaz. Yalnız site sahibi veya superadmin yazabilir; bayi/agent salt-okunurdur
(voice_privacy_can_manage=false).
GET /api/sites/{site}/voice-conversations
GET /api/voice-conversations/{id}
GET /api/voice-conversations/{id}/recording-url # imzalı kayıt URL'i
POST /api/voice-conversations/{id}/clean # transkripti AI ile yeniden temizle
POST /api/voice-conversations/bulk-destroy # { "ids": [1,2,3] } — toplu imha (maks 200; devam eden çağrı silinmez)
"Hat açıldı ama görüşülmedi" çağrılarına gönderilen tek takip mesajının defteri:
GET /api/sites/{site}/missed-call-followups # ?status=detected|sent|skipped|failed & per_page=1-100 (vars. 20)
# → { "success": true,
# "data": [ { "id": 12, "site_id": 5, "phone": "905551234567", "reason": "abandoned",
# "status": "sent", "skip_reason": null, "channel": "whatsapp",
# "template_name": "cevapsiz_cagri", "provider_message_id": "wamid.HBg...",
# "error": null, "detected_at": "...", "sent_at": "..." } ],
# "meta": { "current_page": 1, "last_page": 1, "per_page": 20, "total": 1 } }
PUT /api/sites/{site} gövdesindeki
settings.missed_call_followup bloğuyla yazılır (enabled, delay_minutes, max_duration_sec,
cooldown_hours, daily_cap, profile_id, template_name, template_language, template_vars,
allow_evolution_text, terms_confirmed_at). Blok nested merge edilir → yalnız gönderdiğin
alan değişir, gerisi korunur. enabled: true için KVKK/ETK beyanı zorunludur
(terms_confirmed_at boşsa 422 missed_call_terms_required); seçilen profil siteye bağlı + aktif
değilse 422 missed_call_profile_invalid.voice-conversations ile aynı gizlilik kilidindedir (satır arayanın numarasını taşır) →
bayi, müşterisinin kayıtlarını göremez (403).status=skipped satırlarında skip_reason hangi kapının kestiğini söyler: reached_later ·
cooldown · daily_cap · opt_out · iys_denied · quota · no_profile · no_template ·
no_template_category (şablon UTILITY değil) · window_closed · disabled · no_phone · stale.POST /api/voice/call # 5/dk
{ "site_id": 12, "to_number": "905551112233", "initial_message": "...", "prompt_override": "..." }
to_number ZORUNLU (phone diye bir alan YOK → 422). initial_message / prompt_override opsiyonel.422; yasal hazırlık eksikse 422 { "error": "voice_legal_not_ready", "missing_acceptances": [...] }; günlük outbound limiti (varsayılan 50) dolduysa 429 + today_count / limit.GET /api/sites/{site}/outbound-intents # bekleyen sesli bildirim aramaları
POST /api/sites/{site}/outbound-intents # manuel bildirim araması oluştur
POST /api/outbound-intents/{intent}/approve | /decline | /cancel
GET /api/sites/{site}/outbound-messages # 24h re-engagement mesaj önerileri
POST /api/sites/{site}/outbound-messages # manuel — { channel, target_identifier, initial_message, ... }
POST /api/sites/{site}/outbound-messages/generate # konuşmadan AI öneri üret (20/dk)
POST /api/outbound-messages/{intent}/approve | /decline | /cancel
POST /api/outbound-messages/{intent}/ai-rewrite # öneri metnini AI ile yeniden yaz (30/dk)
GET /api/outbound-messages/all # tüm sitelerin bekleyenleri (badge)
whatsapp · telegram · messenger · instagram ·
tiktok · mail · x; AI öneri (generate) yalnız whatsapp · telegram · messenger · instagram ·
mail (tiktok/x'te konuşma çekimi yok → aday üretilemez).approve artık declined / cancelled / failed durumundaki
önerileri de kabul eder (tekrar kuyruğa alır, failure_reason temizlenir).
completed/queued/in_progress onaylanamaz → 422 (çift gönderim koruması). 24 saatlik
mesajlaşma penceresi gönderim anında kontrol edilir — pencere kapandıysa öneri anlamlı bir
failure_reason ile failed'e düşer.profile_name eklendi —
mesajın hangi kanal profilinden (bağlı numara/hesap) gideceğini gösterir. Eski kayıtlarda
ve manuel oluşturulan önerilerde null olabilir.Ne işe yarar: Yüksek hacimli telefon trafiği için site başına ek kapasite (eşzamanlı +
günlük + aylık çağrı limitleri). Satın alma abonelik akışında (subscriptions/initiate,
plans yanıtındaki capacity_plans planlarıyla); atama buradan:
GET /api/me/voice-capacity # sahip olunan kapasite paketleri + atandıkları site
GET /api/sites/{site}/voice-capacity # sitenin paketi + anlık eşzamanlı / bugünkü çağrı sayacı
POST /api/sites/{site}/voice-capacity # { "subscription_id": 41 } — paket tek siteye atanır
DELETE /api/sites/{site}/voice-capacity # atamayı kaldır (site default 20 eşzamanlıya döner)
voice-conversations üretilmez. (İç gate:
concurrency_limit / daily_limit — operatör loglarında görünür.)Ne işe yarar: Telefonu AI karşılar; arayan "teknik ekiple görüşmek istiyorum" deyince AI onay alır ve çağrıyı aktarım rehberindeki kişinin telefonuna canlı bağlar. AI serbest numara çeviremez — yalnızca rehberdeki kişiler (sunucu tarafında da doğrulanır).
GET /api/sites/{site}/transfer-targets # rehber + enabled (opt-in) durumu
POST /api/sites/{site}/transfer-targets # { "name": "Ahmet Yılmaz", "department": "Teknik Ekip", "phone": "5551112233", "is_active": true }
PUT /api/sites/{site}/transfer-targets/{target} # kısmi güncelleme
DELETE /api/sites/{site}/transfer-targets/{target}
PUT /api/sites/{site} body { "settings": { "voice_transfer_enabled": true } }.
Kapalıyken AI'ya telefonu_aktar aracı hiç tanımlanmaz, davranış değişmez.90 öneki eklenir
(5551112233 → 905551112233). Site başına en fazla 20 hedef; user_id verilirse
site sahibı veya site ekibinden olmalı (aksi 422).language, BCP-47:
tr/en/ru/de/ar/fr/it) müşterinin konuştuğu dilden farklıysa ve
settings.voice_translate_enabled açıksa, çağrı düz bağlama yerine canlı çift-yönlü
çeviriyle aktarılır: müşteri ve temsilci kendi dilinde konuşur, Gemini sesi anlık çevirir.
transfer-targets yanıtındaki translate_enabled ile durum görünür; hedef oluştururken
language ver (boş = düz aktarım, çeviri yok). Çeviri ek AI kredisi tüketir
(§9, source: voice_translate).voice-conversations yanıtlarında başarılı aktarımlar
status: "transferred" + transferred_to_name / transferred_to_phone /
transferred_at alanlarıyla gelir. recorded moddaki Dowaba kaydı yalnız
aktarım öncesi privacy-gated AI fazını içerir; insan/canlı-çeviri/app aktarım
bacaklarında ses veya transkript saklanmaz. İşletme ayrı bir santral/temsilci
sistemiyle kayıt yapıyorsa itirazı kendi durdurma/silme prosedüründe işlemelidir.Ne işe yarar: AI "sizi geri arayalım" dediğinde / müşteri talep bıraktığında oluşan kayıtlar.
GET /api/callback-requests
PUT /api/callback-requests/{id}/status { "status": "resolved" }
# geçerli değerler: pending | contacted | no_answer | resolved | postponed (başkası → 422)
# contacted ve resolved yazıldığında contacted_at otomatik damgalanır
PUT /api/callback-requests/{id}/note { "note": "..." }
POST /api/callback-requests/{id}/beautify-note # notu AI ile düzelt
GET /api/callback-requests/{id}/conversation # talebin tam konuşma dökümü
PUT /api/callback-requests/summary-locale # AI özetinin dili
GET/PUT/DELETE /api/sites/{site}/output-schemas # Özet Şeması: siteye özel AI özet alanları
Özet Şeması tanımlıysa talep yanıtındaki AI özeti, senin tanımladığın alanları (ör. marka / yıl / bütçe)
callback_summary_dataiçinde display-ready[{key, label, value}]olarak da döner; alan konuşmada geçmiyorsa değerinullgelir — model tahmin etmez.
Talepler site ekibindeki bir kullanıcıya atanabilir; istenirse yeni talepler ekipteki
agent rolündeki kullanıcılara en az açık talebi olana otomatik dağıtılır.
GET /api/callback-requests/assignment-options?site_id=12 # atanabilir ekip + açık talep sayıları + auto_assign durumu
POST /api/callback-requests/{id}/assign { "user_id": 17 } # null → atamayı kaldır
PUT /api/callback-requests/auto-assign { "site_id": 12, "enabled": true }
{id} kanal prefix'li bir dizedir — w-42 (WhatsApp), v-17 (Voice), cs-9 (Widget): liste
yanıtındaki id alanını olduğu gibi kullan (raw_id sayısal karşılığıdır, uçlarda KULLANILMAZ).
Site ekibi dışındaki kullanıcıya atama 422 döner.GET /api/callback-requests) yanıtındaki her kayda assigned_user_id +
assigned_user_name alanları eklendi (atanmamışsa null).module:leadsNe işe yarar: Konuşmalardan / talep / lead-ads kaynaklarından oluşan satış adayları (CRM pipeline:
sürüklenebilir aşamalar + ekip ataması). Yalnız KENDİ sitelerinin lead'leri görünür (bayi, müşteri
sitelerinin lead'lerini göremez — mesaj/rehber gizlilik kilidiyle tutarlı). module:leads gerektirir.
GET /api/leads # liste (site/aşama filtreli)
POST /api/leads # manuel aday ekle (kaynak: manuel/öneri/talep/lead-ads)
POST /api/leads/inbox # Mesaj Kutusu konuşmasından aday ekle (channel + identifier)
GET /api/leads/inbox-status # konuşma zaten lead mi? (yazmaz — idempotent probe)
POST /api/leads/scan # konuşmaları AI ile tara → aday çıkar (10/dk)
POST /api/leads/{id}/analyze # tek adayı AI ile analiz et (10/dk)
GET /api/leads/{id}/activities # değişiklik geçmişi: kim ne yaptı (created/stage_changed/assigned/updated)
PATCH /api/leads/{id}/stage # pipeline aşaması değiştir { "stage": "..." }
POST /api/leads/{id}/assign # ekip üyesine ata { "assigned_user_id": 17 } (null → kaldır)
POST /api/leads/bulk-assign # aynı sitedeki en fazla 50 adayı toplu ata/kaldır
# { "lead_ids": [41,42], "assigned_user_id": 17 }
POST /api/leads/reorder # sütun içi sıralama (sürükle)
PATCH /api/leads/{id} # güncelle (ad / telefon / not ...)
DELETE /api/leads/{id} # sil
GET /api/lead-submissions # Lead Ads form yanıtları (Meta/Facebook lead formları — ham gönderimler)
Lead kaynakları: manuel · konuşma taraması (
scan) · Müşteri Talepleri · Lead Ads (Facebook lead formu →POST facebook-page-profiles/{id}/sync-leads,module:publishing→lead-submissions+ otomatik lead). Alan (body) listeleri için /api-docs (Scribe) → "Potansiyel Müşteriler (Leads)" grubuna bak.Değişiklik geçmişi: lead yazma uçları (ekleme / düzenleme / aşama / atama) her değişiklikte otomatik geçmiş kaydı üretir;
GET /api/leads/{id}/activitiesen yeni üstte döner. Kayıt:action,actor_name(sistem işlemlerinde null),actor_role(owner | agent | subuser | system ...), alan bazlıchanges { alan: {from, to} }(atamada okuma anında çözülenfrom_label/to_labelda eklenir). Sürükleme sırası (reorder) bilinçli olarak geçmişe yazılmaz. Toplu atama yalnız aynı siteye ait benzersiz lead id'lerini kabul eder; kapsam dışı tek id tüm isteği 404 ile durdurur. Zaten aynı kullanıcıya atanmış kayıtlar değiştirilmez ve yeniden bildirim üretmez. Uç globallead_bulk_assignmentfeature flag'iyle varsayılan kapalıdır; kapalıyken 503feature_disableddöner. Değişen toplu işlem hedef kullanıcıya tek, müşteri PII'si içermeyen özet bildirim üretir ve kullanıcı başına 10/dk adlı limiter'la korunur.
dse_ anahtar)Ne işe yarar: Kendi sisteminden (rezervasyon motoru, CRM, mobil uygulama) DoWaba'daki bir sitenin randevularını okuyup yazarsın. Bu bölüm rehberin geri kalanından farklı bir kimlik modeli kullanır: Bearer PAT değil, siteye özel External API Key.
⚠️ VARSAYILAN KAPALI. Bu uçlar
EXTERNAL_APPOINTMENTS_ENABLEDbayrağı açılana dek 404 döner (rota hiç yokmuş gibi). Kullanmak istiyorsan DoWaba'dan hesabın için açılmasını iste.
| Panel PAT (Bearer) | External key (dse_) |
|
|---|---|---|
| Kapsam | Kullanıcının tüm authz cascade'i (mesajlar, kanallar, faturalar…) | Tek site, yalnız randevu |
| Sızarsa | Hesabın tamamı | O sitenin randevuları |
| Kim üretir | Panel → Ayarlar → API Anahtarı | Yalnız sitenin tam sahibi: POST /api/sites/{site}/regenerate-external-key |
POST /api/sites/{site}/regenerate-external-key # Bearer PAT ile, site sahibi
# → 200 { "success": true, "external_api_key": "dse_...",
# "custom_integration_affected": false, "message": "..." }
dse_ + 48 karaktertir. Randevu uçları bu biçimi ZORUNLU kılar: eski kurulumlarda
external_api_key kolonunda widget anahtarı (dsk_, sitenin public HTML'inde görünür) durabiliyor
ve o anahtar kabul edilmez — bu uçlar müşteri PII'si döndürür.GET /api/sites / GET /api/sites/{site} onu
DÖNDÜRMEZ (2026-08-08: kolon Site::$hidden'a alındı — site kaydını görebilen salt-okur ajanlar
ve bayiler randevu PII'sini açan bir anahtarı hasat edememeli). Kaybedersen yenile./api/external/instagram/*), bu randevu yüzeyi ve — sitede
"Canlı veri aktarımı" (external_api_base_url) tanımlıysa — DoWaba'nın senin kendi API'ne
yaptığı giden çağrılar. Sonuncusu etkileniyorsa yanıttaki custom_integration_affected true
gelir; hepsini birlikte güncelle.dse_ anahtar giden çağrılarda kullanılmaz (2026-08-08): aksi hâlde
randevu PII'sini açan anahtar, senin sunucunun erişim/URL loglarına düşerdi.X-Api-Key: dse_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Accept: application/json
Authorization: Bearer kullanılmaz. Hata merdiveni:
| Durum | Yanıt |
|---|---|
| Özellik kapalı | 404 (gövde uygulama yanıtı değildir) |
| Başlık yok | 401 {"success": false, "error": "X-Api-Key header required"} |
| Anahtar biçimsiz / bilinmeyen | 401 {"success": false, "error": "Invalid API key"} |
| Site pasif | 403 {"success": false, "error": "Site is inactive"} |
| Aynı randevuda başka durum güncellemesi sürüyor | 409 {"success": false, "error": "status_update_in_progress", "retry_after": 1} |
| Limit aşıldı | 429 {"success": false, "error": "rate_limited", "retry_after": 30} |
Rate limit: anahtar başına 120, kanonik istemci IP'si başına 600 istek/dakika. Anahtar bucket'ı
bir müşterinin trafiğinin diğerini kilitlemesini önler; kaba IP tavanı rastgele geçersiz anahtar
rotasyonunu sınırlar. Retry-After ve X-RateLimit-* başlıkları gelir.
GET /api/external/appointments/services # hizmet kataloğu (fiyat dahil)
GET /api/external/appointments/staff # personel (beyaz liste alanlar)
GET /api/external/appointments/available-slots # müsait saatler
GET /api/external/appointments # randevu listesi (sayfalı)
POST /api/external/appointments # randevu oluştur
PATCH /api/external/appointments/{id}/status # durum değiştir
Hepsi anahtarın SİTESİNE kilitlidir. Başka sitenin (aynı hesabın ikinci sitesi dahil) verisi görünmez; panelden site seçilmeden açılmış randevular da bu yüzeyde YOKTUR.
GET /api/external/appointments/services
# ?include_inactive=1 → pasif hizmetleri de getir (senkron için)
# → { "success": true, "data": [ { "id": 7, "external_ref": "svc-12", "name": "Muayene",
# "description": null, "duration_minutes": 45, "price": "450.00", "currency": "TRY",
# "is_active": true, "staff_ids": [5] } ] }
GET /api/external/appointments/staff
# ?service_id=7 → yalnız o hizmeti verebilen personel
# ?include_inactive=1
# → { "success": true, "data": [ { "id": 5, "external_ref": "stf-42",
# "name": "Dr. Mehmet", "title": "Diş Hekimi", "is_active": true } ] }
price stringtir ("450.00") — para float'a çevrilmez.staff_ids BOŞ dizi "kimse vermiyor" DEĞİL, "TÜM personel verebilir" demektir. Hizmet ↔
personel eşlemesi opsiyoneldir; tanımlanmamışsa kısıt yoktur.GET /api/external/appointments/available-slots?date=2026-08-12&service_id=7&staff_id=5
# → { "success": true, "data": [ {"start": "10:00", "end": "10:45"} ] }
date TAM YYYY-MM-DD olmalı (12.08.2026 → 422).service_id, ızgarayı staff_id belirler.POST de aynı
kombinasyonu 422 ile reddeder. İkisi asla ayrışmaz.POST /api/external/appointments
{
"customer_name": "Ayşe Yılmaz",
"customer_phone": "905551234567",
"date": "2026-08-12",
"start_time": "10:00",
"end_time": "10:45",
"service_id": 7,
"staff_id": 5,
"customer_email": "ayse@example.com",
"notes": "İlk muayene",
"external_ref": "SIP-1001"
}
# → 201 { "success": true, "message": "Randevu oluşturuldu", "data": { ...randevu... } }
user_id / site_id / source gövdeden alınmaz — anahtardan çözülür (source = api).external_ref senin kendi kimliğindir. Aynı değer ikinci kez randevu
yaratmaz, 422 { errors: { external_ref: [...] } } döner → ağ zaman aşımında isteği
tekrarlaman çift randevu + çift SMS üretmez. Mevcut kaydı bulmak için
GET /api/external/appointments?external_ref=SIP-1001. Tekillik hesap kapsamındadır.available-slots sor.external_ref tekrarı ·
alan doğrulaması · yabancı service_id/staff_id (başka sitenin/hesabın id'si).GET /api/external/appointments?status=confirmed&date_from=2026-08-01&date_to=2026-08-31&per_page=50
# diğer süzgeçler: service_id, staff_id, external_ref (TAM eşleşme), page
# → { "success": true,
# "data": [ { "id": 42, "external_ref": "SIP-1001", "site_id": 12, "status": "pending",
# "appointment_date": "2026-08-12", "start_time": "10:00", "end_time": "10:45",
# "service_id": 7, "service_name": "Muayene",
# "staff_id": 5, "staff_name": "Dr. Mehmet",
# "customer_name": "...", "customer_phone": "...", "customer_email": "...",
# "notes": null, "cancel_reason": null, "source": "api",
# "created_at": "...", "updated_at": "..." } ],
# "meta": { "current_page": 1, "per_page": 50, "total": 1, "last_page": 1 } }
per_page tavanı 100. Sıralama: tarih ↓, saat ↑, id ↑ (sayfalar arası kayma olmaz).PATCH /api/external/appointments/42/status
{ "status": "cancelled", "cancel_reason": "Müşteri talebi" }
# status: pending | confirmed | cancelled | completed | no_show
# → 200 { "success": true, "message": "Durum güncellendi", "data": { ...randevu... } }
# → 404 { "success": false, "error": "Randevu bulunamadı" } (başka sitenin randevusu)
cancelled → müşteri + personel iptal SMS'i ve
iptal e-postası; confirmed → onay SMS'i ve onay e-postası. Toplu durum güncellemesi yaparken
bunu hesaba kat.Bu uçların yanıtları müşteri kişisel verisi taşır (ad, telefon, e-posta, not) — randevu yönetiminin doğası budur. Sorumluluğun:
regenerate-external-key ile ANINDA döndür.Dış API'den açtığın randevular da appointment.created / appointment.status_changed olaylarını
üretir (§10 Webhook'lar) ve payload'daki source alanı api olur → kendi yazdığın randevuyu
panelden/widget'tan gelenden ayırt edebilirsin. Böylece "ben yazdım" ile "müşteri panelden değiştirdi"
akışlarını tek defterde birleştirebilirsin.
Sitenin AI'ının cevap verirken kullandığı bilgi. Hepsi sites/{site}/... altında.
GET /api/sites/{site}/faqs
POST /api/sites/{site}/faqs { "question": "...", "answer": "..." }
PUT /api/faqs/{faq}
DELETE /api/faqs/{faq}
POST /api/sites/{site}/faqs/batch # toplu EKLE (yeni SSS'ler)
POST /api/sites/{site}/faqs/batch-update { "updates": [{ "id": 12, "question": "...", "answer": "..." }] } # toplu DEĞİŞTİR (var olanları)
POST /api/faqs/{faq}/ai-rewrite { "instruction": "5551234567 numarasını kaldır" } # tek SSS'i AI talimatla yeniden yaz
POST /api/sites/{site}/faqs/extract-from-text # metinden SSS çıkar (AI)
# { "content": "...(min 100 karakter)", "auto_save": false, "product_name": "..." }
POST /api/sites/{site}/faqs/extract-from-image (görselden)
DELETE /api/sites/{site}/faqs/delete-all
Görsel (maks 6): önce
POST /api/media/uploadile yükle, dönenfile.iddeğerlerini sıralı ver —POST/PUTgövdesinde"media_file_ids": [12, 15]. İlk eleman kapak olur (legacy tekilmedia_file_idhâlâ kabul edilir). Liste pivotu BİREBİR değiştirir:[]tüm resimleri kaldırır, alanı hiç göndermezsen resimlere dokunulmaz. Yalnız kendi medya havuzundaki dosyalar kabul edilir.faqs/batchsatırlarında damedia_file_idsverilebilir;faqs/batch-updategörsel ALMAZ (yalnızid/question/answer/category).
Belgeler/doküman yükleme uçları KALDIRILDI (2026-08-03): eski
sites/{site}/documentsailesi hiç tüketilmeyen yarım bir özellikti (yüklenen doküman bot tarafından okunmuyordu) ve tamamen kaldırıldı. Doküman/katalog içeriğini bota öğretmenin yolu: metninifaqs/extract-from-text'e ver ya da hazır soru-cevaplarıfaqs/batchile bas.
Ne işe yarar: Panelin SSS Stüdyosu ekranı — SSS'leri düzenlerken (a) aralarına "devam butonu" ilişkileri kurmak, (b) botu yan etkisiz denemek için. İkisi de sitenin normal SSS havuzu (§ yukarısı) üzerinde çalışır; ayrı bir bilgi tabanı yoktur.
Panel ekranı: https://<marka-host>/admin/sites/{site}/faq-universe-builder (yol adı eski "SSS Galaksisi"
sahnesinden devralındı, bilinçli korundu — dış linkler kırılmasın).
GET /api/sites/{site}/faq-universe/graph # düğümler (SSS) + kenarlar (ilişkiler)
POST /api/sites/{site}/faq-universe/analyze # AI kümeleme — ASENKRON, işi KUYRUĞA ATAR (10/dk)
# → 200 { "success": true, "status": "running", "started_at": "..." }
# → 409 { "error": "already_running" } aynı sitede ikinci koşum reddedilir
GET /api/sites/{site}/faq-universe/analyze-status # durum yoklama — panel 3 sn'de bir çağırır
# → 200 { "status": "idle" | "running" | "done" | "failed", ... }
# running→ "progress": { "completed": 3, "total": 7 } (büyük sitede partili koşum)
# done → "analysis": { faq_updates, new_faqs, edges, meta } + "consumed": true|false
# meta: { faq_count, batch_total, batches_completed, partial }
# failed → "error_tr": "<kullanıcıya gösterilebilir TR mesaj>"
POST /api/sites/{site}/faq-universe/analyze-ack # öneri paketini "tüketildi" damgala (yayından SONRA)
POST /api/sites/{site}/faq-universe/publish # düğüm + ilişki + yeni SSS'leri ATOMİK yayınla
POST /api/sites/{site}/faqs/batch-update # toplu soru/cevap/kategori değişimi (§ SSS (FAQ))
# TEK ilişki yönetimi (2026-08-15) — publish'in aksine grafın geri kalanına DOKUNMAZ
POST /api/sites/{site}/faq-universe/relations # ekle/güncelle (upsert) (adlı limit: 60/dk)
# { "source_faq_id": 101, "target_faq_id": 202, "button_label": "Taksit Seçenekleri", "delivery_mode": "direct" }
# → 200 { "success": true, "created": true|false, "relation": { "relation_id": 12, ... } }
DELETE /api/sites/{site}/faq-universe/relations # sil — (source,target) ÇİFTİYLE (adlı limit: 60/dk)
# { "source_faq_id": 101, "target_faq_id": 202 } → 200 { "deleted": true } | 404 relation_not_found
⛔
publishTÜM grafı değiştirir (o siteye ait ilişkiler silinir ve gövdedeki listeden yeniden kurulur). Tek bir ilişkiyi eklemek/silmek içinrelationsuçlarını kullan;publish'i "graf tazeleme" sanıp eksik gövdeyle çağırırsan geri kalan ilişkileri kaybedersin.
relationssözleşmesi: aynı(source_faq_id, target_faq_id)çifti varsa güncellenir (created:false).button_labelboş/gönderilmemişse hedef SSS'in buton başlığına, o da yoksa sorusuna düşülür ve 20 karaktere sığdırılır. Her iki SSS de bu siteye ait olmalı: değilse404 faq_not_found(aynı cevap "yok" için de döner — id keşfi yapılamaz).source_faq_id == target_faq_id→422 same_faq. Silmede gerçek satır kimliğine ihtiyacın olursagraphçıktısındakiedges[].relation_idalanını kullan (edges[].idsentetiktir, panel içindir).analyzeneden asenkron: 200+ SSS'li sitede tek AI turu dakikalar sürüyor ve senkron HTTP altyapı timeout'una takılıyordu. Doğru istemci akışı:analyze→status:"running"→analyze-status'u 3 sn aralıkla yokla →donegelinceanalysisbloğunu kullan.⚠️
publishmevcut SSS'lerde YALNIZbutton_label+delivery_modeyazar. Soru/cevap/kategori değişikliğini ayrıcaPUT /api/faqs/{faq}(veya toplufaqs/batch-update) ile göndermen gerekir — yoksa metin düzenlemen sessizce kaybolur.⚠️ Buton sırası anlamlıdır: runtime bir SSS'in ilk 3 ilişkisini alır;
publishilişkileri gönderim sırasına göre yazar. Diziyi sıralaman gerçek bir ayardır.ℹ️
analysis.edgesBOŞ ya da kısa gelebilir — bu bir hata değildir (2026-08-15). Kümeleme artık yalnız gerçek müşteri yolculuğu ilişkilerini önerir; eskiden bağsız kalan her SSS'e yapay bir bağ üretiliyordu (alakasız butonlar). İlişkisi olmayan SSS bağsız kalır.ℹ️
analysis.faq_updatesyalnız DÜZELTİLMESİ GEREKEN SSS'leri içerir (2026-08-15). Etiketi zaten kurallara uyan SSS listeye girmez →publish'e gönderirken o düğümlere dokunma, mevcut değerleri korunur.ℹ️ Büyük site = partili koşum (>150 aktif SSS). İş ~150'lik partilere bölünür, sunucu partileri sırayla koşar ve gerekirse kendini zincirler;
runningyanıtındakiprogressilerlemeyi verir. Bir parti çökerse o ana kadarki sonuçdoneolarak döner veanalysis.meta.partial = trueolur (batches_completedkaç partinin işlendiğini söyler). Yalnız İLK parti çökersefailedgelir. Sebep: tek turda tüm liste istendiğinde model çıktı tavanına takılıp JSON'u kırpıyor ve analiz komple boşa gidiyordu.
analyze-ackidempotent ve yumuşaktır (kayıt yoksa/süresi dolmuşsa/zaten tüketilmişse de 200) — best-effort çağır, başarısızlığı yayını geçersiz kılmaz.
POST /api/sites/{site}/playground/chat # deneme sohbeti (adlı limit: 20/dk)
# { "message": "kargo ücreti ne kadar", "session_id": "...", "mode": "ai"|"direct_gate"|"both",
# "system_prompt_override": "...", "drafts": [ { "question": "...", "answer": "..." } ] }
# → cevabın yanında "neden böyle cevapladı" telemetrisi + ≈kredi
GET /api/sites/{site}/playground/personas # persona kartları + kota + limitler
GET /api/sites/{site}/playground/sample-questions # sitenin GERÇEK son müşteri soruları (?limit ?days)
POST /api/sites/{site}/playground/dialogs # bot-bota diyalog BAŞLAT (adlı limit: 5/dk)
GET /api/sites/{site}/playground/dialogs/{id} # ilerleme yoklama (store KUYRUĞA ALIR, hemen döner)
POST /api/sites/{site}/playground/dialogs/{id}/stop
GET /api/sites/{site}/playground/scenarios # senaryo kütüphanesi (koşum tarifleri)
POST /api/sites/{site}/playground/scenarios
PATCH /api/sites/{site}/playground/scenarios/{id}
DELETE /api/sites/{site}/playground/scenarios/{id}
POST /api/sites/{site}/playground/scenarios/{id}/run # tek senaryo koş (adlı limit: 5/dk)
POST /api/sites/{site}/playground/scenarios/run-all # en çok 10 senaryo (adlı limit: 5/dk)
Yan etki YOKTUR (garanti, sunucu tarafında sabit): yan-etkili fonksiyonlar (insana aktar / SMS / randevu…) dry-run stub'lanır, Mesaj Kutusu'na hiçbir şey düşmez, abonelik mesaj sayacı işlemez, SSS istatistikleri ve direkt-cevap aday panosu kirlenmez. Ama gerçek motor maliyeti krediden düşer.
mode:"both"iki koşum yapar: önce ⚡ direkt-cevap kapısı (yalnız embedding, AI turu yok), sonra 🤖 AI yolu.drafts[]= henüz KAYDEDİLMEMİŞ SSS taslakları (maks 5) — prompta negatif id ile girer, DB'ye yazılmaz.Diyalog/senaryo koşumları aylık soru kotasını Bot Sınavı ile PAYLAŞIR (her müşteri turu 1 soru; kota
playground/personasyanıtındakiquota). Kota bitince422 quota_exceeded;run-allkısmi başarıda yine 200 döner ve kalanlarıskipped[]içinde gerekçesiyle listeler.Bot ana şalteri kapalı sitede diyalog BAŞLATILAMAZ (
bot_disabled) — manuel deneme sohbeti (playground/chat) bu kapıya tabi değildir.⚠️ Bu uçlar site düzenleme yetkisi ister (site sahibi / yetkili alt kullanıcı). Yalnız görüntüleme yetkisi olan ajan hesapları
403alır — tek istisna salt-okumafaq-universe/graph.
Stüdyoda kurduğun ilişkiler, gerçek müşteri sohbetinde cevabın altına tıklanabilir öneri butonu olarak çıkar. Bunun için dört kapı birden açık olmalı (hepsi fail-closed — biri kapalıysa buton yok, sessizce, hata dönmeden):
| # | Kapı | Kim açar |
|---|---|---|
| 1 | Platform kill-switch FAQ_BUTTONS_ENABLED |
DoWaba operatörü (env) |
| 2 | Site opt-in settings.faq_buttons_enabled |
Sen — PUT /api/sites/{site} gövdesinde { "settings": { "faq_buttons_enabled": true } } |
| 3 | Kanal, açık kanallar listesinde mi (FAQ_BUTTONS_CHANNELS) |
DoWaba operatörü (env) |
| 4 | Kanalın teknik hazırlığı | otomatik — kanalda gerçekten buton gönderebilen aktif profil var mı |
Bugün kod dalı yazılmış kanallar: whatsapp (Cloud API — Evolution/QR profillerinde buton ASLA
gitmez), telegram, messenger, instagram, widget. Listede olup dalı olmayan kanal buton
üretmez (sessiz no-op, mevcut davranış birebir korunur).
Butonun cevaba eklendiği yollar: (a) SSS'e verbatim cevap verildiğinde o SSS'in butonları, (b) müşteri bir butona bastığında hedefin butonları (zincir sürer), (c) AI cevabı açıkça tek bir SSS'e dayanıyorsa o SSS'in butonları. Zincir yalnız kullanıcı tıklamasıyla ilerler — otomatik zincirleme yoktur.
Butonları mesaj kaydından okuma (kanal API'sine geri sormana gerek yok — damga mesaj satırının
metadata alanındadır; ör. GET /api/whatsapp/messages/{phone} yanıtındaki her mesajın metadata'sı):
// GİDEN satır (butonlu bot cevabı):
"metadata": { "faq_buttons": [ { "id": "faqbtn_42", "title": "Kargo ücreti", "target_faq_id": 42 } ] }
// GELEN satır (müşteri butona bastı):
"metadata": { "faq_button_click": { "target_faq_id": 42, "label": "Kargo ücreti" } }
Cevap metni kanalın gövde sınırını aşarsa butonlar kısa bir ikinci mesajla gider — damga o zaman o ikinci satıra düşer.
faq_button_click.target_faq_idmüşteri iddiasıdır (webhook girdisi); raporlama/gösterim için kullan, yetki kararı için değil.
GET /api/sites/{site}/functions
POST /api/sites/{site}/functions # DB sorgusu veya HTTP API fonksiyonu tanımla
POST /api/functions/{function}/test
POST /api/functions/{function}/toggle { "site_id": 159, "is_active": false }
# GLOBAL fonksiyonda: site_id + is_active ZORUNLU (idempotent). Merkezi tanım kopyalanmaz/değişmez,
# yalnız hedef sitenin izole disable listesi güncellenir. Yasal/lifecycle korumalı sistem
# fonksiyonlarında 422 "protected_system_function".
# SİTEYE ÖZEL fonksiyonda: site_id gerekmez, is_active OPSİYONELDİR — ama ⚠️ göndermezsen uç mevcut
# durumu TERS ÇEVİRİR (flip) → retry/çift istek durumu geri alır. Her zaman is_active gönder.
# Harici bağlantılar (DB/HTTP):
GET /api/sites/{site}/connections
POST /api/sites/{site}/connections
POST /api/connections/{connection}/generate-functions # tablodan otomatik fonksiyon üret (AI)
# Hazır modül paketi yükle — connection + TÜM fonksiyonlar tek seferde:
POST /api/sites/{site}/bundles/import { "manifest": { ...JSON... }, "api_key": "<secret>", "replace": true }
# veya dış URL'den: { "manifest_url": "https://...", "api_key": "<secret>" }
# → "manifest" (JSON gövde) ÖNERİLİR: dış URL fetch YOK = SSRF yüzeyi sıfır. manifest|manifest_url biri zorunlu.
# api_key = connection bearer/api_key sırrı; replace=true mevcut paketi günceller (idempotent re-sync).
#
# Fonksiyon şablon değişkenleri (url_template / body_template / headers içinde {{...}}):
# {{arg.X}} → AI'ın doldurduğu fonksiyon argümanı (model/kullanıcı girdisi → DOĞRULANMAMIŞ)
# {{connection.base_url}} ... → connection'ın kayıtlı (şifreli) değerleri
# {{user.id|phone|name|email}} → DOĞRULANMIŞ son-kullanıcı kimliği: web widget'ta imzalı data-user-token'dan
# çözülen kullanıcı, WhatsApp/voice'ta Meta-imzalı telefon. Sistem otomatik
# enjekte eder → AI/kullanıcı SPOOF EDEMEZ. "Giriş yapanın KENDİ verisi"
# isteyen fonksiyonlarda (siparişlerim, derslerim, şifre-sıfırlama) kimliği
# {{arg.*}} parametresi YAPMA → body_template'te {{user.id}}/{{user.phone}}
# kullan. Anonim kullanıcıda boş gelir (istek o alan olmadan gider).
Ne işe yarar: Site başına TEK herkese açık işletme profili — https://dowaba.com/isletme/{slug}.
İçerik blok bazlıdır (hero, hizmetler, iletişim, konum, çalışma saatleri, galeri, SSS…) ve sayfada
gösterilecek SSS'ler §8'deki SSS havuzundan seçilir. Akış Özel Entegrasyonlar'ın aynısıdır:
draft → submit → pending → superadmin approve/reject → sahibi yayınlar.
GET /api/sites/{site}/business-page # sayfa + blok şemaları + sektör/il listeleri + ön-dolum + SSS'ler
PUT /api/sites/{site}/business-page { "sector": "beauty", "city": "istanbul", "district": "Kadıköy",
"seo_title": "...", "seo_description": "...",
"settings": { "hero": { "title": "..." } } } # KISMİ merge
GET /api/sites/{site}/business-page/faqs # SSS seçici — ?q=iade&page=2 (50'şer; show ilk sayfayı taşır)
POST /api/sites/{site}/business-page/ai-fill # sitenin promptu + SSS'lerinden metinleri üret (adlı limit: 10/dk)
POST /api/sites/{site}/business-page/chat # sohbet sihirbazı — STATELESS taslak (adlı limit: 20/dk)
POST /api/sites/{site}/business-page/upload # multipart tek görsel (maks 15 MB) → WebP URL
POST /api/sites/{site}/business-page/submit # onaya gönder (draft|rejected → pending)
POST /api/sites/{site}/business-page/publish { "is_published": true } # yalnız approved
POST /api/sites/{site}/business-page/preview-link # imzalı önizleme linki (30 dk)
POST /api/sites/{site}/business-page/checkout # tek seferlik yayın kilidi (PayTR) (adlı limit: 5/dk)
403 { "error": "restricted_account" }
(sayfa dowaba.com markasında yayınlanır, white-label kapsam dışı). Modül anahtarı YOKTUR —
yetki SitePolicy + bu guard'dan gelir.GET yanıtı tek çağrıda panelin ihtiyacı olan her şeyi verir: page (+ public_url, og_image_url),
schemas (blok şemaları, kullanıcı dilinde etiketli), can_manage, entitlement, sectors,
cities, prefill (kanal profillerinden deterministik ön-dolum), faqs (ilk 50) + faqs_total +
selected_faqs. Sayfa yoksa taslak olarak otomatik oluşturulur.PUT kısmi: yalnız gönderdiğin blok/alan değişir. ⚠️ Onaylı sayfada moderasyona tabi bir alan
(settings · seo_title · seo_description · sector · city · district) değişirse sayfa
yeniden onaya düşer ve yayından kalkar → yanıtta requires_reapproval: true. slug ASLA değişmez.
sector/city backend whitelist'lerinden seçilir (GET yanıtındaki sectors/cities).chat hiçbir şey kaydetmez: geçmişi sen taşırsın (messages, maks 20 tur × 2000 karakter),
uç reply + draft_settings + draft_meta + missing_fields + done döner; kaydı PUT
yapar (moderasyon kuralı da orada işler). ai-fill ise doğrudan yazar — kaynak yoksa (ne sistem
promptu ne SSS) LLM'e hiç gidilmez: 422 { "error": "no_content" }. İkisi de AI kredisi/anahtarı
yoksa 422 no_key.upload istek başına tek dosya (image alanı, JPG/PNG/WEBP/GIF, maks 15 MB, 40 MP tavanı);
çıktı her zaman WebP'tir, EXIF taşınmaz, en uzun kenar 3000px'e küçültülür. Dönen url galeri
alanının kabul ettiği tek biçimdir; çoklu yükleme = bu ucu paralel çağırmak.publish yalnız approved sayfada çalışır (aksi 422 not_approved). ⚠️ Tek başına sayfayı public
YAPMAZ: yayın kilidi kapalıysa /isletme/{slug} 404 kalır.preview-link yayın/onay/kilit ŞARTI ARAMAZ — taslak sayfayı da gösterir. Dönen URL imzalıdır ve
30 dakika yaşar (Authorization taşımaz, noindex + no-store servis edilir), kökü daima
dowaba.com'dur (bayi panel domain'inden istesen bile).Yayın kilidi (Monetizasyon v2): hazırlamak/onaya göndermek/önizlemek ücretsizdir; ücrete bağlı
olan HERKESE AÇIK YAYINdır. entitlement = { unlocked, source, yearly_eligible, price } — aktif
yıllık abonelikte dahildir (source: "yearly_sub", abonelik sürdükçe), yoksa tek seferlik
satın almayla kalıcı açılır (one_time; fiyat UI'da hardcode edilmez, entitlement.price'tan
okunur). checkout yalnız ödemeyi başlatır (purchase_id + iframe_token + payment_url); kilidi
açan tek yer PayTR callback'idir — tarayıcıdan dönen "başarılı" ekranı kilit açmaz. Kapılar:
ödeyen site sahibi olmalı (403 not_owner), zaten açıksa 422 already_unlocked, yönetici kilidi
varsa 422 admin_locked, PayTR hatasında 502 checkout_failed.
Public taraf (auth yok, senin çağırmana gerek yok): /isletmeler (dizin) ·
/isletmeler/sektor/{sector} (sektör hub'ı) · /isletme/{slug} (sayfa) · /isletme/{slug}.md
(makine-okunur ikiz) · /isletme-onizleme/{page} (yalnız imzalı önizleme linkiyle). Görünürlük =
onaylı ∧ yayında ∧ kilit açık; üçünden biri eksikse sayfa 404'tür.
GET /api/me/usage # birleşik mesaj+voice snapshot (dashboard)
GET /api/usage # detay
GET /api/usage/stats
GET /api/credits # bakiye
GET /api/credits/transactions
GET /api/me/credit-summary # header badge için rol-aware özet
GET /api/subscriptions/plans
GET /api/subscriptions/current
GET /api/subscriptions/usage
GET /api/subscriptions/price-quote?plan_id=3&sites_count=5&period=monthly # → fiyat
POST /api/subscriptions/initiate { "plan_id": 3, "period": "monthly", "sites_count": 5 }
POST /api/subscriptions/change-sites
# Ödeme: PayTR (TR), Stripe (USD, otomatik webhook), PayPal (USD, manuel onay)
POST /api/subscriptions/stripe/checkout
POST /api/subscriptions/paypal/initiate
plan_id=GET /api/subscriptions/plansyanıtındaki sayısalid'dir (slug DEĞİL — "business" gönderirsen 422).periodher iki uçta da ZORUNLU:monthly | quarterly | semiannual | yearly(lifetime yeni satışta yok). Gizli partner paketlerinde ayrıcapackage_uuidzorunludur.
GET /api/ai-credit/catalog
GET /api/ai-credit/summary # bakiye
GET /api/ai-credit/transactions
POST /api/ai-credit/stripe/checkout | /paytr/checkout | /paypal/initiate
Cüzdan neyi fonlar: Kullanıcının (veya bayisinin) kendi Gemini anahtarı yoksa AI maliyetleri bu cüzdandan düşülür — yalnız sohbet mesajları (tüm kanallar) değil, sesli çağrılar (AI'ın karşıladığı telefon, 2026-06-14'ten itibaren) ve canlı çeviri (telefon aktarımında çapraz dil) de.
transactionsyanıtındakisourcealanı kalemi ayırır (ör.voice_conversation,voice_translate). Maliyet gerçek token kullanımından (GeminiusageMetadata) hesaplanır × kâr marjı; sesli kalem metinden pahalıdır (ses çıkışı yüksek oranlı). Bakiye biterse AI yanıt veremez → kullanıcı paket satın almalı (catalog→checkout).
Ne işe yarar: Kullanıcının kendi API anahtarı (Sanctum PAT), outbound webhook ve OAuth client'ı. Senin trusted-partner client'ından AYRIDIR — bu, kullanıcının panelden kendi entegrasyonunu açması içindir.
⚠️ Bu endpoint'lere erişim için kullanıcının
developermodülü açık olmalı (allowed_modules). Ayrıca token/webhook/oauth oluşturmadan önce geliştirici şartları kabulü zorunlu:GET /api/me/developer-terms→POST /api/me/developer-terms/accept.
GET /api/me/tokens
POST /api/me/tokens { "name": "Entegrasyonum" } # 10/dk — plain token bir kez döner
DELETE /api/me/tokens/{id}
GET /api/webhook-endpoints
POST /api/webhook-endpoints { "url": "https://...", "events": ["message.received"],
"site_id": 12, "full_payload": false, "accept_policy": true } # 20/dk
PATCH /api/webhook-endpoints/{id}
POST /api/webhook-endpoints/{id}/test
GET /api/webhook-endpoints/{id}/deliveries # SON 20 teslimat (debug; sayfalama/filtre yok, payload dönmez)
DELETE /api/webhook-endpoints/{id}
Abone olunabilir olayların TEK doğru listesi
GET /api/webhook-endpointsyanıtındakiavailable_eventsalanıdır — listede olmayan bir olaya abone olmayı denersen422alırsın. Kullanıcı başına en fazla 10 endpoint; URL https olmak zorunda;accept_policy: trueve geliştirici şartları kabulü (me/developer-terms) olmadan endpoint yaratılamaz.
site_id vermezsen endpoint GENEL olur — TÜM sitelerin oraya akar
site_idopsiyoneldir ve kritiktir:
site_idverilmezse endpoint kullanıcı-genelidir → o kullanıcının bütün sitelerinin olayları (her kanal, her müşteri) bu tek URL'e gider.site_idverilirse endpoint yalnız o siteye kilitlenir. Site çözülemeyen olaylar site-kilitli endpoint'e gönderilmez (site sözleşmesi korunur).- Panelde
site_idalanı YOKTUR (Geliştirici → Webhook'lar formu url + olaylar +full_payloadgönderir) → panelden oluşturulan her endpoint GENELDİR. Tek bir siteye kapsamlamak istiyorsan endpoint'i API'densite_idile oluştur.site_idsonradanPATCHile değiştirilemez — yanlış kapsamla oluşturduysan sil ve doğrusite_idile yeniden oluştur.- Verdiğin
site_idsenin erişebildiğin bir site değilse422döner.Çok kiracılı (birden çok site yöneten) bir entegrasyon yazıyorsan: ya site başına ayrı endpoint aç, ya da tek genel endpoint'te gelen
data.site_idalanına göre kendi tarafında ayır.
Olaylar:
| Olay | Ne zaman | Kanallar | Varsayılan |
|---|---|---|---|
message.received |
Yeni gelen mesaj | WhatsApp (Cloud API + QR/Evolution + Coexistence), Instagram DM, Messenger, Telegram, TikTok DM, X DM, Mail, site Widget'ı | her kurulumda açık |
message.status |
Giden WhatsApp mesajının teslim durumu ilerledi (sent → delivered → read) veya failed oldu |
yalnız WhatsApp Cloud API | kurulum bazlı kapalı — available_events'te görünmüyorsa sunucuda açık değildir |
appointment.created |
Yeni randevu oluştu | panel · public widget · AI (WhatsApp/Telegram/sesli) · API | kurulum bazlı kapalı |
appointment.status_changed |
Randevunun durumu gerçekten değişti (pending → confirmed → …) |
panel · onay sayfası · AI · API | kurulum bazlı kapalı |
message.received — kanalı data.channel alanından ayırt et
(whatsapp|instagram|messenger|telegram|tiktok|x|mail|widget); data.from kanala göre telefon /
platform kullanıcı ID'si / e-posta / widget oturum ID'sidir. Mail ve X-sync geçmiş taramalarında
48 saatten eski mesajlar webhook tetiklemez (backfill koruması).
// data (message.received)
{
"channel": "whatsapp", "site_id": 192, "message_db_id": 90210,
"message_id": "wamid.HBg...", "from": "+905551112233",
"contact_name": "Ayşe Yılmaz", "text": "Merhaba", "direction": "in",
"has_media": false, "timestamp": "2026-08-02T10:00:00+03:00"
}
message.status (teslimat makbuzu, 2026-08-02) — WhatsApp Cloud API'de teslimat bilgisi
sorgulanamaz, yalnız Meta'nın gönderdiği makbuzla öğrenilir; bu olay onu sana aktarır.
Yalnızca durum gerçekten ilerlediğinde üretilir: geç gelen bir sent makbuzu zaten read olmuş
bir mesajı geri almaz → olay da üretilmez. Aynı mesaj için tipik olarak sent, delivered, read
sırasıyla 3 ayrı olay alırsın (yüksek hacim — bu yüzden kurulum bazlı kapalıdır).
// data (message.status)
{
"channel": "whatsapp", "site_id": 192, "message_db_id": 90211,
"message_id": "wamid.HBg...", // Meta wamid — gönderim yanıtındaki id ile eşleşir
"to": "+905551112233", // ALICI (message.received'daki from'un karşılığı)
"direction": "out",
"delivery_status": "delivered", // sent | delivered | read | failed
"delivery_error": null, // yalnız failed'da dolu (Meta'nın hata başlığı)
"timestamp": "2026-08-02T10:00:03+03:00"
}
message.received'dan farkları: yönout, muhatap alanıfromdeğilto, mesaj metni/adı yoktur (text/contact_namealanları bu olayda hiç bulunmaz),has_mediayoktur. Maskelemede fark yoktur:to,fromile birebir aynı kuralla maskelenir. Mevcut endpoint'ler yalnızmessage.received'e abone olduğu için bu olay onlara gitmez — almak istiyorsan endpoint'ineventslistesinemessage.status'ü eklemen gerekir.
appointment.created / appointment.status_changed (randevu yaşam döngüsü, 2026-08-07) —
randevu hangi kanaldan açılırsa açılsın aynı olayı alırsın: panel, public widget
(/api/widget/appointment/book), müşteriye giden onay bağlantısı, AI botu (WhatsApp/Telegram/sesli
asistan) ve POST/PUT/PATCH /api/appointments* uçları. Kendi takvimini DoWaba ile senkron tutmak
için polling'e gerek kalmaz.
// data (appointment.created — full_payload: true endpoint)
{
"appointment_id": 4211,
"site_id": 192, // randevu bir siteye bağlı değilse null
"external_ref": "SIP-1001", // senin gönderdiğin kimlik; göndermediysen null
"status": "pending", // pending | confirmed | cancelled | completed | no_show
"appointment_date": "2026-08-12",
"start_time": "10:00", "end_time": "10:30",
"staff_id": 7, "service_id": 3,
"source": "widget", // widget|whatsapp|admin|phone|instagram|bildirim_ai|chatbot|telegram|voice
"occurred_at": "2026-08-07T10:00:00+03:00", // ISO-8601, sunucu saat dilimi ofsetiyle
// ↓ YALNIZ full_payload: true endpoint'lerde bulunur
"customer_name": "Ayşe Yılmaz", "customer_phone": "+905551112233",
"customer_email": "ayse@ornek.com", "notes": "Alerjisi var"
}
appointment.status_changed aynı alanları taşır, ek olarak bir tane daha:
{ "...": "...", "previous_status": "pending", "status": "confirmed" }
Payload'da OLMAYAN alanlar: iptal sebebi (
cancel_reason) ve randevunun onay/takvim token'ları webhook gövdesine hiç konmaz — biri serbest metin PII, diğerleri müşteriye özel sırdır. İptal sebebine ihtiyacın varsaGET /api/appointments/{id}ile çek.
Ne zaman gelmez: durumu değiştirmeyen güncellemeler (not/saat/personel düzeltmesi, aynı durumun tekrar yazılması) ve randevu hatırlatma gönderimleri olay ÜRETMEZ. Randevu silinmesi de olay üretmez (bu sürümde kapsam dışı) — silinenleri yakalamak istiyorsan
GET /api/appointmentsile mutabakat yap.auto_confirmaçık bir kurulumda randevu doğrudanconfirmeddoğar: bu bir durum değişimi değildir, yalnızappointment.createdalırsın (status: "confirmed"ile).PII: maskeli (varsayılan) endpoint'lerde
customer_name,customer_phone,customer_emailvenotesanahtarları payload'da hiç bulunmaz —nulldeğil, yok. Randevuyuappointment_id(ve gönderdiysenexternal_ref) ile eşleştir, ayrıntıyı gerektiğindeGET /api/appointments/{id}ile çek. Mesaj olaylarındakifromson-4 maskesinden farklıdır: orada muhatap TEK korelasyon anahtarıdır, burada değildir.
site_idkapsamı: panelden site seçilmeden açılan randevununsite_id'sinull'dır ve böyle bir olay site-kilitli endpoint'e gönderilmez (yalnız kullanıcı-geneli endpoint'lere). Site bazlı bir entegrasyon yazıyorsan randevuların site'e bağlı açıldığından emin ol.
İmza: X-Dowaba-Signature = ham gövdenin endpoint secret'ıyla HMAC-SHA256 hex özeti
(hash_hmac('sha256', $rawBody, $secret)) — gövdeyi JSON'a çevirmeden, byte'ı byte'ına doğrula.
Ayrıca X-Dowaba-Event ve X-Dowaba-Delivery header'ları gelir. Teslimat 3 kez denenir
(2xx dışı / 15 sn timeout → 1 dk ve 5 dk sonra tekrar) → aynı X-Dowaba-Delivery ID'si birden fazla
gelebilir; bu ID üzerinden tekilleştir.
⚠️ PII ve full_payload — sorumluluk sende. Payload müşteri kişisel verisi taşır
(telefon/ad/mesaj metni). Varsayılan maskelidir (masked=true: from/to son-4 hanesine
kısaltılır — e-postada local-part maskelenir —, contact_name ve text null'a çekilir).
full_payload webhook_endpoints tablosunda gerçek bir kolondur (varsayılan false) ve
oluşturma/güncelleme isteğinde full_payload: true göndererek kendin açarsın:
DoWaba tarafında bir onay/inceleme adımı YOKTUR. Yani "tam içerik yalnız onaylı endpoint'lere
gider" değil; Geliştirici Şartları'nı kabul edip bayrağı açan herkese tam PII akar. Bunu açmak
hukuki bir karardır: veri sorumlusu DoWaba hesabının sahibidir, sen onun adına işleyensin — KVKK
aydınlatma/saklama/imha yükümlülüğü ve olası bir ihlalin sonucu senin tarafındadır (§ 0.5 +
DPA). Öneri: bayrağı kapalı bırak, webhook'u yalnız "olay oldu" tetikleyicisi olarak
kullan, içeriği gerektiğinde API'den çek ve kendi veritabanında saklama.
Teslimat kayıtları (
GET .../deliveries) 90 gün sonra otomatik silinir; maskeli endpoint'lerde payload DoWaba tarafında da maskeli saklanır. ⚠️ Kayıtlar 90 gün dursa da bu uç yalnız son 20 teslimatı gösterir (alanlar:id,event,status,response_code,attempts,last_error,created_at— payload yok) → mutabakat için kendi tarafındakiX-Dowaba-Deliverykaydını kullan.
message.status kurulumda kapalıysa veya push kuramıyorsan, giden WhatsApp mesajlarının durumunu
konuşma bazında okuyabilirsin:
GET /api/whatsapp/messages/{phone}?profile_id=48
# → her mesaj için: id, direction, message,
# message_id (wamid — kendi kaydınla eşleştir)
# delivery_status (sent | delivered | read | failed | null)
# delivery_error (yalnız failed'da dolu)
⚠️ YAN ETKİ: bu çağrı, o telefonun gelen mesajlarını okundu olarak işaretler (panelin sohbet açma davranışıdır). Düzenli polling yapan bir entegrasyon, panel/mobil kullanıcının "okunmadı" rozetlerini sessizce yok eder — operatör yeni mesajı fark edemez. Teslimat takibi için push (
message.status) tercih edilmelidir; polling'i yalnız zorunlu kaldığında ve seyrek (örn. gönderimden 10 dk sonra tek sefer) kullan. Ayrıca her çağrı rate limit sayacından düşer (§ 0.4.2).
GET /api/me/oauth/clients
POST /api/me/oauth/clients # issues_sanctum_token gövdeden AÇILAMAZ (aşağıdaki ayrı uç)
PATCH /api/me/oauth/clients/{id}
POST /api/me/oauth/clients/{id}/rotate-secret
DELETE /api/me/oauth/clients/{id}
Bayiysen kendi uygulamanı (yan uygulama / white-label panel) kendin Güvenilir Ortak
yapabilirsin: kullanıcı senin uygulamandan "DoWaba ile giriş" yapar, token yanıtında
standart access_token'ın yanında sanctum_token da gelir ve bu token ile panel
API'larını kullanıcı adına çağırırsın (§ 0).
POST /api/me/oauth/clients/{id}/trusted-partner # gövde: {"commitment": true}
DELETE /api/me/oauth/clients/{id}/trusted-partner # kapat
Açılış koşulları (hepsi zorunlu):
partner_sso.max_trusted_clients) aşmamak, 7. commitment: true ile veri
işleme taahhüdünü onaylamak.GET /api/me/oauth/clients yanıtındaki partner_sso.requirements bloğu yalnız hesap düzeyindeki 4
koşulu gösterir (is_reseller, has_active_paid_subscription, has_accepted_reseller_agreements,
has_developer_terms). Uygulama düzeyindekiler ayrı okunur: 5. koşul clients[].is_confidential /
clients[].is_active, 6. koşul partner_sso.max_trusted_clients ↔ partner_sso.active_count; 7. koşul
zaten istek gövdesidir. Hata kodları:
403 → self_service_disabled (özellik kapalı) · reseller_required · subscription_required ·
reseller_agreements_required · developer_terms_required422 → client_inactive · public_client · limit_reached · commitment_requiredKapsam kilidi: Self-servis açılan uygulamada kapsam daima tenant'tır — bu uygulamaya
yalnız senin kendi evrenin (sen + müşterilerin + alt kullanıcıların) giriş yapabilir.
Evren dışındaki bir kullanıcı consent ekranında engellenir (out_of_scope_reason=tenant_scope),
superadmin hesapları her durumda engellenir. Bayilik aboneliğin/sözleşmen düşerse
uygulamana yeni giriş yapılamaz (owner_gate).
Kapatınca ne olur: Uygulamaya ait tüm OAuth token'ları iptal edilir ve bağlı
sanctum_token'lar silinir — kullanıcılar yeniden giriş yapmak zorunda kalır. Bayilik
aboneliğin/sözleşmen düşerse aynı temizlik otomatik yapılır (günlük partner-sso:sync):
uygulaman Güvenilir Ortak modundan çıkar, mevcut sanctum_token'lar da silinir.
Panelden: Geliştirici → OAuth Uygulamaları → ilgili uygulama kartı → "Güvenilir Ortak (SSO)". Panel giriş linki için
POST /api/oauth/login-link(§ Trusted Partner SSO).
Kendi doğrulanmış özel domain'in varsa (Bayilik → Özel Domain), giriş + izin ekranı senin adresinde açılabilir — kullanıcı URL çubuğunda hep senin markanı görür:
PATCH /api/me/oauth/clients/{id}/branded-host # gövde: {"host": "destek.firma.com"} — temizlemek için {"host": null}
Yalnız kendi yayındaki (active) domain'lerinden biri seçilebilir; seçilebilir liste
GET /api/me/oauth/clients yanıtındaki partner_sso.available_branded_hosts alanındadır.
Ayarlandıktan sonra https://dowaba.com/oauth/authorize?... isteği aynı sorgu parametreleriyle
https://destek.firma.com/oauth/authorize?... adresine 302 ile taşınır — istemci tarafında
yapman gereken bir şey yok (redirect_uri, PKCE, state aynen korunur, iss yine
https://dowaba.com). Domain yayından kalkarsa yönlendirme durur, akış dowaba.com üzerinden
sorunsuz sürer.
Kullanıcı uygulamana ilk girişte izin kartını görür; onayı kaydedilir ve sonraki girişlerde kart
atlanır. GET /api/oauth/authorize/state yanıtındaki has_prior_consent bunu bildirir.
İstediğin scope setini genişletirsen kart yeniden gösterilir. Kullanıcı izni istediği an geri
çekebilir (panel → Bağlı uygulamalar; DELETE /api/me/oauth/consents/{clientId}) — o kullanıcının
sanctum_token'ı ve OAuth token'ları anında geçersiz olur, uygulaman 401 alır ve kullanıcıyı
yeniden "DoWaba ile giriş"e yönlendirmelidir.
Entegrasyonu elle kodlamak istemiyorsan aşağıdaki prompt'u olduğu gibi kopyala, en üstteki köşeli parantezli alanları kendi bilgilerinle doldur ve kullandığın yapay zeka asistanına (Claude, ChatGPT, Cursor, Copilot...) yapıştır. Prompt, asistanın senin uygulamana/CRM'ine uçtan uca çalışan bir "DoWaba ile Giriş" entegrasyonu yazması için gereken tüm teknik gerçekleri içerir — asistanın DoWaba'yı önceden tanımasına gerek yoktur.
Ön koşul: Panel → Geliştirici → OAuth Uygulamaları'ndan client'ını oluştur (Confidential işaretli),
client_id+client_secret'ı kopyala ve Güvenilir Ortak (SSO) modunu aç (yukarıdaki bölüm). İstersen "giriş adresi" olarak kendi özel domain'ini seç — kullanıcıların gördüğü login ekranı senin markanla açılır.
Uygulamama "DoWaba ile Giriş" (OAuth 2.0 + OIDC, Güvenilir Ortak modu) entegre etmeni
istiyorum. Aşağıda sağlayıcının TÜM teknik detayları var — bunlara birebir uy, uydurma.
BENİM BİLGİLERİM (doldur):
- Uygulamam / stack: [ör. Laravel 11 + Blade / Node.js Express + React / Django ...]
- Uygulamamın adresi: [https://uygulamam.com]
- client_id: [dosc_...]
- client_secret: [dosec_...] ← YALNIZ sunucu tarafında kullanılacak
- redirect_uri (DoWaba panelinde kayıtlı olanla BİREBİR aynı): [https://uygulamam.com/oauth/dowaba/callback]
- Giriş adresi (white-label seçtiysem, yoksa dowaba.com): [https://hesap.markam.com veya https://dowaba.com]
SAĞLAYICI GERÇEKLERİ (DoWaba):
1. Authorize (tarayıcı yönlendirmesi): GET {GİRİŞ_ADRESİ}/oauth/authorize
Zorunlu query parametreleri: client_id, redirect_uri, response_type=code,
scope="openid profile email", state (CSRF için rastgele, session'da sakla ve
callback'te doğrula), code_challenge, code_challenge_method=S256.
PKCE ZORUNLUDUR: 64+ karakterlik rastgele code_verifier üret; challenge =
base64url(sha256(verifier)) (padding'siz). Verifier'ı session'da tut.
2. Token (SUNUCUDAN sunucuya): POST https://dowaba.com/api/oauth/token
Body (JSON veya form): grant_type=authorization_code, client_id, client_secret,
code (callback'teki ?code=), redirect_uri (aynısı), code_verifier.
Başarılı yanıt alanları:
access_token ("doat_..." — 1 saat ömürlü, yalnız userinfo için)
id_token (RS256 imzalı JWT; doğrulama anahtarı JWKS'te)
expires_in (3600)
refresh_token ("dort_..." — dönerse sakla; rotation vardır: her kullanımda
YENİSİ döner, eskisini bir daha KULLANMA — tekrar kullanım tüm
token ailesini iptal eder)
sanctum_token ("12|..." — GÜVENİLİR ORTAK ANAHTARI: süresizdir, kullanıcı
adına DoWaba panel API'larını çağırır. ASIL kalıcı erişim budur.)
sanctum_token'ı kullanıcı kaydında ŞİFRELİ sakla; tarayıcıya/frontend'e ASLA verme.
3. Kimlik: GET https://dowaba.com/api/oauth/userinfo
Header: Authorization: Bearer {access_token} → { sub, email, name, ... } döner.
sub = DoWaba kullanıcı ID'si; kendi tablomda kullanıcıyı "dowaba_user_id = sub" ile
eşle (Trusted Partner deseninde kendi şifre/kimlik sistemimi KURMAM — kullanıcı
havuzu DoWaba'dadır; bende yalnız sub + sanctum_token + profil kopyası durur).
4. Kullanıcı adına DoWaba API'ları: Authorization: Bearer {sanctum_token} ile
https://dowaba.com/api/... uçları (örn. GET /api/me, GET /api/sites,
GET /api/conversations). Yetki otomatik olarak kullanıcının kendi kapsamıdır;
BAŞKA kullanıcının verisine erişilemez. Not: kullanıcı bir bayinin müşterisiyse
mesaj İÇERİĞİ uçları bu anahtarla kapalıdır (403/boş) — site/profil yönetimi çalışır.
Tüm uçların dokümanı: https://dowaba.com/api-docs/ ve https://dowaba.com/gelistirici-rehberi
5. Panele geçiş (SSO): POST https://dowaba.com/api/oauth/login-link
Header: Authorization: Bearer {sanctum_token} → {"url": "..."} tek kullanımlık,
5 dk geçerli panel giriş linki döner. Uygulamama "Panele Git" butonu koy, bu URL'e
yönlendir (her tıklamada YENİ link iste, cache'leme).
6. Çıkış / bağlantıyı kesme: POST https://dowaba.com/api/oauth/revoke
Body: client_id, client_secret, token (refresh_token ya da access_token).
Revoke sanctum_token'ı da siler. Kendi session'ımı da temizle.
7. Consent-once: kullanıcı izni İLK girişte bir kez onaylar, sonraki girişler kartsız
akar. Kullanıcı izni DoWaba panelinden geri çekebilir → elimdeki sanctum_token
401 dönmeye başlar. HER 401'de: kayıtlı token'ı sil, kullanıcıyı yeniden
authorize akışına gönder (otomatik dene; kullanıcı hâlâ yetkiliyse kartsız geçer).
8. Kapsam (tenant): Bu client'a YALNIZ benim DoWaba bayi evrenimdeki kullanıcılar
(ben + müşterilerim + alt kullanıcılarım) giriş yapabilir. Kapsam dışı kullanıcı
consent ekranında "bu uygulama size açık değil" görür; uygulamam callback'te
?error=access_denied alabilir → kibar bir hata sayfası göster.
9. Hata durumları: callback'te ?error= gelirse (access_denied vb.) token isteği
YAPMA, hata sayfası göster. Token endpoint'i 4xx dönerse response body'deki
error alanını logla. state uyuşmazsa isteği REDDET (CSRF).
YAPMANI İSTEDİKLERİM:
a) /oauth/dowaba/redirect (giriş başlatma: PKCE + state üret, authorize URL'ine 302)
ve /oauth/dowaba/callback (state doğrula, kodu token'a çevir, userinfo çek,
kullanıcıyı upsert et, session aç) rotalarını yaz.
b) Kullanıcı tablosuna/koleksiyonuma dowaba_user_id (unique) + dowaba_sanctum_token
(şifreli) + email + name alanlarını ekleyen migration/şema değişikliği.
c) "DoWaba ile Giriş" butonu + "Panele Git" (login-link) butonu + "Bağlantıyı kes"
(revoke + yerel çıkış) akışı.
d) sanctum_token ile DoWaba API çağrısı yapan, 401'de token'ı temizleyip yeniden
yetkilendirmeye yönlendiren küçük bir API istemci sınıfı/yardımcısı.
e) Güvenlik kuralları: client_secret ve sanctum_token yalnız sunucuda; tüm istekler
HTTPS; state + PKCE doğrulaması atlanamaz; token'lar loglanmaz.
f) Sonunda elle test adımlarını listele (giriş → panel API çağrısı → login-link →
izin geri çekme → 401 kurtarma).
Kodun tamamını benim stack'ime uygun, çalışır halde yaz. Bilmediğin bir davranışı
varsayma — yukarıdaki gerçeklerle çelişen hiçbir şey ekleme.
Prompt'taki akış klasik "Authorization Code + PKCE" olduğu için asistanın üreteceği kod
standart OAuth kütüphaneleriyle de (Laravel Socialite custom provider, openid-client,
authlib...) uyumludur — asistan kütüphane kullanmayı seçerse endpoint'leri ve
sanctum_token alanını yukarıdaki gibi elle eşlemesi yeterli.
Canonical MCP resource:
https://dowaba.com/mcp
Bu yüzey REST panel API'si değildir; MCP Streamable HTTP + JSON-RPC kullanır.
ChatGPT uygulamasında Dowaba hesabı OAuth Authorization Code + PKCE ile bağlanır.
MCP client public/predefined'dır, resource=https://dowaba.com/mcp ile
audience-bound token alır ve Trusted Partner sanctum_token kullanmaz.
Akış:
expected_revision + idempotency_key ile canlı olmayan taslak oluşturulur.MCP mesaj/konuşma okumaz, mesaj göndermez, bot açıp kapatmaz, credential döndürmez ve site/kanal lifecycle'ına dokunmaz. ChatGPT belleği Dowaba'nın eriştiği bir API değildir; ChatGPT yalnız kullanıcının konuşmada gördüğü yapılandırılmış persona ve doğrulanmış SSS sonucunu gönderir. Ham bellek veya ham konuşma saklanmaz.
Discovery:
GET /.well-known/oauth-protected-resource/mcp
GET /.well-known/oauth-authorization-server
GET /.well-known/openai-apps-challenge
Araç adları, OAuth scope'ları, publish/rollback gate'leri ve üretim runbook'unun
iç operasyon tek otoritesi repo kökündeki MCP.md dosyasıdır. Public entegrasyon
desteği için aydin@dowaba.com ile iletişime geç.
GET /api/auth/user # kimlik + role + allowed_modules (§0.3)
GET /api/user # profil detay
PUT /api/user # profil güncelle
PUT /api/user/locale { "locale": "tr" }
PUT /api/user/menu-settings # sol menü görünürlüğü
GET /api/settings # kullanıcı global ayarları (key-value; API anahtarları maskeli)
PUT /api/settings
GET/PUT /api/settings/gemini-models # Gemini model tercihi (hesap geneli)
PUT /api/settings/gemini-fallback # yoğunlukta devreye giren yedek model zinciri
GET /api/settings/openai-chat-models # OpenAI sohbet modeli KATALOĞU (allowlist + fiyat) — SALT OKUNUR
PUT /api/settings { "chat_provider_default": "gemini|openai",
"openai_chat_model_default": "gpt-5.6-terra" }
GET/PUT /api/settings/whatsapp | /sms | /sip
Sohbet motoru / modelinin yazma ucu ayrı değildir —
PUT /api/settingsgövdesine yazılır;null/boş göndermek seçimi temizler (platform varsayılanına dönülür), katalog dışı değer422.GET /api/settingsyanıtısettings(key-value) +chat_provider_default+chat_provider_platform_default
openai_chat_model_default+openai_chat_model_platform_defaultdöner. Cascade site → hesap → platform: site payload'ındakichat_provider_effective/chat_provider_sourceveopenai_chat_model_effective/openai_chat_model_source(site|account|default) alanlarından hangi katmanın kazandığını okuyabilirsin.
GET /api/me/billing # bayiden gelen borç + taksit + ödeme
GET /api/customer-invoices/{invoice}/data # fatura JSON (PDF client-side üretilir)
role=reseller)GET /api/reseller/summary # KPI kartları
GET /api/reseller/customers
POST /api/reseller/customers # bkz. aşağıdaki gövde — bayi başına 10 müşteri/saat (aşımda 429)
GET /api/reseller/customers/{user}
POST /api/reseller/customers/{user}/send-password-link # müşteriye şifre kurulum bağlantısı
# müşteri başına 5 istek/15 dk + bayi başına 10/saat → aşımda 429
POST /api/reseller/customers/{user}/sites # müşteri adına site + lisans
# Mevcut BAĞIMSIZ bir hesabı müşteriye çevirme (teklif → kabul; POST customers bunu reddeder)
GET /api/reseller/customer-invites # gönderdiğin davetler
POST /api/reseller/customer-invites { "email": "..." }
DELETE /api/reseller/customer-invites/{invite} # daveti iptal et
# Hedef kullanıcı KENDİ hesabından:
GET /api/reseller-invites
POST /api/reseller-invites/{invite}/accept | /reject
GET /api/reseller/invoices # bekleyen/ödenmiş lisans faturaları
POST /api/reseller/customers/{user}/billing/invoices # müşteriye fatura kes (cari hesap)
GET /api/reseller/branding # white-label marka
POST /api/reseller/customers
{
"phone": "+905551112233", // ZORUNLU
"name": "...", "email": "...", // opsiyonel
"allowed_modules": ["whatsapp"], // opsiyonel; null = kısıtsız, [] = hepsi kapalı
"customer_terms_commitment": true // ⚠️ ZORUNLU — kabul edilmezse 422
}
customer_terms_commitment, bayinin "müşterisine platform kullanım şartlarından daha düşük koruma içermeyen şartları kabul ettireceği" beyanıdır (Bayilik Çerçeve Sözleşmesi v2 m. 11.2). Her istekte gönderilir veaudit_logs'a kanıt satırı yazar → arka planda otomatik gönderme, kullanıcıya göster.
Davet akışı (mevcut hesap):
POST /api/reseller/customerssistemde zaten kayıtlı bağımsız bir hesabı doğrudan müşteri yapmaz; rızaya dayalı yolcustomer-invites'tır. Uygunluk kapısı fail-closed ve tek jeneriknot_eligibledöner (hesap enumerasyonu yapılamaz). Davet göndermek de güncel bayilik sözleşme paketini şart koşar (403 requires_reseller_agreement).
⚠️ Müşteri şifresi (2026-06-11):
POST/PUT /reseller/customers*artıkpasswordparametresi kabul etmez (gönderilirse yok sayılır). Müşteri şifresini, kendisine e-posta/SMS ile giden kurulum bağlantısıyla kendisi belirler (send-password-link; yeni müşteri oluşturmada otomatik gönderilir — yanıttapassword_link_sent+password_link_channel). Bayi müşterisinin konuşma/mesaj içeriğine de erişemez (§ 0.2).
requires_reseller_agreement)Bayi yazma uçları (müşteri ekleme, müşteriye site açma/atama/geri alma, kendi sitesini açma, DID yönlendirme yönetimi, bayilik planı satın alma) güncel bayilik sözleşme paketinin (3 belge) onaylı olmasını şart koşar. Okuma uçları sözleşme sormaz — listeler/raporlar kesilmez.
Sözleşme paketi güncellenirse (yeni sürüm yayınlanır ve yürürlük tarihi ileri çekilirse) eski onaylar geçersiz sayılır ve bu yazma uçları — panel de API de — yeniden onaya kadar şu yanıtı döner:
403 { "success": false, "requires_reseller_agreement": true, "message": "..." }
Entegrasyon tarafında yapılması gereken: 403 + requires_reseller_agreement: true yakalandığında
işlemi kuyruklamayı bırakıp bayi kullanıcıyı onaya yönlendirin. İki yol:
https://<panel>/reseller/agreement sayfasında tek tıkla onaylar (önerilen).GET /api/reseller/agreement # 3 belgenin metni + sha256 + accepted / requires_reacceptance
POST /api/reseller/agreement { "accepted": true, "acknowledged_hashes": { "reseller-agreement": "<sha>", "reseller-mesafeli": "<sha>", "reseller-dpa": "<sha>" } }
⚠️ Onay bayinin bilinçli, insan eylemi olmalıdır — belgeleri kullanıcıya göstermeden arka planda otomatik
POSTetmeyin. Her kabulaudit_logs'a sürüm + sha256 ile kanıt satırı yazar; onayın kim tarafından, hangi metne verildiği yasal delil zinciridir. Not: Referans Programı katılımı aynı ön koşulu tek akışta çözer (422 requires_reseller_bundle, aşağıdaki bölüm).
role=reseller)Ne işe yarar: Bayi, dowaba'ya müşteri yönlendirir; müşteri dowaba'ya öder, bayi hakediş alır (brütün %20'si). Toptan/white-label modelin TERSİ para yönü: müşteri dowaba'nın kendi müşterisidir, bayi onun panel/mesaj verisine erişemez.
Ön koşullar (API'de de geçerli): role=reseller + hesap aktif + bayilik sözleşme paketi
(3 belge) güncel sürümle onaylı + programa katılım (opt-in). Sözleşme eksikse opt-in
422 requires_reseller_bundle döner ve belgeler aynı yanıtta gelir — ayrı sayfaya gitmeden,
tek istekte onaylanır.
| Panel sekmesi | Endpoint(ler) |
|---|---|
| (durum/ön koşul) | GET /api/reseller/referral |
| Programa Katıl | POST /api/reseller/referral/opt-in · POST /api/reseller/referral/opt-out |
| Genel Bakış | GET /api/reseller/referral · PATCH /api/reseller/referral/settings |
| Kanal Kodları | POST /api/reseller/referral/codes · PATCH /api/reseller/referral/codes/{id} |
| Referans Paketlerim | GET/POST /api/reseller/referral/packages · PATCH .../packages/{id} · GET .../packages/modules |
| Müşteriler & Hakedişler | GET /api/reseller/referral/customers · GET /api/reseller/referral/commissions |
| Ödemeler | GET/POST /api/reseller/referral/payouts · GET .../payouts/{id}/invoice |
GET /api/reseller/referral # durum + oranlar + sözleşme metinleri + kodlar + sayaçlar
POST /api/reseller/referral/opt-in { "accepted": true, "acknowledged_sha256": "<sha>" }
POST /api/reseller/referral/opt-out # yeni tahakkuk durur; paketler pasifleşir
PATCH /api/reseller/referral/settings { "public_listed": true, "payout_iban": "TR33...", "payout_iban_holder": "..." }
POST /api/reseller/referral/codes { "label": "Instagram", "code": "INSTA2026" } # link: /register?ref=KOD
PATCH /api/reseller/referral/codes/{id} { "is_active": false } # silme YOK, pasifleştirme
GET /api/reseller/referral/packages/modules # allowed_modules için geçerli anahtar seti
GET /api/reseller/referral/packages # paketler + sales sayaçları + limits (taban katsayıları)
POST /api/reseller/referral/packages { "name","message_quota","voice_quota","sites","allowed_modules"[],"prices"{},"page"{} }
PATCH /api/reseller/referral/packages/{id} { "name"?, "is_active"?, "page"? } # ÇEKİRDEK IMMUTABLE
GET /api/reseller/referral/customers # ad + tarih + abonelik durumu (KVKK-minimal, iletişim bilgisi YOK)
GET /api/reseller/referral/commissions # hakediş defteri (accrued/approved/paid/reversed)
GET /api/reseller/referral/payouts # ödeme talepleri/geçmişi
POST /api/reseller/referral/payouts # multipart: invoice=@fatura.pdf VEYA issue_einvoice=true
GET /api/reseller/referral/payouts/{id}/invoice # yüklenen faturayı indir (binary)
Sözleşme eksikse tek adımda katılım:
POST /api/reseller/referral/opt-in
{
"accepted": true,
"acknowledged_sha256": "<data.agreement.sha256>",
"accept_reseller_bundle": true,
"bundle_acknowledged_hashes": {
"reseller-agreement": "<sha>", "reseller-mesafeli": "<sha>", "reseller-dpa": "<sha>"
}
}
Hash'ler
GET /api/reseller/referralyanıtındakidata.agreement.sha256vedata.reseller_bundle[].sha256alanlarından gelir. Metin bu arada güncellenmişse409 stale_documentalırsın → yeniden oku, yeni hash'le gönder.
Gizli satış paketi (partner paketi): Paket hiçbir fiyat listesinde görünmez; satış yalnız
POST yanıtındaki data.sale_url (https://dowaba.com/paket/{uuid}) ile açılır. Müşteri o
sayfadan önce öder, sonra şifresini belirler; dowaba'nın OWNER müşterisi olur (fatura
dowaba'dan). Kota/fiyat/site/modül oluşturulduktan sonra değiştirilemez — değişiklik için yeni
paket açıp eskisini is_active:false yap.
Taban fiyat (422 yemeden önce hesapla):
aylık taban = mesaj × 0,20₺ + çağrı × 0,20₺ + (site − 1) × 150₺, dönem tabanı = aylık taban ×
çarpan (monthly 1 · quarterly 3 · semiannual 5 · yearly 10). Örn. 1.000 mesaj + 100 çağrı +
1 site → aylık 220₺, yıllık 2.200₺. Katsayılar GET .../packages → data.limits.
Ödeme talebi: kısmi tutar YOK — birikmiş tüm hakediş tek talepte toplanır. Ön koşullar: IBAN
kayıtlı + eşik (data.min_payout_try, varsayılan 500₺) aşılmış + açık talep yok + fatura
(dosya veya kendi Nilvera hesabından issue_einvoice=true).
⚠️ Müşteri verisi sınırı:
customersucu yalnız ad + tarih + abonelik durumu döner. E-posta/telefon/site/konuşma bilinçli olarak yoktur ve bunları veren başka bir uç da yok (bayi müşterisi ≠ referans müşterisi). Tam alan listesi: API Dokümantasyonu → "Bayi — Referans Programı".
Onaylı bayi demo modülleri kataloğu (tüm kullanıcılar) + bayi ilan yönetimi (role=reseller
ve aktif ödenmiş abonelik — yazma uçları abonelik pasifken 403 subscription_required):
GET /api/custom-integrations # katalog (approved + published)
GET /api/custom-integrations/{slug} # detay (galeri + demo erişimi)
GET /api/reseller/custom-integrations # kendi ilanların + can_manage + limits
POST /api/reseller/custom-integrations { "title", "short_description", "description", "demo_url", "price"?, "gallery_media_ids"?[] }
PUT /api/reseller/custom-integrations/{id} # onaylı ilanda içerik değişirse yeniden onaya düşer
POST /api/reseller/custom-integrations/{id}/submit # taslağı onaya gönder
PATCH /api/reseller/custom-integrations/{id}/publish { "is_published": true } # yalnız approved
DELETE /api/reseller/custom-integrations/{id} # ilanı tamamen sil (yayından kaldırmak için PATCH .../publish yeterli)
GET/POST /api/reseller/custom-integrations/{id}/faqs # SSS önerileri (onaylanınca dowaba.com SSS'ine eklenir)
DELETE /api/reseller/custom-integration-faqs/{id} # yalnız pending/rejected öneri
Görseller mevcut
POST /api/media/uploadile yüklenir;gallery_media_idsen fazla 20, yalnız kendi medya havuzundan. Yayın kararı superadmin onayına bağlıdır (durumlar:draft → pending → approved/rejected).
Onaylı bayi hizmet profilleri kataloğu + teklif talebi. DoWaba aracıdır, para akışına girmez (v1'de komisyon yok).
GET /api/experts # katalog — ?category=ads|social|content|bot|web|ecommerce &city= &language= &q=
GET /api/experts/{slug} # profil detayı + hizmetler
POST /api/experts/{slug}/inquiries { "consent": true, "contact_name": "...",
"contact_email" | "contact_phone": "...", # en az biri ZORUNLU
"message"?, "budget_note"?, "expert_service_id"? }
consent: truezorunludur (iletişim bilgin iş ortağına aktarılır — rızasız kayıt yazılmaz); ikisi de boşsa422 contact_required. Uç adlı rate limitlidir (5/dk) + aynı profile 24 saat cooldown →429 { "error": "inquiry_cooldown" }. Kısıtlı hesaplar (bayi müşterisi / alt kullanıcı / agent)403 restricted_account; bayiler teklif gönderemez403 expert_inquiry_forbidden; özellik kapatılırsa503 feature_disabled(bayi paneli çalışmaya devam eder).
Bayi tarafı (role=reseller + aktif ödenmiş abonelik + güncel bayilik sözleşmesi; eksikse yazma
uçları 403 { "error": "subscription_required" }):
GET /api/reseller/expert-profile
PUT /api/reseller/expert-profile { "headline", "about", "city"?, "languages"?[], "categories"?[],
"contact_email"?, "contact_phone"?, "website"? }
POST /api/reseller/expert-profile/submit # taslağı onaya gönder (draft|rejected → pending)
POST /api/reseller/expert-profile/toggle-publish { "is_published": true } # yalnız approved
GET/POST /api/reseller/expert-profile/services
PUT /api/reseller/expert-profile/services # toplu kaydet + yeniden sırala (maks 8)
PUT/DELETE /api/reseller/expert-profile/services/{id}
GET /api/reseller/expert-profile/inquiries # ?status=new|read|archived
POST /api/reseller/expert-profile/inquiries/{id}/read | /archive
PATCH /api/reseller/expert-profile/inquiries/{id} { "status": "read" }
Onaylı profilde moderasyona tabi bir alan değişirse profil yeniden onaya düşer (
pending) ve yayından kalkar. Katalogda görünürlük = onaylı ∧ yayında ∧ lisans aktif.Abonelik/sözleşme kapısı yalnız yazma uçlarındadır:
GET expert-profile(yanıttacan_manage,limits.max_services,options,inquiry_counts),GET .../servicesve gelen talep triyajı (inquiries+read/archive/PATCH) kapı sormaz, yalnız sahiplik arar — başka bayinin talebi404(varlık sızdırmaz).submiten az bir hizmet ister (422 services_required).
*/send, mail/reply) idempotent değildir —
aynı isteği iki kez atarsan iki mesaj gider. Kendi tarafında dedup uygula.toggle-bot ile o konuşmada botu kapat.profile_id (hangi bağlı numara/hesap) bekler.
inbox/unified ve {kanal}/conversations yanıtlarında gelir; mesaj/gönder isteğinde geri ver.issues_sanctum_token için aydin@dowaba.com.Sürüm notları artık panelde (dinamik, sürümlü): Geliştirici → Sürüm Notları sekmesi (
/admin/developer). Her deploy otomatik bir taslak üretir (eklenen/çıkan endpoint + git değişiklikleri); ekip bunu geliştirici-dostu dille düzenleyip yayınlar. Geçmiş ve yeni tüm API değişikliklerini oradan takip et — eski statik liste kaldırıldı (her sürümde elle güncellenmiyordu, geride kalıyordu).
Aynı veriyi API'den de okuyabilirsin (değişiklikleri otomatik izlemek için):
GET /api/developer/changelog # yayınlanmış sürüm notları (son 100, meta YOK)
GET /api/developer/changelog?page=1&per_page=10&q=santral # sayfalı + arama (yanıta meta bloğu eklenir)
developermodülü açık olan hesaplar okur; yazma/yayınlama superadmin'dedir. Kayıt alanları:version,summary,entries[] {type, scope, description},released_at.
Bunlara güvenebilirsin — kırıcı değişiklik olursa burada duyurulur:
sanctum_token TTL'siz — yalnız oauth/revoke veya refresh rotation ile silinir (§ 0.2).*/send, mail/reply
ve iki send-template ucu birden — whatsapp/send-template (tek alıcı, wamid döner) ve
contacts/groups/{id}/send-template (grup bazlı, wamid dönmez). Alıcı bazında idempotent tek
yol POST /api/scheduled-jobs'tur (§ 4).{success,data} / kök array / {data,total}) — ilk çağrıda şekli doğrula (§ 0.4).