Hipcall Geliştirici Bilgi Bankası / API

Transkriptin Stream Edilmesi -New

Transkriptin Stream Edilmesi

Güncellendi October 11, 2026
Oku 21 dk
Bu sayfada

Hipcall, çağrılarınızda konuşulanları çağrı sürerken yazıya döker. Bu rehber, o metni kendi sisteminize canlı olarak nasıl alacağınızı anlatır: bir WebSocket bağlantısı açarsınız, hesabınızdaki çağrıların transkripti konuşuldukça size akar.

Kimler için: Kendi yazılımında çalışan bir geliştirici. Hipcall ekranlarında bir şey yapmak için bu rehbere ihtiyacınız yok; transkript zaten çağrı kaydının yanında görünür. Bu akış, metni kendi sisteminize taşımak için.

Nasıl çalışır

Çağrı bağlandığı anda konuşma tanıma oturumu açılır. Tanınan metin, çağrı hâlâ sürerken parça parça yayınlanır. Siz bu parçaları bir WebSocket üzerinden alır ve kendi tarafınızda birleştirirsiniz.

Çağrı (Hipcall telefon platformu)
└─ konuşma tanıma → transkript olayları
└─ wss://stream.hipcall.com.tr/v1/stream
└─ sizin sunucunuz (WebSocket istemcisi)

Bağlantıyı siz açarsınız; Hipcall sizin sunucunuza bağlanmaz. Webhook'lardan farkı burada: dışarıya açık bir uç nokta yayınlamanız, IP açmanız ya da imza doğrulamanız gerekmez.

Bilmeniz gerekenler bu kadar:

  1. Her olay bir zarf (envelope) içinde gelir: v, type, seq, call_uuid, ts, data.
  2. Metni yeniden kurmanın tek bir kuralı var: segment_id'ye göre üzerine yaz, is_final: true olan bir parça artık donmuştur.
  3. Çağrı bitince gelen transcript.completed olayı nihai metindir; öncesinde gönderilen her şeyin yerine geçer.

Önkoşullar

