KAP bildirim akışı (WebSocket)

Resmi KAP (Kamuyu Aydınlatma Platformu) bildirimleri, yayımlandıkça tek bir WebSocket bağlantısıyla sisteminize gelir — seçtiğiniz şirketler ve bildirim tiplerine göre süzülür, bağlantı koparsa kaldığı yerden boşluksuz devam eder.

Kaynak

Bildirimler resmi KAP veri yayınından, sunulduğu haliyle gelir — yeniden yazma yok, yapay zeka işlemesi yok. KAP'ın kendisi itme (push) yapmaz: PortGate KAP'ı birkaç saniyede bir yoklar, her yeni bildirimi saklar ve ardından size iter. Her bildirimin KAP tarafından verilen, ardışık ve boşluksuz bir disclosure_index numarası vardır; kaldığı yerden devam etmeyi ve tekilleştirmeyi kesin yapan bu numaradır.

/v1/kap ile aynı değil
/v1/kap REST akışı (news_id, yapay zeka özeti ve duygu analizi) farklı bir üründür. Bu akış ve /v1/kap-feed/disclosures yalnızca resmi bildirimi taşır ve KAP'ın kendi disclosure_index numarasını kullanır.

1 — Bağlanın ve kimlik doğrulayın

Bağlanın, ardından ilk kare olarak 10 saniye içinde auth gönderin. Anahtarın kap kapsamına sahip olması gerekir; yoksa sunucu 4403 ile kapatır.

wss endpoint (scope: kap)
wss://api.theportfoy.com/portgate/v1/kap-feed/stream
Eski adres wss://api.theportfoy.com/portgate/v1/kap/stream aynı protokolle çalışmaya devam eder, ancak /v1/kap-feed/stream lehine kullanımdan kaldırılmaktadır (deprecated). Uygun olduğunuzda geçin; başka hiçbir şey değişmez.
client → server
{"action": "auth", "api_key": "pg_live_…"}
server → client
{"type": "auth_ok", "plan": "tester"}

2 — Konulara abone olun

Enstrüman kimliklerine değil konulara abone olursunuz. Mesaj başına en fazla 100, bağlantı başına en fazla 200 aktif konu. Bilinmeyen konular invalid içinde geri döner; geçerli olanlar yine uygulanır. Birden çok konunuzla eşleşen bir bildirim tek kez gelir.

AlanTürNotlar
alltopicTüm bildirimler.
type:<TYPE>topicTek bir bildirim tipi: FR (finansal rapor), ODA (özel durum açıklaması), DG (diğer), DUY (düzenleyici kurum bildirimi), CA (hak kullanımı / kurumsal işlem), FON (fon bildirimi). ÖDA ve DKB de kabul edilir; ODA ve DUY olarak geri döner.
symbol:<CODE>topicÖneksiz bir KAP kodu: symbol:THYAO ya da symbol:UPG gibi bir fon kodu. Kod, bildirimin symbols listesindeyse eşleşir. Burada piyasa öneki yoktur — symbol:bist:THYAO geçersizdir.
client → server
{"action": "subscribe", "ids": ["symbol:THYAO", "type:FR", "bist:THYAO"]}
{"action": "unsubscribe", "ids": ["type:FR"]}
server → client
{"type": "subscribed", "ids": ["symbol:THYAO", "type:FR"], "invalid": ["bist:THYAO"]}
{"type": "unsubscribed", "ids": ["type:FR"]}

3 — Bildirim karesi

Her bildirim tek bir kare olarak gelir. Canlı karelerde replay false, kaldığı yerden devam ederken gönderilen karelerde true'dur (bölüm 5).

