D

DoWaba — Geliştirici Rehberi

API Dokümantasyonu →

DoWaba — Geliştirici Entegrasyon Rehberi (Menü → API)

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) § 10message.received · message.status · appointment.created · appointment.status_changed · imza · site_id kapsamı · full_payload
API'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

Hangi yol senin için? — PAT mi OAuth mu, site mi alt hesap mı

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 HESAPPOST /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:


0. Nasıl çalışır — Trusted Partner OAuth

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.

0.1 sanctum_token'ı nasıl alıyorsun (özet)

  1. (Tek seferlik) Uygulamanda 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.
  2. Kullanıcı "Login with DoWaba" ile gelir (PKCE + onay ekranı).
  3. Backend'in POST /api/oauth/token çağırır (grant_type=authorization_code).
  4. Yanıtta standart alanların yanında ekstra döner:
{
  "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_token gelmiyorsa dört sebep olabilir:

  1. client trusted partner değil (issues_sanctum_token kapalı);
  2. kullanıcı superadmin (superadmin trusted partner app'lere giremez — onay ekranında bloklanır);
  3. uygulama tenant kapsamlı ve kullanıcı sahibin evreninde değil (onay ekranı out_of_scope_reason: "tenant_scope" döner);
  4. 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.

0.2 Her istekte

curl https://dowaba.com/api/sites \
  -H "Authorization: Bearer 12|abc..." \
  -H "Accept: application/json"

0.3 "Kullanıcı kim?" (kimlik / yetki kontrolü)

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.

0.4 Yanıt zarfı & hatalar

0.4.2 Genel rate limit & kota sorgulama (2026-07-12)

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
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ç developer modülü gerektirir: kısıtlı hesaplarda (bayi müşterisi / alt kullanıcı / agent — allowed_modules içinde developer yoksa) 403 döner. O hesaplarda limiti her yanıttaki X-RateLimit-Limit / X-RateLimit-Remaining header'larından oku. Son iki source değerinde unlimited: true + limit: null gelir — istemci bilinmeyen bir source gördüğünde "limitsiz" varsaymak yerine unlimited alanına bakmalı.

0.4.1 Kullanıcıyı panele taşı — tek-kullanımlık SSO giriş linki (2026-06-17)

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

0.5 ⚠️ Veri kullanım kuralları (Meta uyumu — ZORUNLU)

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

Özet: "İşletme kendi DoWaba verisi için senin aracını kullanıyor" = uygun. "Sen DoWaba verisiyle kendi ürününü besliyorsun" = ihlal.


0.6 ⚠️ Toplu gönderim & pazarlama uyumu (ZORUNLU)

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.

2. İYS onayı — pazarlamada ön onay zorunlu. Ticari/pazarlama içerikli toplu mesaj için alıcının önceden onayı

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=call sesli 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, POST 403 verir (§ 0.2). O hesaplarda red listesini kendi tarafında tutmak senin sorumluluğundadır.


Menü → bölüm haritası

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_account alı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 appointment modü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.


1. Siteler

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.

Listeleme

GET /api/sites
{ "success": true, "sites": [ { "id": 12, "name": "Örnek Mağaza", "is_active": true,
                               "api_key": "...", "is_owner": true, ... } ] }

⚠️ Liste sites anahtarındadır (data değil) — data'ya bakan istemci boş liste görür, JSON hatası bile almaz. ?messaging_scope=1 eklenirse bayi müşterisine ait siteler listeden çıkar.

Tek site

GET /api/sites/{site}

Yeni site açma (hibrit slot / pay-per-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": "..." }

Bayi, müşteri adına site açar: POST /api/reseller/customers/{user}/sites (§12).

Güncelle / Sil / Geri al

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_deadline verir; 30 gün dolduysa restore 404. 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: true gönderirsen siteye özel kanal bağlantıları da KALICI silinir (Evolution instance, WABA aboneliği, webhook'lar) — restore bunları geri getirmez. Varsayılan false.

widget-theme/ai yalnız öneri döner ({theme, summary}); kalıcı kayıt için dönen theme objesini PUT /api/sites/{site} ile settings.widget_theme alanı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.

Site domain'leri (widget origin doğrulama)

GET    /api/sites/{site}/domains
POST   /api/sites/{site}/domains          { "domain": "magaza.com" }
POST   /api/domains/{domain}/verify
DELETE /api/domains/{domain}

2. Mesaj Kutusu (Inbox)

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

Birleşik konuşma listesi ← önerilen giriş noktası

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. counts dü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.

Okundu işaretle (7 DM/yorum kanalı — tek endpoint)

POST /api/inbox/mark-read
{ "channel": "whatsapp", "identifier": "+905551112233", "profile_id": 4 }

Desteklenen channel: whatsapp · instagram · telegram · messenger · tiktok · mail · widget. x ve voice bu uçta YOK (422) — birleşik listede görünseler bile. widget kabul edilir ama okunmamış sayacı olmadığı için çağrı no-op'tur (updated: 0).

Gönderilemeyen cevabı tekrar dene (2026-06-12)

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)

Mesaja ekli medya indir (foto/video/dosya)

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.


3. Kanallar — mesajlaşma

Tüm mesajlaşma kanalları aynı 4 adımlı pattern'i izler:

  1. Konuşmaları listeleGET {kanal}/conversations
  2. Bir konuşmanın mesajlarıGET {kanal}/messages/{identifier}
  3. Yanıt gönderPOST {kanal}/send (Instagram/X/TikTok/Mail'de farklı isim)
  4. Botu aç/kapa (o konuşmada AI cevap versin mi) → POST {kanal}/toggle-bot

Bot mantığı: toggle-bot ile 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-block ile konuşmayı engellersin.

⚠️ Mail istisnası: mail/toggle-bot gövdesi { account_id, site_id? }'dir ve konuşma bazlı değildirsite_id verirsen o site-hesap eşleşmesini, vermezsen hesabın tamamını kapatır. Tek bir yazışmayı susturmak için POST /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_id vermezsen 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.) limit 1–200 clamp.

3.1 WhatsApp — tam örnek

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 olarak message.status webhook'uyla, alternatif olarak GET whatsapp/messages/{phone} yanıtındaki delivery_status alanıyla izlenir — ikisinin kuralları ve polling'in okundu işaretleme yan etkisi § 10 → Outbound webhook'ta. ⚠️ wamid yalnı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-template 60/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önderimi POST /api/scheduled-jobs ile 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_replay alanını içermez ve davranış bugünküyle birebir aynıdır. idempotency_key bir 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)

3.2 Diğer kanallar — aynı pattern

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
WhatsApp 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
Instagram 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
Mail 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}/messages ve .../dm/conversations/{openId}/messages yalnız geri uyumluluk içindir — open_id standart 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ı IGSID recipient_id alanı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'da instagram/profiles diye 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.

Ortak: AI ile mesaj iyileştirme & medya yükleme

# 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ı / agent 403 alır. O hesaplarda kanal-aware ucu kullan.


4. Rehber & Toplu Gönderim

Ne işe yarar: Kişi/grup yönetimi (CRM) + WhatsApp şablonuyla toplu/zamanlı gönderim.

Gruplar & kişiler

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 verilmezse 422 { "contact": null, "error": "Bir identifier gerekli" } döner. channel + identifier sözleşmesi bu uçta DEĞİL, conversation-ai-overrides ucunda geçerlidir.

Dış URL'den kişi çek (CSV/JSON) — import-url

Kendi 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
}

Akıllı segment (yalnız superadmin): POST /api/contacts/segments/{key}/sync. Geçerli key değ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_at dolu/boş). Mail segmenti WhatsApp toplu gönderiminde 400 ile reddedilir — WhatsApp için *_wa anahtarını kullan. Tazeleme: mail kampanyası start'ında otomatik; WhatsApp yollarında otomatik DEĞİL → gönderimden önce sync'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_instructions alanını kabul etmez (gönderirsen 200 döner ama talimat kaydedilmez), yalnız bu uç geçerlidir.

Toplu WhatsApp gönderimi (şablonla)

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_id ve params alanları YOKTUR (gönderilirse sessizce yok sayılır) — site profile_id'den çözülür.

⚠️ Uyum: Bu anlık toplu gönderim de scheduled-jobs gibi opt-out + İYS reddini otomatik filtreler (red'li alıcı atlanır; yanıtta skipped + skipped_samples döner). Yine de pazarlama içerikse alıcıların İYS onayını önceden almış olmalısın (§ 0.6).

Zamanlanmış işler

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_at damgası delil olarak kaydedilir. Arka planda otomatik gönderme — kullanıcıya göster, o onaylasın.

⚠️ scheduled-jobs ile 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.

Alıcıya özel şablon değişkenleri (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" } }

İYS onay yönetimi (toplu gönderimden önce zorunlu)

İYS (İleti Yönetim Sistemi) onaylarını yükle/sorgula. Toplu pazarlama göndermeden önce gerekli — § 0.6.

Bu endpoint'ler iys modü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_date boş / 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 tarih 422 ile 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.


5. Şablonlar & Kampanyalar

WhatsApp şablonları (Meta onaylı)

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

Mail şablonları (AI üretimli + manuel)

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_id ZORUNLU (AI anahtarı site üzerinden çözülür); sitede Gemini anahtarı yoksa 422.

Mail kampanyaları (zamanlanmış toplu gönderim)

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

Çağrı Kampanyası (toplu sesli AI araması — 2026-06-13)

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

5.4. Paylaşım — çok kanallı otomatik yayın kuyruğu (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/publishingOtomatik 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.

Önce uygun kanalları gör

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.

Tek istekte tam bundle oluştur

Ö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
    }
  ]
}

Kuyruğun erimesi ve yeniden başlaması

Yönetim uçları

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:

  1. İlk 500'ü sabit Idempotency-Key ile queue create'te gönder.
  2. Kalanları 500'lük parçalarla /{queue}/items ucuna gönder; her parçaya ayrı ve stabil Idempotency-Key, her öğeye kuyruk içinde benzersiz external_id ver.
  3. Bir chunk'ın sonucu belirsizse aynı anahtar ve aynı gövdeyi güvenle tekrar gönder. Sunucu batch ledger'ından 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 hesabı bağlama ve yayın

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.


5.5. Reklam Yönetimi (Meta Ads) — module:adManagement

Ne 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

Tam alan/payload listesi için Scribe'daki Reklam Yönetimi gruplarını kullan; Meta wire kuralları için meta-ads.md tek otoritedir.


5.6. Sipariş Bildirimleri (e-ticaret → WhatsApp)

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

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. Authorization başlığı YOKTUR — kimlik opak webhookToken + platform doğrulamasıyla kurulur (Shopify X-Shopify-Hmac-SHA256; app secret tanımsızsa fail-closed reddedilir · ikas gövdedeki merchantId ↔ bağlantı eşleşmesi). Rate limit bilinçli olarak yoktur: webhook'a 429 dönmek platformun retry mantığını bozar (ikas 3 başarısız denemede aboneliği tamamen durdurur).


6. Voice / Çağrılar + Outbound

SIP trunk ve çağrı gizlilik politikası

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

PUT sırasında numara başka bir sitede kayıtlıysa 422 döner ve yanıtta bir conflict objesi bulunur ({ phone_number, other_site_id, other_site_name, can_move }). conflict.can_move=true ise (o siteye de erişimin var demektir) numarayı buraya taşımak için move ucunu çağır. Erişimin yoksa site adı/ID'si sızdırılmaz (null) ve can_move=false gelir.

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
}

