PKCryptolisting WebSocket
연동 가이드

단 몇 줄의 코드로 업비트·빗썸 실시간 공지를 수신할 수 있습니다.
텔레그램 봇에서 API 키를 발급받은 후 아래 가이드를 따라 연결하세요.

ENDPOINT
wss://kr.pkcryptolisting.com
PROTOCOL
WebSocket Secure (WSS)
AUTH
API Key (텔레그램 발급)
SERVER LOCATION
AWS Seoul (ap-northeast-2)
1. 연결 및 인증
WebSocket 연결 후 즉시 인증 메시지를 전송해야 합니다. 5초 내 인증하지 않으면 자동 종료됩니다.
특정 거래소 공지만 수신하려면 exchange 필드를 추가하세요.
pip install websockets
npm install ws
go get github.com/gorilla/websocket
[dependencies]
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
futures-util = "0.3"
serde_json = "1"
url = "2"
import asyncio, json, websockets

async def connect():
    async with websockets.connect("wss://kr.pkcryptolisting.com") as ws:

        # send auth message
        await ws.send(json.dumps({
            "type":     "auth",
            "key":      "YOUR_API_KEY",   # issued via Telegram bot
            # "exchange": "Upbit"  # optional: "Upbit" | "Bithumb" (omit for all)
        }))

asyncio.run(connect())
// npm install ws
const WebSocket = require("ws");
const ws = new WebSocket("wss://kr.pkcryptolisting.com");
ws.on("open", () => {
  // send auth message
  ws.send(JSON.stringify({ type: "auth", key: "YOUR_API_KEY",   // issued via Telegram bot
    // exchange: "Upbit"  // optional: "Upbit" | "Bithumb" (omit for all)
  }));
});
// go get github.com/gorilla/websocket
conn, _, _ := websocket.DefaultDialer.Dial("wss://kr.pkcryptolisting.com", nil)
// send auth message
auth, _ := json.Marshal(map[string]string{"type": "auth", "key": "YOUR_API_KEY"})
// "exchange": "Upbit"  // optional: "Upbit" | "Bithumb" (omit for all)
conn.WriteMessage(websocket.TextMessage, auth)
let url = url::Url::parse("wss://kr.pkcryptolisting.com").unwrap();
let (mut ws, _) = connect_async(url).await.unwrap();
// send auth message
let auth = serde_json::json!({"type": "auth", "key": "YOUR_API_KEY"
  // "exchange": "Upbit"  // optional: "Upbit" | "Bithumb" (omit for all)
}).to_string();
ws.send(Message::Text(auth)).await.ok();
2. 인증 응답
인증 성공 시 플랜, 딜레이, 만료일 정보가 반환됩니다.
✓ 성공 (auth_ok)
{
  "type":       "auth_ok",
  "plan":       "premium",
  "delay_ms":   0,
  "expires_at": "2026-06-30",
  "message":   "Connected..."
}
✕ 실패 (error)
{
  "type":    "error",
  "code":    4004,
  "message": "Invalid key"
}
3. 메시지 수신
인증 후 새 공지가 감지되면 자동으로 수신됩니다. async for를 사용하면 연결 종료 시 자동으로 루프가 탈출됩니다.
# message loop — exits cleanly on disconnect
async for raw in ws:
    msg      = json.loads(raw)
    msg_type = msg.get("type")

    if msg_type == "auth_ok":
        print(f"connected  plan={msg['plan']}  delay={msg['delay_ms']}ms")

    elif msg_type == "error":
        print(f"error {msg['code']}: {msg['message']}")
        break

    elif msg_type == "plan_updated":  # plan changed
        print(f"plan updated: {msg['plan']}  delay={msg['delay_ms']}ms")

    else:  # announcement
        print(f"[{msg['exchange']}] {msg['title']}")
        print(f"  symbol={msg['symbol']}  detected={msg['detectedTime']}")