server → client
{"type": "disclosure", "replay": false, "data": {
  "disclosure_index": 1671665,
  "type": "CA", "class": "ODA",
  "symbols": ["PCILT"], "filer_codes": ["PCILT"],
  "filer_title": "PC İLETİŞİM VE MEDYA HİZMETLERİ SANAYİ TİCARET A.Ş.",
  "published_at": "2026-10-03T09:56:03Z",
  "reason": "UPD", "related_index": 1671010,
  "subject": {"tr": "Payların Geri Alınmasına İlişkin Bildirim",
              "en": "Notification Regarding Share Buy-Back"},
  "summary": {"tr": "02.10.2026 Tarihli Pay Geri Alım İşlemleri", "en": null},
  "link": "https://www.kap.org.tr/Bildirim/1671665",
  "attachment_count": 0,
  "year": null, "period": null, "consolidation": null
}}
AlanTürNotlar
disclosure_indexintegerKAP'ın ardışık bildirim numarası. Tekildir; tekilleştirme ve devam etme için kullanın.
typestringFR · ODA · DG · DUY · CA · FON
classstringSunulduğu haliyle KAP bildirim sınıfı.
symbolsstring[]Bildirimin ilgili olduğu tüm kodlar: bildirim sahibinin kodları, fon bildirimlerinde fon kodu, Borsa İstanbul devre kesici bildirimlerinde işlemi durdurulan hisse. Kodu olmayan piyasa geneli bildirimlerde boştur.
filer_codesstring[]Bildirim sahibinin kendi borsa kodları, sunulduğu haliyle.
filer_titlestring | nullBildirim sahibinin unvanı, sunulduğu haliyle.
published_atstring | nullKAP yayım zamanı, ISO-8601 UTC. İstanbul UTC+3'tür — göstermeden önce çevirin.
reasonstring | nullAsıl bildirimde NEW; önceki bir bildirimi güncelliyor, düzeltiyor ya da iptal ediyorsa UPD, CORR veya CANC.
related_indexinteger | nullBu bildirimin güncellediği, düzelttiği ya da iptal ettiği önceki bildirim; NEW'de null.
subject{tr, en} | nullKAP konusu, TR/EN, sunulduğu haliyle.
summary{tr, en} | nullBildirim sahibinin kendi özeti, TR/EN, yazdığı haliyle. Çoğu zaman boştur (null).
linkstring | nullBildirimin kap.org.tr sayfası.
attachment_countintegerEk sayısı (yoksa 0). Dosyaları REST detayından alın.
year · period · consolidationstring | nullYalnızca finansal raporlarda (FR): mali yıl, KAP'ın dönem etiketi (ör. Yıllık, 6 Aylık) ve CS/NC. Diğer türlerde null. FR türünde de KAP göndermediğinde bir değer null olabilir — ör. faaliyet raporlarında ya da sorumluluk beyanlarında consolidation.

Her alan her zaman bulunur; KAP'ın vermediği değer null'dır. Aynı bildirimin canlı ve replay kareleri birebir aynıdır.

Düzeltmeler ve güncellemeler
reason, asıl bildirimde NEW; bildirim daha önceki bir bildirimi güncelliyor, düzeltiyor ya da iptal ediyorsa UPD, CORR veya CANC olur ve related_index o önceki bildirimi gösterir. Yukarıdaki örnek, 1671010'un güncellemesidir.

4 — Finansal kalemler ve ekler

Kare, bildirimin kimliğini ve KAP'ın kendi tanımlayıcı alanlarını taşır. Finansal rapor kalemleri ve ek bağlantıları için kaydı numarasıyla GET /v1/kap-feed/disclosures/{disclosure_index} ucundan (aynı kap kapsamı).

curl (scope: kap)
curl -H "X-API-Key: $PORTGATE_KEY" \
  "https://api.theportfoy.com/portgate/v1/kap-feed/disclosures/1671665"
200 (excerpt)
{
  "disclosure_index": 1671665,
  "type": "CA",
  "reason": "UPD",
  "related_index": 1671010,
  "published_at": "2026-10-03T09:56:03Z",
  "symbols": ["PCILT"],
  "subject": {"tr": "Payların Geri Alınmasına İlişkin Bildirim",
              "en": "Notification Regarding Share Buy-Back"},
  "summary": {"tr": "02.10.2026 Tarihli Pay Geri Alım İşlemleri", "en": null},
  "link": "https://www.kap.org.tr/Bildirim/1671665",
  "attachment_count": 0,
  ...
}

Bildirimin KAP'ta sunulduğu haliyle tam içeriği bir çağrı uzağınızda: GET /v1/kap-feed/disclosures/{index}/body

5 — Boşluksuz devam

İşlediğiniz en yüksek disclosure_index'i saklayın. Her yeniden bağlanmada aboneliğinize last_seen_index olarak ekleyin. Sunucu sırasıyla:

  1. subscribed ile yanıt verir,
  2. last_seen_index'ten sonraki, konularınızla eşleşen tüm kayıtlı bildirimleri eskiden yeniye, replay: true ile gönderir,
  3. replay_done gönderir,
  4. canlı devam eder. Tekrar sırasında gelen canlı bildirimler bekletilir ve replay_done'dan hemen sonra gönderilir — kayıp yok, çift yok.
client → server
{"action": "subscribe", "ids": ["all"], "last_seen_index": 1671664}
server → client
{"type": "subscribed", "ids": ["all"], "invalid": []}
{"type": "disclosure", "replay": true, "data": {"disclosure_index": 1671665, ...}}
{"type": "disclosure", "replay": true, "data": {"disclosure_index": 1671666, ...}}
{"type": "replay_done", "last_index": 1671666, "truncated": false}
{"type": "disclosure", "replay": false, "data": {"disclosure_index": 1671667, ...}}

Bir tekrar en fazla 1000 bildirim kapsar. Daha fazlası varsa truncated true olur: kalanını GET /v1/kap-feed/disclosures ucundan sayfalayarak alın (en yeni önce, imleçle sayfalama) ve daha yeni bir last_seen_index ile yeniden abone olun.

Yeniden bağlanmalar arasında teslimat en az bir kezdir (at-least-once): disclosure_index ile tekilleştirin. last_seen_index göndermezseniz yalnızca abone olduğunuz andan itibaren canlı bildirimleri alırsınız.