Gizlilik politikası (trunk gerekmez)

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

Çağrı geçmişi (site bazlı)

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)

Cevapsız çağrı → WhatsApp takibi (kayıt listesi)

"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 } }

Dışarı arama (outbound call)

POST /api/voice/call         # 5/dk
{ "site_id": 12, "to_number": "905551112233", "initial_message": "...", "prompt_override": "..." }

Bildirim aramaları & re-engagement mesajları (onay akışı)

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)

Kapasite Paketleri (2026-06-12)

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)

Santral / Çağrı Aktarımı (2026-06-12)

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}

7. Müşteri Talepleri (Callback)

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_data içinde display-ready [{key, label, value}] olarak da döner; alan konuşmada geçmiyorsa değeri null gelir — model tahmin etmez.

Talep atama (2026-06-11)

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 }

7.5. Potansiyel Müşteriler (Leads / CRM) — module:leads

Ne 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:publishinglead-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}/activities en 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ülen from_label/to_label da 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ç global lead_bulk_assignment feature flag'iyle varsayılan kapalıdır; kapalıyken 503 feature_disabled dö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.


7.6. Randevu — Dış API (server-to-server, 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_ENABLED bayrağı açılana dek 404 döner (rota hiç yokmuş gibi). Kullanmak istiyorsan DoWaba'dan hesabın için açılmasını iste.

Neden PAT yerine bu?

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

Anahtarı al

POST /api/sites/{site}/regenerate-external-key      # Bearer PAT ile, site sahibi
#   → 200 { "success": true, "external_api_key": "dse_...",
#           "custom_integration_affected": false, "message": "..." }

Her istekte

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.

Uçlar

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.

Katalog

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 } ] }