// message loop — exits cleanly on disconnect
ws.on("message", (raw) => {
  const msg = JSON.parse(raw);
  if (msg.type === "auth_ok")
    console.log(`connected  plan=${msg.plan}  delay=${msg.delay_ms}ms`);
  else if (msg.type === "error") { console.log(`error ${msg.code}: ${msg.message}`); ws.close(); }
  else if (msg.type === "plan_updated")  // plan changed
    console.log(`plan updated: ${msg.plan}  delay=${msg.delay_ms}ms`);
  else {  // announcement
    console.log(`[${msg.exchange}] ${msg.title}`);
    console.log(`  symbol=${msg.symbol}  detected=${msg.detectedTime}`);
  }
});
// message loop — exits cleanly on disconnect
for {
    _, raw, err := conn.ReadMessage()
    if err != nil { break }
    var msg map[string]interface{}
    json.Unmarshal(raw, &msg)
    switch msg["type"] {
    case "auth_ok":  fmt.Printf("connected  plan=%v  delay=%vms
", msg["plan"], msg["delay_ms"])
    case "error":    fmt.Printf("error %v: %v
", msg["code"], msg["message"]); break
    case "plan_updated":  // plan changed
        fmt.Printf("plan updated: %v  delay=%vms
", msg["plan"], msg["delay_ms"])
    default:  // announcement
        fmt.Printf("[%v] %v
", msg["exchange"], msg["title"])
        fmt.Printf("  symbol=%v  detected=%v
", msg["symbol"], msg["detectedTime"])
    }
}
// message loop — exits cleanly on disconnect
while let Some(Ok(raw)) = ws.next().await {
    let msg: Value = serde_json::from_str(&raw.to_string()).unwrap_or_default();
    match msg["type"].as_str().unwrap_or("") {
        "auth_ok" => println!("connected  plan={}  delay={}ms", msg["plan"], msg["delay_ms"]),
        "error" => { println!("error {}: {}", msg["code"], msg["message"]); break; }
        "plan_updated" =>  // plan changed
            println!("plan updated: {}  delay={}ms", msg["plan"], msg["delay_ms"]),
        _ => {  // announcement
            println!("[{}] {}", msg["exchange"], msg["title"]);
            println!("  symbol={}  detected={}", msg["symbol"], msg["detectedTime"]);
        }
    }
}
4. 수신 데이터 형식
공지 데이터는 아래 형식으로 전달됩니다. symbol은 단일 문자열, 배열, 또는 빈 문자열일 수 있습니다.
⚠️ 주의사항
title, category 필드는 한국어로 수신됩니다.
detectedTime저희 엔진이 공지를 감지한 시각 (마이크로초 정밀도)
postedTime거래소가 공지를 게시한 시각
{
  "title":        "펏지펭귄(PENGU) 신규 거래지원 안내 (KRW, BTC, USDT 마켓)",
  "category":     "거래",
  "symbol":       "PENGU",       
  "exchange":     "Upbit",      
  "detectedTime": "2025-05-09 15:00:00.333851",
  "postedTime":   "2025-05-09T15:00:00+09:00",
  "url":          "https://www.upbit.com/service_center/notice?id=5104&view=share"
}
필드타입설명
titlestring거래소 공지 원문 제목 (한국어)
categorystring공지 카테고리 (한국어) — 아래 카테고리 목록 참고
symbolstring | array 추출된 코인 심볼. 단일 "BTC", 다중 ["BTC","ETH"], 없으면 "", Free 플랜은 "***"
exchangestring 공지 출처 거래소. "Upbit" 또는 "Bithumb"
detectedTimestring 저희 엔진이 공지를 감지한 시각 (마이크로초 정밀도). Basic 플랜은 +30ms 적용됨
postedTime string 거래소가 공지를 게시한 시각. 거래소 원본 형식 그대로 전달됨
urlstring공지 원문 링크
플랜별 차이
모든 플랜은 동일한 인프라를 사용합니다. 딜레이와 심볼 공개 여부만 다릅니다.
Free
0ms 딜레이
심볼 *** 마스킹
Basic
+30ms 딜레이
심볼 전체 공개
Premium
0ms 딜레이
심볼 전체 공개
⚠️ 주의사항
API Key 하나로 동시에 여러 기기 접속이 불가능합니다. 이미 연결된 상태에서 다른 곳에서 연결을 시도하면 새 연결 시도가 에러 코드 4006으로 차단됩니다.
에러 코드
연결 또는 인증 실패 시 아래 코드가 반환됩니다.
CODE설명처리 방법
4001인증 타임아웃연결 후 5초 내 auth 메시지 전송
4002잘못된 JSONJSON 형식 확인
4003인증 메시지 누락type: "auth" 메시지 전송
4004유효하지 않은 키텔레그램 봇에서 키 확인
4005구독 만료텔레그램 봇에서 플랜 갱신
4006중복 접속기존 연결 종료 후 재접속
4029과도한 요청잠시 후 다시 시도하세요
카테고리 목록
필드에서 수신되는 값 목록입니다. category 필드 값은 한국어로 수신됩니다.
UPBIT 8개 카테고리
안내일반 서비스 안내
거래신규 마켓 추가 및 상장폐지
입출금입출금 중단·재개
점검서비스 점검
디지털 자산에어드랍 및 기타
NFTNFT 관련 공지
서비스+부가 서비스
이벤트이벤트·프로모션
BITHUMB 12개 카테고리
거래유의거래 유의 종목
거래지원종료상장폐지
공시공시 정보
마켓 추가신규 마켓 추가
수수료 이벤트수수료 혜택
신규서비스신규 서비스 출시
안내일반 안내
업데이트앱·서비스 업데이트
이벤트이벤트·프로모션
입출금입출금 중단·재개
점검서비스 점검
후기빗썸기타 리뷰
전체 예제 코드
자동 재연결 및 지수 백오프가 적용된 완성 코드입니다. YOUR_API_KEY 만 교체하고 바로 실행하세요.
import asyncio, json, websockets

KEY         = "YOUR_API_KEY"   # issued via Telegram bot
ENDPOINT    = "wss://kr.pkcryptolisting.com"
RETRIES     = 20

def handle_notice(evt: dict):
    exchange = evt.get("exchange", "")
    title    = evt.get("title", "")
    category = evt.get("category", "")
    symbol   = evt.get("symbol", "")
    detected = evt.get("detectedTime", "")
    posted   = evt.get("postedTime", "")
    url      = evt.get("url", "")
    print(f"[{exchange}] [{category}] {title}")
    print(f"  symbol={symbol}")
    print(f"  detected={detected}  posted={posted}")
    print(f"  {url}")

async def listen():
    for attempt in range(RETRIES):
        try:
            async with websockets.connect(ENDPOINT) as ws:
                await ws.send(json.dumps({"type": "auth", "key": KEY}))

                async for raw in ws:
                    evt      = json.loads(raw)
                    evt_type = evt.get("type")

                    if evt_type == "auth_ok":
                        print(f"connected  plan={evt['plan']}  delay={evt['delay_ms']}ms  expires={evt['expires_at']}")
                        attempt = 0  # reset retry counter on success

                    elif evt_type == "error":
                        print(f"[{evt['code']}] {evt['message']}")
                        if evt["code"] in (4004, 4005, 4006, 4029):
                            return  # do not reconnect
                        break

                    elif evt_type == "plan_updated":
                        print(f"plan changed → {evt['plan']}  delay={evt['delay_ms']}ms  expires={evt['expires_at']}")

                    else:
                        handle_notice(evt)

        except websockets.ConnectionClosed as e:
            print(f"closed: {e}")
        except Exception as e:
            print(f"error: {e}")

        backoff = min(2 ** attempt, 300)
        print(f"retry in {backoff}s...")
        await asyncio.sleep(backoff)

if __name__ == "__main__":
    asyncio.run(listen())
// npm install ws
const WebSocket = require("ws");

const KEY      = "YOUR_API_KEY";   // issued via Telegram bot
const ENDPOINT = "wss://kr.pkcryptolisting.com";
const RETRIES  = 20;

function handleNotice(evt) {
  console.log(`[${evt.exchange}] [${evt.category}] ${evt.title}`);
  console.log(`  symbol=${evt.symbol}`);
  console.log(`  detected=${evt.detectedTime}  posted=${evt.postedTime}`);
  console.log(`  ${evt.url}`);
}

function listen(attempt = 0) {
  if (attempt >= RETRIES) return;
  const ws = new WebSocket(ENDPOINT);
  ws.on("open", () => ws.send(JSON.stringify({ type: "auth", key: KEY })));
  ws.on("message", (raw) => {
    const evt = JSON.parse(raw);
    if (evt.type === "auth_ok") {
      console.log(`connected  plan=${evt.plan}  delay=${evt.delay_ms}ms  expires=${evt.expires_at}`);
      attempt = 0;  // reset retry counter on success
    } else if (evt.type === "error") {
      console.log(`[${evt.code}] ${evt.message}`);
      if ([4004,4005,4006,4029].includes(evt.code)) return;  // do not reconnect
      ws.close();
    } else if (evt.type === "plan_updated")
      console.log(`plan changed → ${evt.plan}  delay=${evt.delay_ms}ms  expires=${evt.expires_at}`);
    else handleNotice(evt);
  });
  ws.on("close", () => {
    const b = Math.min(2 ** attempt, 300);
    console.log(`retry in ${b}s...`);
    setTimeout(() => listen(attempt + 1), b * 1000);
  });
  ws.on("error", (e) => console.log(`error: ${e.message}`));
}

listen();
// go get github.com/gorilla/websocket
package main

import (
    "encoding/json"; "fmt"; "math"; "time"
    "github.com/gorilla/websocket"
)

const (key = "YOUR_API_KEY"; endpoint = "wss://kr.pkcryptolisting.com"; retries = 20)   // issued via Telegram bot

type Evt struct {
    Type,Plan,Message,ExpiresAt,Exchange,Title,Category,DetectedTime,PostedTime,URL string
    Code,DelayMs int; Symbol interface{}
}

func handleNotice(e Evt) {
    fmt.Printf("[%s] [%s] %s
  symbol=%v
  detected=%s  posted=%s
  %s
",
        e.Exchange, e.Category, e.Title, e.Symbol, e.DetectedTime, e.PostedTime, e.URL)
}

func listen() {
    for a := 0; a < retries; a++ {
        c, _, err := websocket.DefaultDialer.Dial(endpoint, nil)
        if err != nil { fmt.Println("error:", err) } else {
            b, _ := json.Marshal(map[string]string{"type":"auth","key":key})
            c.WriteMessage(websocket.TextMessage, b)
            for { _, raw, err := c.ReadMessage(); if err != nil { fmt.Println("closed:", err); break }
                var e Evt; json.Unmarshal(raw, &e)
                switch e.Type {
                case "auth_ok": fmt.Printf("connected  plan=%s  delay=%dms  expires=%s
",e.Plan,e.DelayMs,e.ExpiresAt); a=0  // reset retry counter on success
                case "error": fmt.Printf("[%d] %s
",e.Code,e.Message)
                    if e.Code==4004||e.Code==4005||e.Code==4006||e.Code==4029 { return }  // do not reconnect
                case "plan_updated": fmt.Printf("plan changed → %s  delay=%dms  expires=%s
",e.Plan,e.DelayMs,e.ExpiresAt)
                default: handleNotice(e)
                }
            }; c.Close()
        }
        bf := time.Duration(math.Min(math.Pow(2,float64(a)),300))*time.Second
        fmt.Printf("retry in %vs...
", bf.Seconds()); time.Sleep(bf)
    }
}
func main() { listen() }
use futures_util::{SinkExt, StreamExt};
use tokio_tungstenite::{connect_async, tungstenite::Message};
use serde_json::{json, Value};
use std::time::Duration;

const KEY: &str      = "YOUR_API_KEY";   // issued via Telegram bot
const ENDPOINT: &str = "wss://kr.pkcryptolisting.com";
const RETRIES: u32   = 20;

fn handle_notice(e: &Value) {
    println!("[{}] [{}] {}
  symbol={}
  detected={}  posted={}
  {}",
        e["exchange"],e["category"],e["title"],e["symbol"],e["detectedTime"],e["postedTime"],e["url"]);
}

#[tokio::main]
async fn main() {
    for attempt in 0..RETRIES {
        match connect_async(url::Url::parse(ENDPOINT).unwrap()).await {
            Ok((mut ws, _)) => {
                ws.send(Message::Text(json!({"type":"auth","key":KEY}).to_string())).await.ok();
                while let Some(Ok(raw)) = ws.next().await {
                    if let Ok(e) = serde_json::from_str::<Value>(&raw.to_string()) {
                        match e["type"].as_str().unwrap_or("") {
                            "auth_ok" => println!("connected  plan={}  delay={}ms  expires={}",
                                e["plan"],e["delay_ms"],e["expires_at"]),
                            "error" => {
                                println!("[{}] {}",e["code"],e["message"]);
                                let c=e["code"].as_i64().unwrap_or(0);
                                if [4004,4005,4006,4029].contains(&c) { return; }  // do not reconnect
                                break;
                            }
                            "plan_updated" => println!("plan changed → {}  delay={}ms  expires={}",
                                e["plan"],e["delay_ms"],e["expires_at"]),
                            _ => handle_notice(&e),
                        }
                    }
                }
            }
            Err(e) => println!("error: {}", e),
        }
        let b = (2u64.pow(attempt)).min(300);
        println!("retry in {}s...", b);
        tokio::time::sleep(Duration::from_secs(b)).await;
    }
}

API 키 발급

텔레그램 봇에서 무료로 발급받을 수 있습니다.
유료 플랜 업그레이드도 봇에서 가능합니다.

텔레그램에서 발급 →