6 — Bağlantıyı canlı tutun

{"action": "ping"} mesajını her 30 saniyede bir gönderin. Bildirimler arasında dakikalar (gece ve hafta sonu saatler) olabilir; ping olmadan atıl bağlantı ağ yolu üzerinde kapatılır. Sunucu {"type": "pong"} ile yanıt verir.

Tam örnek (yeniden bağlanma + devam + tekilleştirme)

const URL = "wss://api.theportfoy.com/portgate/v1/kap-feed/stream";
const TOPICS = ["symbol:THYAO", "type:FR"];

let lastSeen = loadLastSeen();   // your durable store; null on first run
const seen = new Set();          // recent indexes, for at-least-once dedupe
let backoff = 1000;

function connect() {
  const ws = new WebSocket(URL);
  let pingTimer;

  ws.onopen = () => ws.send(JSON.stringify({ action: "auth", api_key: KEY }));

  ws.onmessage = (ev) => {
    const msg = JSON.parse(ev.data);
    if (msg.type === "auth_ok") {
      backoff = 1000;
      const sub = { action: "subscribe", ids: TOPICS };
      if (lastSeen !== null) sub.last_seen_index = lastSeen;
      ws.send(JSON.stringify(sub));
      pingTimer = setInterval(
        () => ws.send(JSON.stringify({ action: "ping" })), 30_000);
    } else if (msg.type === "disclosure") {
      const d = msg.data;
      if (seen.has(d.disclosure_index)) return;   // duplicate
      seen.add(d.disclosure_index);
      handle(d);                                  // queue it; keep this fast
      lastSeen = Math.max(lastSeen ?? 0, d.disclosure_index);
      saveLastSeen(lastSeen);
    } else if (msg.type === "replay_done" && msg.truncated) {
      backfillFromRest(lastSeen);                 // GET /v1/kap-feed/disclosures
    } else if (msg.type === "error") {
      console.warn(msg.code, msg.message);
    }
  };

  ws.onclose = (ev) => {
    clearInterval(pingTimer);
    if (ev.code === 4401 || ev.code === 4403) return;   // fix the key
    setTimeout(connect, backoff + Math.random() * 1000);
    backoff = Math.min(backoff * 2, 60_000);
  };
}

connect();

Hatalar ve kapanış kodları

AlanTürNotlar
4401close10 sn içinde auth yok, ilk kare auth değil ya da anahtar geçersiz/iptal — anahtarı düzeltin, döngüde yeniden denemeyin
4403closeAnahtarda kap kapsamı yok — yeniden denemeyin
4400closeÜç hatalı mesaj (bozuk JSON, bilinmeyen action, hatalı alanlar)
4429closeslow_consumer: istemciniz okumayı bıraktı ve 512 karelik tamponu doldu. Bildirimler asla sessizce düşürülmez — last_seen_index ile yeniden bağlanın, tekrar boşluğu doldurur
1013closestream_unavailable: canlı yayın geçici olarak kullanılamıyor — geri çekilmeyle yeniden deneyin
1011closeserver_error: dahili aktarım hatası — geri çekilmeyle yeniden deneyin
1001closeSunucu kapanıyor (dağıtım/yeniden başlatma) — last_seen_index ile yeniden bağlanın
error{code:"subscription_limit"}message200 aktif konu aşılırdı; aboneliğin tamamı reddedilir
error{code:"bad_message"}messageHatalı mesaj; üçüncü hataya kadar bağlantıyı kapatmaz
error{code:"server_error"} + replay_donemessageTekrar başarısız; ardından truncated: true ile replay_done gelir — eksiği GET /v1/kap-feed/disclosures ile tamamlayın

Ölçüm

Kimliği doğrulanmış bağlantı başına bir birim (/v1/kap-feed/stream) artı teslim edilen her bildirim karesi için bir birim, canlı ya da tekrar (/v1/kap-feed/stream:events). İkisi de kullanımınızda diğer REST çağrıları gibi görünür. Eski /v1/kap/stream adresindeki bağlantılar /v1/kap/stream ve /v1/kap/stream:events altında ölçülür.

İyi uygulamalar

  • last_seen_index'i kalıcı bir yerde (veritabanı, dosya) ve yalnızca bildirimi işledikten sonra saklayın — böylece bir çökme ya da dağıtım hiçbir bildirimi kaybettirmez.
  • Üstel geri çekilme ve rastgele gecikmeyle yeniden bağlanın. 4401/4403'te yeniden bağlanmayın: bunlar anahtar düzeltmesi gerektirir.
  • Akışın yalnızca bir kısmına ihtiyacınız varsa dar abone olun (symbol: ya da type:) — daha az kare, daha az birim.
  • Okuma döngüsünü hızlı tutun: yavaş işleri döngü içinde yapmak yerine bildirimleri bir kuyruğa/işçiye verin; aksi hâlde 4429 riski doğar.