Müsait saatler

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"} ] }

Randevu oluştur

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

Listeleme

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 } }

Durum değiştir

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)

🔐 KVKK / veri koruma

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:

Webhook ile birlikte kullanım

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.


8. Bilgi Tabanı (SSS)

Sitenin AI'ının cevap verirken kullandığı bilgi. Hepsi sites/{site}/... altında.

SSS (FAQ)

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/upload ile yükle, dönen file.id değerlerini sıralı ver — POST/PUT gövdesinde "media_file_ids": [12, 15]. İlk eleman kapak olur (legacy tekil media_file_id hâ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/batch satırlarında da media_file_ids verilebilir; faqs/batch-update görsel ALMAZ (yalnız id/question/answer/category).

Belgeler/doküman yükleme uçları KALDIRILDI (2026-08-03): eski sites/{site}/documents ailesi 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: metnini faqs/extract-from-text'e ver ya da hazır soru-cevapları faqs/batch ile bas.

SSS Stüdyosu (2026-08-06)

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

Akış grafiği (SSS ilişkileri + AI kümeleme)

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

publish TÜM grafı değiştirir (o siteye ait ilişkiler silinir ve gövdedeki listeden yeniden kurulur). Tek bir ilişkiyi eklemek/silmek için relations uçlarını kullan; publish'i "graf tazeleme" sanıp eksik gövdeyle çağırırsan geri kalan ilişkileri kaybedersin.

relations sözleşmesi: aynı (source_faq_id, target_faq_id) çifti varsa güncellenir (created:false). button_label boş/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ğilse 404 faq_not_found (aynı cevap "yok" için de döner — id keşfi yapılamaz). source_faq_id == target_faq_id422 same_faq. Silmede gerçek satır kimliğine ihtiyacın olursa graph çıktısındaki edges[].relation_id alanını kullan (edges[].id sentetiktir, panel içindir). analyze neden 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ışı: analyzestatus:"running"analyze-status'u 3 sn aralıkla yokla → done gelince analysis bloğunu kullan.

⚠️ publish mevcut SSS'lerde YALNIZ button_label + delivery_mode yazar. Soru/cevap/kategori değişikliğini ayrıca PUT /api/faqs/{faq} (veya toplu faqs/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; publish ilişkileri gönderim sırasına göre yazar. Diziyi sıralaman gerçek bir ayardır.

ℹ️ analysis.edges BOŞ 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_updates yalnı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; running yanıtındaki progress ilerlemeyi verir. Bir parti çökerse o ana kadarki sonuç done olarak döner ve analysis.meta.partial = true olur (batches_completed kaç partinin işlendiğini söyler). Yalnız İLK parti çökerse failed gelir. 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-ack idempotent 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.

Oyun Odası (yan etkisiz deneme)

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/personas yanıtındaki quota). Kota bitince 422 quota_exceeded; run-all kı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ı 403 alır — tek istisna salt-okuma faq-universe/graph.