Gereken Nerede
Hesabınızda transkript özelliğinin açık olması Ayarlar → Yapay zekâ → Transkript
Planınızda API erişimi Planınızda yoksa "Canlı transkript akışı" anahtarı kilitli görünür
Geçerli bir API token Ayarlar → Geliştirici → API Token'ları
Jeton bakiyesi Transkript dakika başına jetonla ücretlendirilir; bakiye bitince yeni metin üretilmez
WebSocket istemcisi çalıştırabilen bir sunucu Tarayıcıdan değil, sunucudan bağlanın (kimlik bilgisi header'da gider)

1. Adım: Transkripti ve canlı akışı açın

https://use.hipcall.com.tr/portal/settings/ai/transcription/

  1. Çağrıların transkriptini çıkar anahtarını açın.
  2. Hangi çağrılar altından en az birini seçin: Gelen çağrılar, Giden çağrılar, Dahili çağrılar. Hiçbiri seçili değilken kayıt yapılamaz.
  3. Metin ne zaman hazır olsun altında Çağrı sürerken seçeneğini işaretleyin. Canlı akış yalnızca bu seçenekle çalışır. Çağrı bittikten sonra seçilirse metin sadece çağrı kaydının yanında oluşur, akıtılacak bir şey olmaz.
  4. Canlı transkript akışı anahtarını açın.
  5. Kaydet'e basın.

[Screenshot: Transkript ayarları sayfası; "Çağrı sürerken" seçili ve "Canlı transkript akışı" açık]

Not: Çağrı bittikten sonra seçeneğine geri dönerseniz canlı akış da otomatik olarak kapanır. Sayfa bunu kaydetmeden önce uyarı olarak gösterir.

Beta: Bu özellik beta aşamasındadır. Sayfada "Bu ayarlar çağrılarınıza henüz uygulanmıyor" uyarısını görüyorsanız, hangi çağrıların yazıya döküldüğü hâlâ hesabınızın mevcut kurulumuna göre belirleniyor demektir; buradaki anahtarlar akışın kendisini açıp kapatmaya devam eder. Uyarı, telefon platformu bu seçimleri okumaya başladığında kaybolur.

2. Adım: API token oluşturun

https://use.hipcall.com.tr/portal/settings/developer/api-tokens/

Yeni API Token → bir ad ve son kullanma tarihi verin (varsayılan 1 yıl, en fazla 3 yıl) → Kaydet. Token yalnızca bir kez gösterilir; bir yere kaydedin.

Bu, REST API'de kullandığınız token'ın aynısıdır. Ayrı bir akış kimliği yoktur: token saklanmış transkriptleri okuma yetkisini zaten veriyor, canlı akış bunu daha erken teslim etmekten ibaret.

Token'ı iptal ederseniz açık bağlantı da kapanır. Sunucu her 5 dakikada bir token'ı ve hesabın yetkisini yeniden kontrol eder; iptal edilmiş ya da süresi dolmuş bir token'la bağlantı 1008 koduyla düşürülür.

3. Adım: Bağlanın

wss://stream.hipcall.com.tr/v1/stream

Kimlik bilgisi header ile gider:

Authorization: Bearer <api_token>

Query string (?token=...) desteklenmez. Bu sunucudan sunucuya bir entegrasyon olduğu için buna gerek yok, ve ?token= uzun ömürlü bir kimlik bilgisini aradaki her erişim kaydına yazardı.

Bağlantı kurulduktan sonra bir abonelik mesajı gönderirsiniz:

→ {"action": "subscribe", "v": 1}
← {"action": "subscribed", "v": 1, "call_uuid": null}

call_uuid verirseniz sadece o çağrıyı, vermezseniz hesaptaki bütün çağrıları dinlersiniz:

→ {"action": "subscribe", "v": 1, "call_uuid": "b6e2-..."}
← {"action": "subscribed", "v": 1, "call_uuid": "b6e2-..."}

Bir bağlantı = bir abonelik. İkinci bir subscribe gönderirseniz already_subscribed hatası alırsınız. İki belirli çağrıyı ayrı ayrı izlemek istiyorsanız iki bağlantı açın ya da hepsine abone olup kendi tarafınızda süzün.

Bağlantıyı canlı tutmak için uygulama seviyesinde ping gönderebilirsiniz:

→ {"action": "ping"}
← {"action": "pong"}

WebSocket protokolünün kendi ping/pong'u da çalışır; istemcinizde 20 saniyelik bir ping aralığı yeterlidir. Bir çağrı dakikalarca tamamen sessiz kalabilir, bu yüzden boşta kalan bağlantıyı kendiniz canlı tutun.

Protokol

İstemciden sunucuya

Mesaj Alanlar
{"action": "subscribe"} v (tam sayı, opsiyonel; verilmezse sunucunun en yeni sürümü), call_uuid (opsiyonel)
{"action": "ping"} —

Sunucudan istemciye

Mesaj Anlamı
{"action": "subscribed", "v": 1, "call_uuid": null} Abonelik kuruldu
{"action": "pong"} Ping yanıtı
{"action": "error", "code": "...", "message": "..."} Hata (aşağıdaki tablo)
zarf Transkript olayı; action yerine type taşır

Gelen mesajı ayırt etmenin yolu: action varsa kontrol mesajıdır, type varsa transkript olayıdır.

Zarf

Her transkript olayı, tipi ne olursa olsun, bu kabukla gelir:

{
"v": 1,
"type": "transcript.segment",
"seq": 42,
"call_uuid": "b6e2-...",
"account_id": 1234,
"ts": "2026-09-19T10:31:04.812Z",
"data": { }
}
Alan Açıklama
v Bu kablo biçiminin sürümü. Bugün 1.
type Olay tipi (aşağıda).
seq Çağrı başına 1'den başlayan sayaç. Gönderilmeyen olayları da sayar; bkz. Kayıp ve tekrar.
call_uuid Çağrının kimliği. Aynı bağlantıda birden çok çağrı akabilir; olayları buna göre ayırın.
account_id Hesap kimliği.
ts Olayın UTC zaman damgası (ISO 8601).
data Tipe özel gövde.

v'yi subscribe sırasında bildirirsiniz. Sunucu daha yeni olayları anladığınız sürüme indirger, indirgeyemiyorsa aboneliği reddeder. Beklemediğiniz bir biçimi sessizce almazsınız.

Olay tipleri

transcript.started

Bir çağrının ilk olayıdır, her zaman. Önce transcript.segment görüyorsanız çağrıya sonradan katıldınız demektir.

{
"language": "tr",
"direction": "inbound",
"from": "+90...",
"to": "+90..."
}

direction: inbound, outbound veya internal.

transcript.segment

Konuşuldukça gelen parça.

{
"segment_id": 7,
"speaker": "1",
"start_ms": 1500,
"end_ms": 3120,
"text": "Cumartesi. 8 Ağustos",
"is_final": false
}
Alan Açıklama
segment_id Çağrı içinde artan parça numarası. Birleştirmede bunu anahtar olarak kullanın.
speaker Sağlayıcı konuşmacı ayrımı yaptıysa "1", "2" gibi bir etiket; yoksa null. Kimlik değildir ve yalnızca o çağrı içinde anlamlıdır, çağrılar arasında taşımayın.
start_ms / end_ms Çağrı başlangıcına göre milisaniye.
text Parçanın o anki metni.
is_final true ise bu parça donmuştur, bir daha değişmez.

transcript.failed

{"reason": "provider_error"}

Sonlandırıcıdır: bu çağrıda artık yeni metin gelmez. Öncesinde gelen her şey geçerlidir ve transcript.completed yine de gönderilir.

reason bizim tanımladığımız kapalı bir kümedir. Konuşma tanıma sağlayıcısının kendi hata kodu geçirilmez. O kod sağlayıcıya özeldir ve anlamı sağlayıcıyla birlikte değişir.

transcript.completed

Çağrı başına bir kez, çağrı kaydı (CDR) yazıldığında gelir. Nihaidir: o çağrı için gönderilmiş tüm transcript.segment olaylarının yerine geçer.

{
"cdr_uuid": "...",
"segments": [
{"segment_id": 1, "speaker": "1", "start_ms": 0, "end_ms": 3120,
"text": "...", "is_final": true}
]
}

cdr_uuid, metni kendi tarafınızdaki çağrı kaydına bağlayan alandır.

Canlı önizlemeye ihtiyacınız yoksa sadece bu olayı dinleyin, diğer her şeyi yok sayın. Kurulabilecek en basit entegrasyon bu.

Transkripti yeniden kurma

Kuralın tamamı şu:

segment_id'ye göre upsert edin. is_final: true donmuş demektir. Metin, segment_id sırasına göre birleştirilir.

segments = {} # segment_id -> segment
def apply(segment):
segments[segment["segment_id"]] = segment
def text():
return "".join(s["text"] for _, s in sorted(segments.items()))

Bu kadar basit olmasının sebebi, konuşma tanıma motorunun ham geçici/kesin metin semantiğinin bizim tarafımızda çözülmüş olması. Ham akışta geçici sonuçlar kararsız kuyruğu baştan yazar, kesin sonuçlar yalnızca yeni donan parçayı taşır ve o parça geçici akıştan kaybolur. Birleştirirseniz "CCumCumarCumartesi." elde edersiniz, üzerine yazarsanız donmuş metni çöpe atarsınız. Bunların hiçbiri size ulaşmıyor.

Hata gibi görünen ama hata olmayan iki davranış var:

  • Bir parçanın metni kısalabilir. Geçici kuyruğun bir sonraki konuşmacıya ait olduğu anlaşılırsa, parça yalnızca kesinleşmiş metniyle donar ve kuyruk bir sonraki segment_id altında yeniden görünür. Upsert bunu kendiliğinden halleder.
  • speaker boş gelebilir. Konuşmacı ayrımı her zaman mümkün olmaz.

Kayıp ve tekrar: seq

seq, çağrının ürettiği her olayı sayar, gönderilmeyenleri de. Bir boşluk gördüğünüzde sunucu yük altında canlı önizlemeyi düşürmüş demektir.

  • Düşen tek şey, canlı (henüz donmamış) parça güncellemeleridir. Geç gelen bir önizleme hiç gelmeyenden kötüdür.
  • transcript.started, donmuş (is_final: true) parçalar, transcript.failed ve transcript.completed asla düşmez. Yük altında akış, harf harf önizleme yerine her konuşma sırasını kapandığında teslim etmeye iner; söylenen hiçbir kelime kaybolmaz.
  • seq geriye giderse aynı olayı ikinci kez almışsınızdır. Taşıma katmanı aynı mesajı yeniden teslim edebilir. Sırası karışmış değil, tekrarlanmış: yok sayın.

Sıralama yalnızca çağrı bazında ve yalnızca seq ile garantilidir. Varış sırasına güvenmeyin.

def check_gap(call_uuid, seq):
previous = last_seq.get(call_uuid)
last_seq[call_uuid] = seq
if previous is None:
return
if seq <= previous:
pass # tekrar teslim — yok say
elif seq > previous + 1:
pass # önizleme düştü — metin yine de gelecek

Bir de şu tuzak var: tekrar teslim edilen bir olay, çağrı bittikten sonra gelebilir. Tamamlanmış çağrıların kimliklerini sınırlı bir kümede tutup geç gelen olayları yok sayın; yoksa kapanmış bir çağrı tek satırlık bir transkriptle "yeniden canlanır".

Örnek kod

Python — en sade hâli

pip install websockets # 10.x ve 14+ ikisi de çalışır
export HIPCALL_TOKEN=<api token>
import asyncio, json, os, ssl, websockets
URL = "wss://stream.hipcall.com.tr/v1/stream"
async def main():
headers = {"Authorization": f"Bearer {os.environ['HIPCALL_TOKEN']}"}
async with websockets.connect(
URL,
additional_headers=headers, # websockets < 14 için: extra_headers
ssl=ssl.create_default_context(),
ping_interval=20,
ping_timeout=20,
) as socket:
await socket.send(json.dumps({"action": "subscribe", "v": 1}))
calls = {} # call_uuid -> {segment_id: segment}
async for frame in socket:
message = json.loads(frame)
if message.get("action"): # kontrol mesajı
print(message)
continue
call_uuid, data = message["call_uuid"], message["data"]
if message["type"] == "transcript.segment":
segments = calls.setdefault(call_uuid, {})
segments[data["segment_id"]] = data
print(call_uuid[:8],
"".join(s["text"] for _, s in sorted(segments.items())))
elif message["type"] == "transcript.completed":
text = "".join(s["text"] for s in data["segments"])
print(f"BİTTİ {call_uuid} (cdr {data['cdr_uuid']}): {text}")
calls.pop(call_uuid, None)
asyncio.run(main())

Bu kadarı akışı görmenizi sağlar ama üretime yetmez: bağlantı koptuğunda geri gelmez, kaçırdığı olayı fark etmez, tekrar teslim edilen olayı iki kez işler. Aşağıdaki tam istemci bu üçünü de kapsıyor.

Python — tam istemci

Olduğu gibi kopyalayıp hipcall_client.py olarak kaydedin ve çalıştırın:

pip install websockets # 10.x ve 14+ ikisi de çalışır
export HIPCALL_TOKEN=<api token>
# hesabın tüm çağrıları
python3 hipcall_client.py wss://stream.hipcall.com.tr/v1/stream
# tek bir çağrı
python3 hipcall_client.py wss://stream.hipcall.com.tr/v1/stream <call_uuid>

Kendi istemcinize taşımaya değer üç parça var: Transcript.apply, seq boşluk kontrolü ve yeniden bağlanma döngüsü. Gerisi ekrana yazdırmak.

#!/usr/bin/env python3
"""
Hipcall canlı transkript akışı için çalışan bir istemci.
Canlı bir hesaba karşı çalıştırın; her çağrının transkriptini konuşuldukça
ekrana yazar.
pip install websockets
export HIPCALL_TOKEN=<api token>
python3 hipcall_client.py wss://stream.hipcall.com.tr/v1/stream [call_uuid]
"""
import asyncio
import json
import os
import ssl
import sys
from collections import OrderedDict
from typing import Optional
import websockets
# websockets 10.x alt modüllerini tembel yükler, bu import gereksiz değil.
import websockets.exceptions
# Bu istemcinin anladığı zarf sürümü. Sunucu daha yeni olayları bu sürüme
# indirger, indiremiyorsa aboneliği reddeder — her iki durumda da beklemediğiniz
# bir biçimi sessizce almazsınız.
VERSION = 1
# websockets 14.0'da header argümanının adı değişti ve istisnalar yeniden
# şekillendi. Dağıtımlar hâlâ 10.x gönderdiği için bu örnek ikisinde de çalışır.
_WS_MAJOR = int(websockets.__version__.split(".")[0])
HEADER_KWARG = "additional_headers" if _WS_MAJOR >= 14 else "extra_headers"
# 14+ InvalidStatus, öncesi InvalidStatusCode fırlatır. İkisi de yeniden
# denemeye değmez: ikisi de yükseltmenin 401 ya da 403 ile reddedildiği
# anlamına gelir. hasattr yerine sürüme bakılıyor, çünkü 14+ eski adı hâlâ
# gösteriyor ve ona dokunmak DeprecationWarning üretiyor.
REFUSED = (
getattr(
websockets.exceptions,
"InvalidStatus" if _WS_MAJOR >= 14 else "InvalidStatusCode",
),
)
def refusal_detail(error) -> str:
"""
Sunucunun 401 ya da 403 ile birlikte gönderdiği gövde.
Sunucu sebebi yazar — invalid_token, revoked, expired, forbidden — ve o
olmadan bir ret sadece "HTTP 401"dir; bu da beş saniyelik bir düzeltmeyle
bir öğleden sonra arasındaki fark. Gövdeyi yalnızca websockets 14+ açar;
eski sürümlerde sebebi görmek için curl kullanın.
"""
response = getattr(error, "response", None)
body = getattr(response, "body", None) if response is not None else None
if body:
return f" - {body.decode('utf-8', 'replace').strip()}"
return " (sebebi görmek için websockets>=14 kullanın ya da curl ile bakın)"
def close_code(error) -> tuple[Optional[int], str]:
"""Sunucunun gönderdiği kapanma çerçevesi, iki istisna biçimi için de."""
received = getattr(error, "rcvd", None)
if received is not None:
return received.code, received.reason
return getattr(error, "code", None), getattr(error, "reason", "")
class Transcript:
"""
Bir çağrının transkripti, olaylardan yeniden kurulmuş hâli.
Kuralın tamamı: **`segment_id`'ye göre upsert, `is_final` donmuş demek.**
Hata gibi görünen ama hata olmayan iki davranış:
* Bir parçanın metni **kısalabilir**. Geçici kuyruğun bir sonraki
konuşmacıya ait olduğu anlaşılırsa, parça yalnızca kesinleşmiş metniyle
donar ve kuyruk bir sonraki `segment_id` altında yeniden görünür. Upsert
bunu siz fark etmeden halleder.
* `speaker`, konuşmacı ayrımı yapılmadıysa `None`'dır; dolu olduğunda da
çağrıya özel bir etikettir ("1", "2"), kimlik değil. Çağrılar arasında
taşımayın.
"""
def __init__(self) -> None:
self.segments: dict[int, dict] = {}
def apply(self, segment: dict) -> None:
self.segments[segment["segment_id"]] = segment
def replace_all(self, segments: list[dict]) -> None:
"""`transcript.completed` nihaidir — her şeyin yerine geçer."""
self.segments = {segment["segment_id"]: segment for segment in segments}
def text(self) -> str:
return "".join(
segment["text"] for _id, segment in sorted(self.segments.items())
)
def settled(self) -> str:
"""Yalnızca donmuş kısım — çağrı sürerken bir modele vereceğiniz metin."""
return "".join(
segment["text"]
for _id, segment in sorted(self.segments.items())
if segment["is_final"]
)
class Session:
"""Bir bağlantıdaki çağrıları ve bir şey kaçırıp kaçırmadığımızı izler."""
# Aynı olay yeniden teslim edilebilir ve bu tekrar, çağrı bittikten *sonra*
# gelebilir. Bu olmadan eski bir parça tamamlanmış bir çağrıyı diriltir ve
# elinizde canlı görünen tek satırlık bir transkript kalır. Uzun çalışan bir
# istemcide sınırsız büyümesin diye sınırlı tutuluyor.
FINISHED_MEMORY = 1_000
def __init__(self) -> None:
self.calls: dict[str, Transcript] = {}
self.last_seq: dict[str, int] = {}
self.finished: OrderedDict[str, None] = OrderedDict()
def check_gap(self, call_uuid: str, seq: int) -> None:
"""
`seq`, çağrının ürettiği her olayı sayar — sunucunun yük altında
düşürdüklerini de. Yani boşluk bir hata değil, sunucunun canlı
önizlemeyi yetişmek için düşürdüğünün göstergesidir. Donmuş parçalar ve
`transcript.completed` asla düşmez, dolayısıyla metin yine gelir;
gelmeyen sadece harf harf önizlemedir.
Tekrar da mümkündür. `seq`'in geriye gitmesi "bunu zaten gördüm"
demektir, "sıra karıştı" değil.
"""
previous = self.last_seq.get(call_uuid)
self.last_seq[call_uuid] = seq
if previous is None:
return
if seq <= previous:
print(f" [tekrar teslim: seq {seq}, öncesi {previous}]")
elif seq > previous + 1:
print(f" [boşluk: seq {seq} öncesinde {seq - previous - 1} olay düştü]")
def handle(self, event: dict) -> None:
kind = event["type"]
call_uuid = event["call_uuid"]
data = event["data"]
if call_uuid in self.finished:
print(f" [{call_uuid[:8]} kapanmış çağrıya geç {kind}, yok sayıldı]")
return
self.check_gap(call_uuid, event["seq"])
if kind == "transcript.started":
self.calls[call_uuid] = Transcript()
print(
f"\n== {call_uuid} başladı "
f"({data['direction']}, {data['language']}, "
f"{data['from']} -> {data['to']})"
)
elif kind == "transcript.segment":
# Çağrının ortasında bağlanan bir istemci `transcript.started`
# görmemiştir, o yüzden çağrının bilindiğini varsaymayın.
transcript = self.calls.setdefault(call_uuid, Transcript())
transcript.apply(data)
marker = "*" if data["is_final"] else " "
print(f"{marker} {call_uuid[:8]} {transcript.text()}")
elif kind == "transcript.failed":
# Sonlandırıcı: bu çağrıda artık metin gelmez. Öncesinde geleni
# atmayın, `transcript.completed` yine de gelecek.
print(f"!! {call_uuid[:8]} transkript başarısız: {data['reason']}")
elif kind == "transcript.completed":
transcript = self.calls.setdefault(call_uuid, Transcript())
transcript.replace_all(data["segments"])
print(f"\n== {call_uuid} tamamlandı (cdr {data['cdr_uuid']})")
print(f" {transcript.text()}\n")
self.calls.pop(call_uuid, None)
self.last_seq.pop(call_uuid, None)
self.finished[call_uuid] = None
while len(self.finished) > self.FINISHED_MEMORY:
self.finished.popitem(last=False)
else:
# Yeni olay tipi eklenmesi kırıcı bir değişiklik değildir.
# Tanımadığınızda hata vermek yerine yok sayın.
print(f"?? {call_uuid[:8]} bilinmeyen olay {kind}")
async def stream(url: str, token: str, call_uuid: Optional[str]) -> None:
session = Session()
# Kimlik bilgisi header'da gider, asla query string'de: bu sunucudan
# sunucuya bir entegrasyon ve `?token=` uzun ömürlü bir kimlik bilgisini
# aradaki her erişim kaydına yazardı.
headers = {"Authorization": f"Bearer {token}"}
options = {
HEADER_KWARG: headers,
# Boşta kalan bağlantıyı canlı tut — bir çağrı dakikalarca tamamen
# sessiz kalabilir.
"ping_interval": 20,
"ping_timeout": 20,
}
if url.startswith("wss://"):
options["ssl"] = ssl.create_default_context()
async with websockets.connect(url, **options) as socket:
subscribe = {"action": "subscribe", "v": VERSION}
if call_uuid:
subscribe["call_uuid"] = call_uuid
await socket.send(json.dumps(subscribe))
async for frame in socket:
message = json.loads(frame)
# Kontrol mesajları "action", transkript olayları "type" taşır.
action = message.get("action")
if action == "subscribed":
scope = message["call_uuid"] or "tüm çağrılar"
print(f"abone olundu: {scope} (v{message['v']})")
elif action == "error":
print(f"hata [{message['code']}]: {message['message']}")
elif action == "pong":
pass
else:
session.handle(message)
async def main() -> None:
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
url = sys.argv[1]
call_uuid = sys.argv[2] if len(sys.argv) > 2 else None
token = os.environ.get("HIPCALL_TOKEN")
if not token:
print("HIPCALL_TOKEN tanımlı değil.")
sys.exit(1)
# Geri çekilerek yeniden bağlan. Sunucu, kimlik bilgisi iptal edildiğinde
# ya da süresi dolduğunda, hesabın yetkisi kalktığında veya bu istemci
# yetişemeyecek kadar geride kaldığında bağlantıyı kapatır — bunlardan
# yalnızca sonuncusu yeniden denemeye değer, o yüzden kapanma koduna
# bakılıyor.
backoff = 1.0
while True:
try:
await stream(url, token, call_uuid)
print("bağlantıyı sunucu kapattı")
except REFUSED as error:
# Yükseltmede 401 ya da 403: yeniden denemek hiçbir şeyi düzeltmez.
print(f"reddedildi: {error}{refusal_detail(error)}")
return
except websockets.exceptions.ConnectionClosed as error:
code, reason = close_code(error)
if code == 1008:
# İptal edilmiş, süresi dolmuş ya da yetkisiz.
print(f"politika gereği kapatıldı: {reason}")
return
print(f"bağlantı koptu: {error}")
except OSError as error:
print(f"bağlanılamadı: {error}")
# Yeniden bağlandığınızda, ayrı kaldığınız sürede söylenenler tekrar
# oynatılmaz. O çağrı için `transcript.completed` hâlâ bekliyorsa tam
# metni orada alırsınız.
print(f"{backoff:.0f}s sonra yeniden bağlanılıyor")
await asyncio.sleep(backoff)
backoff = min(backoff * 2, 30.0)
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
pass

Node.js

import WebSocket from "ws"; // npm install ws
const socket = new WebSocket("wss://stream.hipcall.com.tr/v1/stream", {
headers: { Authorization: `Bearer ${process.env.HIPCALL_TOKEN}` },
});
const calls = new Map(); // call_uuid -> Map(segment_id -> segment)
socket.on("open", () => socket.send(JSON.stringify({ action: "subscribe", v: 1 })));
socket.on("message", (frame) => {
const message = JSON.parse(frame);
if (message.action) { // subscribed / pong / error
console.log(message);
return;
}
const { type, call_uuid, data } = message;
if (type === "transcript.segment") {
if (!calls.has(call_uuid)) calls.set(call_uuid, new Map());
calls.get(call_uuid).set(data.segment_id, data);
const text = [...calls.get(call_uuid).entries()]
.sort(([a], [b]) => a - b)
.map(([, segment]) => segment.text)
.join("");
console.log(call_uuid.slice(0, 8), text);
}
if (type === "transcript.completed") {
console.log("BİTTİ", call_uuid, data.segments.map((s) => s.text).join(""));
calls.delete(call_uuid);
}
});
socket.on("close", (code, reason) => {
console.log("kapandı", code, reason.toString());
// 1008 dışında: geri çekilerek yeniden bağlanın
});

Bağlantı yönetimi

Üretimde tek eksik parça yeniden bağlanma döngüsüdür. Kuralları şöyle:

Durum Ne yapmalı
Yükseltme isteğine 401 Yeniden denemeyin. Token yanlış, silinmiş ya da süresi dolmuş. Sebep yanıt gövdesinde yazar.
Yükseltme isteğine 403 Yeniden denemeyin. Hesap canlı akışa yetkili değil; ayarları 1. Adım'a göre kontrol edin.
Kapanma kodu 1008 Yeniden denemeyin. Token iptal edildi, süresi doldu ya da yetki kalktı.
Kapanma kodu 1011 + too_slow Yeniden bağlanın. Mesajları yeterince hızlı okumuyorsunuz.
Diğer kopmalar (ağ, dağıtım, zaman aşımı) Geri çekilerek (1s → 2s → 4s … en çok 30s) yeniden bağlanın.

Ayrıca:

  • Bağlantıda 12 saat boyunca hiç veri alınmazsa düşürülür. Düzenli ping bunu zaten engeller; yine de uzun süreli istemcilerin yeniden bağlanma döngüsü olmalı.
  • Ayrıldığınız sürede söylenenler tekrar oynatılmaz. Akış bir kuyruk değil, canlı bir yayındır. Yeniden bağlandığınızda o çağrı için transcript.completed hâlâ bekliyorsa tam metni orada alırsınız; almadıysanız transkript Hipcall panelinde çağrı kaydının yanında durmaya devam eder.
  • Mesajları okumazsanız sunucu tamponu büyütmez, bağlantıyı kapatır. Böylece yavaş bir tüketici sessizce bozuk bir transkript biriktirmez.

Hata kodları

{"action": "error", "code": "...", "message": "..."} ile gelir.

code Anlamı
malformed_json Gönderdiğiniz çerçeve geçerli JSON değil.
missing_action Çerçevede action yok.
unknown_action action tanınmadı.
invalid_version v tam sayı olmalı.
unsupported_version İstenen v bu servisin konuştuğu sürümden büyük (ya da 1'den küçük).
already_subscribed Bu bağlantı zaten abone. Bir bağlantı = bir abonelik.
unsupported_frame Metin dışı (binary) çerçeve gönderildi.
feed_unavailable Abonelik şu an kurulamadı; geri çekilerek tekrar deneyin.
too_slow Çok geride kaldınız, bağlantı kapatıldı.
forbidden Hesap canlı akışa yetkili değil.
revoked Kimlik bilgisi artık geçerli değil.
expired Kimlik bilgisinin süresi dolmuş.

HTTP yükseltme adımında reddedilirseniz sebep yanıt gövdesinde düz metin olarak döner. curl ile görebilirsiniz:

curl -i -H "Authorization: Bearer $HIPCALL_TOKEN" \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: $(openssl rand -base64 16)" \
https://stream.hipcall.com.tr/v1/stream

Payload'da bilerek olmayanlar

Yok Neden
Konuşma tanıma sağlayıcısının adı Sağlayıcı değişebilir. Ona göre dallanan bir entegrasyon, sağlayıcı değişimini kırıcı hâle getirirdi.
Ham sağlayıcı yanıtı Mesajın büyük kısmını kaplar ve kimsenin API'si değildir.
confidence (güven skoru) Sağlayıcıya göre kalibre edilir, sağlayıcılar arasında karşılaştırılamaz. Bilgilendirmekten çok yanıltır.
Kanal/leg kimlikleri Bugünkü kurulumda çağrı kimliğiyle çakışması bir tesadüf; ona güvenen entegrasyon sessizce kırılır.

Alan eklemek kırıcı bir değişiklik değildir, çıkarmak kırıcıdır. Bu yüzden tanımadığınız type ve alanları hata vermeden yok sayacak şekilde yazın.

Sorun giderme

Bağlanıyorum ama hiç olay gelmiyor

Sırayla kontrol edin:

  1. subscribed yanıtını aldınız mı? Almadıysanız subscribe mesajınız gitmemiştir.
  2. Ayarlarda Çağrı sürerken seçili mi? Çağrı bittikten sonra seçiliyse akış yoktur.
  3. Hangi çağrılar altında test ettiğiniz yön seçili mi?
  4. Jeton bakiyeniz var mı? Bakiye bittiğinde yeni transkript üretilmez.
  5. Çağrı gerçekten bağlandı mı? Transkript oturumu çağrı karşılandığında değil, taraflar birbirine bağlandığında başlar. Karşılama anonsu, IVR ve kuyrukta beklerken duyulan müzik yazıya dökülmez.

403 alıyorum

Hesap canlı akışa yetkili değil. Canlı transkript akışı anahtarı açık mı, mod Çağrı sürerken mi, transkript ana anahtarı açık mı? Üçü birden gerekir. Anahtar kilit simgesiyle görünüyorsa planınızda API erişimi yok demektir.

401 alıyorum, token'ın doğru olduğundan eminim

Yanıt gövdesindeki sebebe bakın. Token silinmiş, süresi dolmuş ya da başka bir hesaba ait olabilir. Token'ı panelde yeniden oluşturup deneyin.

Metin "CCumCumartesi" gibi birikiyor

Parçaları birbirine ekliyorsunuz. segment_id'ye göre üzerine yazın; bkz. Transkripti yeniden kurma.

Aynı cümleyi iki kez işliyorum

seq'i takip edip geriye giden olayları yok sayın ve tamamlanmış çağrılara ait geç olayları eleyin.

Bir konuşma sırasının metni kısaldı

Beklenen davranış. Kuyruk bir sonraki konuşmacıya aitmiş; bir sonraki segment_id altında gelecek.

Sık sorulan sorular

Akışı açmak Hipcall ekranlarında bir şeyi değiştirir mi?

Hayır. Transkript zaten çağrı kaydının yanında görünür. Bu anahtar yalnızca metni kendi sisteminize taşımak içindir.

Ayrı bir paket almam gerekir mi?

Hayır. Transkript dakika başına jetonla ücretlendirilir. Akışın kendisi için ek bir ücret yoktur.

Tarayıcıdan bağlanabilir miyim?

Hayır. Kimlik bilgisi Authorization header'ı ile gider ve tarayıcı WebSocket API'si özel header göndermeye izin vermez. Ayrıca API token'ını tarayıcıya koymak istemezsiniz. Sunucunuzdan bağlanın, gerekiyorsa kendi istemcilerinize oradan dağıtın.

Aynı anda kaç bağlantı açabilirim?

Aynı hesap için birden çok bağlantı açabilirsiniz; hepsi aynı çağrıları alır. Ama her bağlantı tek bir aboneliğe sahiptir.

Çağrı sırasında metni bir yapay zekâ modeline verebilir miyim?

Evet, ama yalnızca is_final: true olan parçaları kullanın. Donmamış parçalar hâlâ değişebilir.

Geçmiş bir çağrının transkriptini bu akıştan alabilir miyim?

Hayır. Akış yalnızca canlıdır, geçmişi oynatmaz. Geçmiş transkriptler Hipcall panelinde çağrı kaydının yanındadır.

Konuşmacıları ayırt edebilir miyim?

speaker alanı doluysa evet, ama bu bir kimlik değil çağrıya özel bir etikettir. "1" bir çağrıda müşteri, başka bir çağrıda temsilci olabilir.

Bu makale yardımcı oldu mu?

Hâlâ yardıma mı ihtiyacınız var?

Aradığınızı bulamıyor musunuz? Bize ulaşın, yardımcı olalım.