Canlı SSS butonları — hangi şalter neyi açar

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 SenPUT /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_id müşteri iddiasıdır (webhook girdisi); raporlama/gösterim için kullan, yetki kararı için değil.

Fonksiyon Gateway (AI'ın çağırdığı dış fonksiyonlar — ileri seviye)

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

8.5. İşletme Sayfası (İşletme Rehberi)

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)

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.


9. Kullanım, Krediler & Abonelik

Kullanım & kredi özeti (panelin "Kullanım" sayfası)

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

Abonelik

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/plans yanıtındaki sayısal id'dir (slug DEĞİL — "business" gönderirsen 422). period her iki uçta da ZORUNLU: monthly | quarterly | semiannual | yearly (lifetime yeni satışta yok). Gizli partner paketlerinde ayrıca package_uuid zorunludur.

AI Mesaj Paketi (Gemini kredi cüzdanı — anahtarsız kullanıcı)

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. transactions yanıtındaki source alanı kalemi ayırır (ör. voice_conversation, voice_translate). Maliyet gerçek token kullanımından (Gemini usageMetadata) 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ı (catalogcheckout).


10. Geliştirici paneli (self-service)

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 developer modülü açık olmalı (allowed_modules). Ayrıca token/webhook/oauth oluşturmadan önce geliştirici şartları kabulü zorunlu: GET /api/me/developer-termsPOST /api/me/developer-terms/accept.

Kişisel API anahtarları (Sanctum PAT)

GET    /api/me/tokens
POST   /api/me/tokens          { "name": "Entegrasyonum" }     # 10/dk — plain token bir kez döner
DELETE /api/me/tokens/{id}

Outbound webhook (DoWaba → senin sunucun)

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-endpoints yanıtındaki available_events alanıdır — listede olmayan bir olaya abone olmayı denersen 422 alırsın. Kullanıcı başına en fazla 10 endpoint; URL https olmak zorunda; accept_policy: true ve geliştirici şartları kabulü (me/developer-terms) olmadan endpoint yaratılamaz.

⚠️ site_id vermezsen endpoint GENEL olur — TÜM sitelerin oraya akar

site_id opsiyoneldir ve kritiktir:

  • site_id verilmezse 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_id verilirse endpoint yalnız o siteye kilitlenir. Site çözülemeyen olaylar site-kilitli endpoint'e gönderilmez (site sözleşmesi korunur).
  • Panelde site_id alanı YOKTUR (Geliştirici → Webhook'lar formu url + olaylar + full_payload gönderir) → panelden oluşturulan her endpoint GENELDİR. Tek bir siteye kapsamlamak istiyorsan endpoint'i API'den site_id ile oluştur. site_id sonradan PATCH ile değiştirilemez — yanlış kapsamla oluşturduysan sil ve doğru site_id ile yeniden oluştur.
  • Verdiğin site_id senin erişebildiğin bir site değilse 422 dö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_id alanı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ön out, muhatap alanı from değil to, mesaj metni/adı yoktur (text/contact_name alanları bu olayda hiç bulunmaz), has_media yoktur. Maskelemede fark yoktur: to, from ile birebir aynı kuralla maskelenir. Mevcut endpoint'ler yalnız message.received'e abone olduğu için bu olay onlara gitmez — almak istiyorsan endpoint'in events listesine message.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 varsa GET /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/appointments ile mutabakat yap. auto_confirm açık bir kurulumda randevu doğrudan confirmed doğar: bu bir durum değişimi değildir, yalnız appointment.created alırsın (status: "confirmed" ile).

PII: maskeli (varsayılan) endpoint'lerde customer_name, customer_phone, customer_email ve notes anahtarları payload'da hiç bulunmaznull değil, yok. Randevuyu appointment_id (ve gönderdiysen external_ref) ile eşleştir, ayrıntıyı gerektiğinde GET /api/appointments/{id} ile çek. Mesaj olaylarındaki from son-4 maskesinden farklıdır: orada muhatap TEK korelasyon anahtarıdır, burada değildir.

site_id kapsamı: panelden site seçilmeden açılan randevunun site_id'si null'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ındaki X-Dowaba-Delivery kaydını kullan.

Teslim durumunu POLLING ile alma (push'a alternatif) — ⚠️ yan etkili

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

OAuth client (kendi "Login with DoWaba" entegrasyonu)

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}

Güvenilir Ortak (SSO) self-servis — bayiler için

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

  1. Bayi hesabı olmak, 2. aktif ve ödenmiş bayilik aboneliği, 3. güncel bayilik sözleşmelerinin onaylı olması, 4. geliştirici şartlarının kabulü, 5. uygulamanın confidential (sunucu tarafı, secret'lı) ve aktif olması, 6. TP-açık uygulama tavanını (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_clientspartner_sso.active_count; 7. koşul zaten istek gövdesidir. Hata kodları:

Kapsam 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).

Giriş ekranını kendi domain'inde açtırma (white-label)

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ı bir kez onaylar (consent-once)

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.

🤖 Yapay zekaya yazdır: hazır entegrasyon prompt'u

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.

MCP / ChatGPT — konuşarak sistem promptu, WhatsApp üslubu ve SSS hazırlama

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ış:

  1. Kullanıcının bizzat sahibi olduğu düzenlenebilir siteler listelenir.
  2. Mevcut sistem promptu, WhatsApp promptu ve sayfalı yerel SSS okunur.
  3. expected_revision + idempotency_key ile canlı olmayan taslak oluşturulur.
  4. Tam fark kullanıcıya gösterilir; bu preview çağrısı server tarafında damgalanır.
  5. İşletme gerçekleri kullanıcı tarafından doğrulanmışsa ve kullanıcı açıkça “yayınla” derse prompt + en fazla 25 SSS değişikliği atomik uygulanır.
  6. Geri alma istenirse ayrı rollback-preview aracı tam geri alma farkını gösterir.
  7. Sonradan çakışan değişiklik yoksa aynı MCP yayını bu fark için verilen ayrı açık onayla geri alınabilir.

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


11. Profil & Ayarlar

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ğildirPUT /api/settings gövdesine yazılır; null/boş göndermek seçimi temizler (platform varsayılanına dönülür), katalog dışı değer 422. GET /api/settings yanıtı settings (key-value) + chat_provider_default + chat_provider_platform_default

  • openai_chat_model_default + openai_chat_model_platform_default döner. Cascade site → hesap → platform: site payload'ındaki chat_provider_effective / chat_provider_source ve openai_chat_model_effective / openai_chat_model_source (site | account | default) alanlarından hangi katmanın kazandığını okuyabilirsin.

12. Fatura & Bayi

Müşterinin kendi cari hesabı (read-only)

GET /api/me/billing                          # bayiden gelen borç + taksit + ödeme
GET /api/customer-invoices/{invoice}/data    # fatura JSON (PDF client-side üretilir)

Bayi paneli (sadece 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 ve audit_logs'a kanıt satırı yazar → arka planda otomatik gönderme, kullanıcıya göster.

Davet akışı (mevcut hesap): POST /api/reseller/customers sistemde zaten kayıtlı bağımsız bir hesabı doğrudan müşteri yapmaz; rızaya dayalı yol customer-invites'tır. Uygunluk kapısı fail-closed ve tek jenerik not_eligible dö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ık password parametresi 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ıtta password_link_sent + password_link_channel). Bayi müşterisinin konuşma/mesaj içeriğine de erişemez (§ 0.2).

Bayilik sözleşme paketi — API davranışı (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:

  1. Panel: kullanıcı https://<panel>/reseller/agreement sayfasında tek tıkla onaylar (önerilen).
  2. API ile onay akışı (uygulamanız belgeleri kendisi gösterecekse):
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 POST etmeyin. Her kabul audit_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).

Referans Programı — Menü: Bayi > Referans Programı (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/referral yanıtındaki data.agreement.sha256 ve data.reseller_bundle[].sha256 alanlarından gelir. Metin bu arada güncellenmişse 409 stale_document alı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 .../packagesdata.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ı: customers ucu 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ı".

Özel Entegrasyonlar (2026-07-13) — Menü: "Özel Entegrasyonlar" + Bayi > Marka & Tanıtım > "Özel Entegrasyonlarım"

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/upload ile yüklenir; gallery_media_ids en fazla 20, yalnız kendi medya havuzundan. Yayın kararı superadmin onayına bağlıdır (durumlar: draft → pending → approved/rejected).

İş Ortakları — "Uzman Bul" (2026-08-03) — Menü: "İş Ortakları" + Bayi > Marka & Tanıtım

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: true zorunludur (iletişim bilgin iş ortağına aktarılır — rızasız kayıt yazılmaz); ikisi de boşsa 422 contact_required. Uç adlı rate limitlidir (5/dk) + aynı profile 24 saat cooldown429 { "error": "inquiry_cooldown" }. Kısıtlı hesaplar (bayi müşterisi / alt kullanıcı / agent) 403 restricted_account; bayiler teklif gönderemez 403 expert_inquiry_forbidden; özellik kapatılırsa 503 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ıtta can_manage, limits.max_services, options, inquiry_counts), GET .../services ve gelen talep triyajı (inquiries + read/archive/PATCH) kapı sormaz, yalnız sahiplik arar — başka bayinin talebi 404 (varlık sızdırmaz). submit en az bir hizmet ister (422 services_required).


Ek — pratik notlar


13. Sürüm Notları + Kararlı API Sözleşmeleri

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)

developer modülü açık olan hesaplar okur; yazma/yayınlama superadmin'dedir. Kayıt alanları: version, summary, entries[] {type, scope, description}, released_at.

Değişmeyen kararlı sözleşmeler (stable)

Bunlara güvenebilirsin — kırıcı değişiklik olursa burada duyurulur: