DEVELOPER GUIDE

하나의 연결로, 실시간 배당을.

고객사 API 키를 발급한 뒤 WebSocket에 연결하세요.
처음 연결할 때 현재 배당을 받고, 이후 변경된 선택지만 이어받습니다.

01 API 키 준비

관리자가 고객사를 등록하고 API 키를 발급합니다. 키에 지정된 종목, 경기 전·라이브 범위와 최대 동시 연결 수가 실제 구독에 적용됩니다.

키 원문은 발급 직후 한 번만 표시됩니다. 소스 코드, 공개 저장소나 URL 쿼리에 키를 넣지 마세요. 서버 연동에서는 환경변수와 X-API-Key 헤더를 사용하세요.

인증 헤더 설정

헤더 이름은 X-API-Key, 값은 발급받은 bc_live_... 키 원문입니다. 아래의 발급받은_API_키 부분을 실제 키로 바꿔 입력하세요. 서버 WebSocket 클라이언트는 연결을 열 때 이 헤더를 전달합니다.

X-API-Key: 발급받은_API_키

헤더를 직접 입력하는 클라이언트에서도 이름에 X-API-Key, 값에 키 원문을 설정한 뒤 연결하세요. Python·Node.js·PHP의 설정 위치는 아래 언어별 탭에서 확인할 수 있습니다.

서버 실행 전 API_KEY 설정

Python·Node.js·PHP 예제는 현재 프로세스의 API_KEY 환경변수를 읽습니다. 사용하는 터미널에서 아래 명령을 실행한 뒤 같은 터미널에서 예제를 실행하세요.

Windows PowerShell

$env:API_KEY = "발급받은_API_키"

Linux·macOS (bash / zsh)

export API_KEY="발급받은_API_키"

브라우저는 애플리케이션이 전달한 apiKey로 첫 인증 메시지를 보냅니다. 자세한 방법은 브라우저 탭을 확인하세요.

02 WebSocket 구독

다음 주소는 축구 라이브 목록 배당 구독 예시입니다. 운영 환경에서는 배포된 서비스의 wss:// 주소를 사용합니다.

wss://odds-flow.com/ws?snapshot=true&phase=live&sport_id=1

event_id를 생략하면 상시 수집하는 목록용 승패 계열 배당을 받습니다. 원본이 제공하는 연장 포함 유형도 목록에 포함됩니다. 경기의 전체 상세 마켓을 받으려면 event_id에 목록에서 확인한 경기 ID를 지정하세요.

wss://odds-flow.com/ws?snapshot=true&phase=live&sport_id=1&event_id=경기ID

상세 요청 시 해당 경기의 원본 구독을 시작하고 초기 수집을 완료한 뒤 데이터를 보냅니다. 같은 경기를 요청한 고객은 원본 구독 하나를 공유합니다. 첫 요청에는 수집 대기 시간이 있으며, 기본 대기 한도는 45초입니다. 마지막 요청이 끝난 뒤 기본 30초 동안 구독을 유지한 후 해제합니다. 상세 구독 전의 상세배당 변동 이력은 제공되지 않습니다.

파라미터설명
snapshottrue: 초기 목록과 이후 변경분 수신. false 또는 생략: 이후 변경분만 수신. since를 지정하면 초기 목록 없이 해당 커서 이후부터 재개
sport_id구독 종목 ID 문자열. 예: 축구 1, 리그 오브 레전드 77, 도타 2 76. 생략하면 이 키에 허용된 종목 전체
phaselive: 진행 중인 경기, prematch: 경기 전. 생략하면 이 키에 허용된 범위 전체
event_id지정하면 해당 경기의 전체 상세배당 수집을 요청합니다. 생략하면 목록용 대표 배당을 받습니다. 키에 허용된 종목·경기 구분의 경기만 요청할 수 있습니다
since마지막으로 처리한 메시지 id 문자열. 예: 1793145600000-0. 상세 수집이 해제된 기간을 건너뛰는 커서는 reset_required로 초기 목록 재요청이 필요합니다
include_game_statetrue: 배당과 함께 스코어·경기 시간·통계·타임라인 수신. false 또는 생략: 배당만 수신

03 언어별 연동 예제

Python 서버 연동

websockets 라이브러리를 설치하고, API_KEY 환경변수로 발급된 키를 전달합니다.

라이브러리 설치

python -m pip install websockets

Python 헤더 설정

connect()의 additional_headers 인수에 아래 값을 전달합니다. os.environ에서 현재 프로세스의 API_KEY 값을 읽습니다.

additional_headers={"X-API-Key": os.environ["API_KEY"]}

subscribe.py 파일로 저장하세요.

import asyncio, json, os
from websockets.asyncio.client import connect

async def main():
    async with connect(
        "wss://odds-flow.com/ws?snapshot=true&phase=live&sport_id=1",
        additional_headers={"X-API-Key": os.environ["API_KEY"]},
        max_size=4 * 1024 * 1024,
    ) as ws:
        async for message in ws:
            payload = json.loads(message)
            # snapshot_end까지 초기 목록을 모은 뒤 변경분을 반영합니다.
            print(payload)

asyncio.run(main())

API_KEY 환경변수를 설정한 뒤 실행하세요.

python subscribe.py

04 메시지와 연결 복구

snapshot_start

초기 목록 시작. 임시 상태에 데이터를 모으기 시작합니다.

snapshot

items 배열로 배당을 받습니다. 한 메시지에 최대 500개 선택지가 들어갑니다.

snapshot_end

초기 목록 완료. count를 확인하고 임시 목록을 실제 상태에 반영합니다.

odds

변경된 선택지를 data로 받습니다. id는 재연결에 사용하는 커서입니다.

heartbeat

연결과 재개 위치를 유지합니다. 마지막 id를 보관하세요.

reset_required

재생 범위를 벗어난 상태입니다. 미완료 목록을 버리고 since 없이 snapshot=true로 다시 연결합니다.

연결이 끊기면 마지막으로 처리한 odds.id 또는 heartbeat.id를 since에 지정하세요. snapshot_end.resume_after는 초기 목록 완료 후 사용할 수 있습니다. 중복 변경은 (source, phase, event_id, market_id, selection_id)와 change_id로 구분합니다.

wss://odds-flow.com/ws?phase=live&sport_id=1&since=메시지ID
  • 상태가 active인 선택지만 활성 배당으로 사용합니다. suspended는 중지, removed는 삭제 상태입니다.
  • 초기 목록 전송 도중 종료되면 미완료 목록을 적용하지 말고 다시 받아야 합니다.
  • API 키 폐기, 만료 또는 고객사 이용 중지 시 연결이 제한됩니다.
  • 스냅샷과 배당 변경의 중복은 정상이며 change_id가 더 큰 상태를 유지합니다.

05 수신 데이터와 필드 설명

WebSocket으로 받은 문자열을 JSON으로 읽은 뒤 최상위 type으로 메시지를 구분합니다. 아래 JSON과 값은 형식 설명을 위한 가상 예시입니다.

메시지 공통 필드

초기 목록은 items 배열, 실시간 변경은 data 객체로 전달됩니다. data 안의 type은 변경 종류를 나타내며 최상위 type과 구분해서 읽습니다.

WebSocket 메시지 공통 필드
필드자료형의미값 예시
typestring메시지 종류입니다. auth_required, snapshot_start, snapshot, game_state_snapshot, snapshot_end, odds, game_state, heartbeat, reset_required 중 하나입니다.odds
idstringodds·game_state·heartbeat의 재개 커서입니다. 마지막으로 처리한 값을 since에 그대로 넣습니다.1793145601000-0
resume_afterstringsnapshot_start·snapshot_end의 초기 목록 기준 커서입니다. 초기 목록 완료 후 변경을 받기 전 재연결하면 snapshot_end 값을 사용합니다. reset_required에서는 현재 최신 커서이지만 새 초기 목록을 먼저 받아야 합니다.1793145600000-0
itemsarraysnapshot의 배당 배열 또는 game_state_snapshot의 경기 상태 배열입니다. 두 배열 항목은 아래 각 필드 표를 따릅니다.[{...}]
dataobjectodds의 변경된 배당 1개 또는 game_state의 변경된 경기 상태 1개입니다. 필드를 포함한 평면 객체이며 이 안에 quote 객체를 한 번 더 감싸지 않습니다.{"type":"odds.changed","event_id":"example-event-1",...}
countnumber (정수)snapshot_end에 포함되며 전달한 초기 배당 선택지의 총 수입니다. 패킷 수나 경기 수가 아닙니다.1
game_countnumber (정수, 선택)include_game_state=true일 때만 snapshot_end에 포함되는 초기 경기 상태 수입니다.1
timeoutMsnumber (정수)auth_required의 인증 제한 시간(밀리초)입니다. 헤더 인증이 없는 연결에서 안내됩니다.5000
reason"cursor_unavailable"reset_required의 초기 목록 재요청 이유입니다. 요청 커서가 현재 재생 범위에 없음을 나타냅니다.cursor_unavailable
requested_idstringreset_required에서 더 이상 재생할 수 없는 커서입니다. 초기 목록 전송 중 기준 커서가 범위를 벗어난 경우도 포함합니다.1793145500000-0
snapshot_urlstringreset_required의 고객용 배당 REST 조회 경로입니다. 다시 조회할 때 기존 필터와 API 키를 적용합니다. 경기 상태의 초기 목록은 WebSocket으로 받습니다./api/v1/odds
websocket_snapshotstringreset_required의 WebSocket 초기 목록 경로입니다. since를 제거하고 종목·경기·phase 등 기존 필터를 다시 적용합니다. 경기 상태 모드는 include_game_state=true가 포함됩니다./ws?snapshot=true&include_game_state=true

배당 데이터: odds.data / snapshot.items[]

배당 한 건은 한 경기의 마켓에 속한 선택지 하나입니다. 실시간 odds.data는 그 선택지의 현재 전체 값입니다. 초기 snapshot.items의 각 항목도 같은 기본 필드를 사용하며, 변경 메시지 전용 type과 outbox_id는 포함하지 않습니다.

{
  "type": "odds",
  "id": "1793145601000-0",
  "data": {
    "source": "x10x10s",
    "phase": "live",
    "event_id": "example-event-1",
    "sport_id": "1",
    "sport_name": "Football",
    "region_name": "예시 지역",
    "league_id": "example-league-1",
    "league_name": "예시 리그",
    "home_team": "예시 홈팀",
    "away_team": "예시 원정팀",
    "start_time": "2026-10-27T23:00:00Z",
    "market_id": "example-market-1",
    "market_name": "Match Result",
    "market_type": "P1XP2",
    "line": null,
    "selection_id": "example-selection-home",
    "selection_name": "W1",
    "odds": "2.20",
    "status": "active",
    "received_at": "2026-10-28T00:00:01Z",
    "source_updated_at": null,
    "change_id": 102,
    "changed_at": "2026-10-28T00:00:01Z",
    "type": "odds.changed",
    "outbox_id": 501
  }
}
배당 선택지 필드와 값 의미
필드자료형의미값 예시
sourcestring원본 데이터 제공처 식별자입니다.x10x10s
phase"prematch" | "live"prematch는 경기 전, live는 라이브 수집 구분입니다.live
event_idstring원본 경기 ID입니다. source·phase와 함께 경기를 구분합니다.example-event-1
sport_idstring | null종목 ID입니다. 아래 종목 ID 표와 대응하며 JSON에서는 문자열로 전달합니다."1"
sport_namestring | null원본 종목 이름입니다.Football
region_namestring | null원본 국가·지역 이름입니다.예시 지역
league_idstring | null원본 리그·대회 ID입니다.example-league-1
league_namestring | null원본 리그·대회 이름입니다.예시 리그
home_teamstring | null홈팀 또는 첫 번째 선수 이름입니다.예시 홈팀
away_teamstring | null원정팀 또는 두 번째 선수 이름입니다.예시 원정팀
start_timeISO 8601 string | null예정된 경기 시작 시각입니다. 시간대가 포함됩니다.2026-10-27T23:00:00Z
market_idstring경기 안의 배당 마켓 ID입니다. 승무패·핸디캡·언더오버 등 개별 마켓을 구분합니다.example-market-1
market_namestring | null원본 마켓 이름입니다.Match Result
market_typestring | null원본 마켓 유형 코드입니다. 값은 원본에 따라 달라집니다.P1XP2
linestring | null핸디캡·언더오버 등의 기준점입니다. 선택지 기준점이 있으면 이를 사용하고, 없으면 마켓 기준점을 사용합니다. 기준점이 없는 마켓은 null입니다."2.5" 또는 null
selection_idstring마켓 안의 선택지 ID입니다.example-selection-home
selection_namestring | null홈 승·무승부·오버 등 원본 선택지 이름입니다. 이름이 없으면 원본 선택지 유형을 사용합니다.W1
oddsstring | null십진 배당 계수입니다. 정밀도 보존을 위해 JSON 문자열로 전달하며, 값이 없거나 제거된 배당은 null입니다."2.15"
status"active" | "suspended" | "removed"active는 활성, suspended는 원본 중지·유효 배당 없음·수집 단절 등으로 중지, removed는 수집 목록에서 제거된 상태입니다. 배당 사용 전 상태를 확인합니다.active
received_atISO 8601 string수집 서비스가 해당 데이터를 관측한 UTC 시각입니다.2026-10-28T00:00:00Z
source_updated_atISO 8601 string | null원본 변경 시각을 담는 필드입니다. 현재 수집 데이터에서는 null입니다.null
change_idnumber (정수)배당 상태 변경 이력의 버전입니다. 같은 배당 키에서 더 큰 값을 최신으로 적용합니다. 현재 JSON 정수이므로 JavaScript 안전 정수 범위를 넘는 값은 일반 JSON.parse에서 정밀도가 보장되지 않습니다.102
changed_atISO 8601 string서비스가 해당 상태 변경을 기록한 시각입니다. received_at과 구분합니다.2026-10-28T00:00:01Z
type"odds.changed" (변경분에만)실시간 odds 메시지의 data에 포함되는 내부 변경 종류입니다. 초기 snapshot.items의 각 항목에는 없습니다.odds.changed
outbox_idnumber (정수, 변경분에만)실시간 변경 발행 기록 ID입니다. 초기 snapshot.items에는 없으며, 재개에는 바깥쪽 id 커서를 사용합니다. 큰 정수의 정밀도 주의는 change_id와 같습니다.501

선택지는 (source, phase, event_id, market_id, selection_id)로 식별합니다. 같은 키가 다시 오면 해당 선택지를 갱신하고 status가 active인 선택지만 활성 배당으로 사용하세요. odds는 소수 정밀도를 보존하는 문자열이며 값이 없을 때는 null입니다.

경기 상태: 스코어·시간·통계·타임라인

include_game_state=true를 지정하면 game_state_snapshot과 game_state도 같은 WebSocket으로 받습니다. 기본값은 false입니다.

wss://odds-flow.com/ws?snapshot=true&phase=live&sport_id=1&include_game_state=true
{
  "type": "game_state",
  "id": "1793145602000-0",
  "data": {
    "source": "x10x10s",
    "phase": "live",
    "event_id": "example-event-1",
    "sport_id": "1",
    "sport_name": "Football",
    "region_name": "예시 지역",
    "league_id": "example-league-1",
    "league_name": "예시 리그",
    "home_team": "예시 홈팀",
    "away_team": "예시 원정팀",
    "start_time": "2026-10-27T23:00:00Z",
    "score": {
      "home": "1",
      "away": "0"
    },
    "clock": {
      "time": "54",
      "state": "set2"
    },
    "info": {
      "score1": "1",
      "score2": "0",
      "current_game_time": "54",
      "current_game_state": "set2"
    },
    "stats": {},
    "live_events": [],
    "status": "active",
    "received_at": "2026-10-28T00:00:02Z",
    "change_id": 202,
    "changed_at": "2026-10-28T00:00:02Z",
    "type": "game.state_changed",
    "outbox_id": 502
  }
}
경기 상태 필드와 값 의미
필드자료형의미값 예시
sourcestring원본 데이터 제공처 식별자입니다.x10x10s
phase"prematch" | "live"경기 전·라이브 수집 구분입니다.live
event_idstring원본 경기 ID입니다. 경기 상태의 고유 키는 source·phase·event_id입니다.example-event-1
sport_idstring종목 ID입니다. 배당 모델과 달리 경기 상태에서는 필수 문자열입니다."1"
sport_namestring | null원본 종목 이름입니다.Football
region_namestring | null원본 국가·지역 이름입니다.예시 지역
league_idstring | null원본 리그·대회 ID입니다.example-league-1
league_namestring | null원본 리그·대회 이름입니다.예시 리그
home_teamstring | null홈팀 또는 첫 번째 선수 이름입니다.예시 홈팀
away_teamstring | null원정팀 또는 두 번째 선수 이름입니다.예시 원정팀
start_timeISO 8601 string | null예정된 경기 시작 시각입니다.2026-10-27T23:00:00Z
score.home / score.awaystring | null원본 info.score1·info.score2를 문자열로 정규화한 점수입니다. 실제 0은 문자열 0이며, 원본에 값이 없으면 null입니다.{"home":"1","away":"0"}
clock.timestring | null원본 current_game_time입니다. 종목에 따라 분·시계 문자열 등 형식이 다르며 초 단위로 강제 변환하지 않습니다."53" 또는 "05:59"
clock.statestring | null원본 current_game_state입니다. 종목별 원본 상태 값을 그대로 전달합니다."set2"
infoobject점수·경기 진행 정보의 원본 객체입니다. 필드와 값 형식은 종목·공급 원본에 따라 달라지며, 정보가 없으면 빈 객체입니다.{"score1":"1","score2":"0"}
statsobject원본 경기 통계 객체입니다. 세부 항목·중첩 구조는 종목과 원본에 따라 달라지며 정보가 없으면 빈 객체입니다.{}
live_eventsobject | array원본 타임라인 이벤트입니다. type_id·side·time 등 세부 값은 원문을 보존하며 임의로 골·카드 이름이나 시간 단위로 해석하지 않습니다. 정보가 없으면 빈 객체이며 원본 배열은 배열로 유지합니다.[] 또는 {}
status"active" | "suspended" | "removed"경기 상태의 활성·중지·수집 목록 제거 여부입니다. 중지·제거 시 마지막으로 관측한 점수와 통계가 남을 수 있습니다.active
received_atISO 8601 string수집 서비스가 경기 상태를 관측한 UTC 시각입니다.2026-10-28T00:00:00Z
change_idnumber (정수)경기 상태 변경 이력의 버전입니다. 배당 change_id와 별도 체계이므로 같은 종류·경기 키 안에서 비교합니다. JavaScript 안전 정수 범위를 넘는 JSON 정수는 일반 JSON.parse에서 정밀도가 보장되지 않습니다.202
changed_atISO 8601 string서비스가 경기 상태 변경을 기록한 시각입니다.2026-10-28T00:00:02Z
type"game.state_changed" (변경분에만)실시간 game_state.data의 내부 변경 종류입니다. 초기 game_state_snapshot.items에는 없습니다.game.state_changed
outbox_idnumber (정수, 변경분에만)실시간 변경 발행 기록 ID입니다. 초기 경기 상태 항목에는 없으며 재개에는 바깥쪽 id를 사용합니다. 큰 정수 정밀도 주의는 change_id와 같습니다.502

경기 상태는 (source, phase, event_id)로 식별합니다. game_state.data 전체를 교체하여 빈 live_events나 null 점수도 반영하세요. clock.time의 단위와 clock.state의 코드는 종목과 원본에 따라 달라지므로 원문 그대로 해석합니다. info·stats·live_events의 내부 키와 값도 종목마다 다르며, 타임라인의 type_id를 임의의 골·카드 이름으로 변환하지 않습니다.

경기 상태를 함께 받는 경우 Python의 max_size와 Node.js의 maxPayload를 8 * 1024 * 1024로 설정하세요. 배당 전용 구독에서 경기 상태 포함 구독으로 바꿀 때는 since를 생략하고 snapshot=true로 초기 목록을 다시 받습니다.

초기 목록·인증·연결 복구 JSON

snapshot=true로 처음 연결하면 snapshot_start → game_state_snapshot(선택) → snapshot → snapshot_end 순서로 받습니다. 중간 목록 메시지는 여러 번 오거나 데이터가 없으면 생략될 수 있습니다. snapshot_end를 받은 뒤 모아 둔 초기 목록을 적용하세요.

auth_required
{
  "type": "auth_required",
  "timeoutMs": 5000
}
snapshot_start
{
  "type": "snapshot_start",
  "resume_after": "1793145600000-0"
}
game_state_snapshot
{
  "type": "game_state_snapshot",
  "items": [
    {
      "source": "x10x10s",
      "phase": "live",
      "event_id": "example-event-1",
      "sport_id": "1",
      "sport_name": "Football",
      "region_name": "예시 지역",
      "league_id": "example-league-1",
      "league_name": "예시 리그",
      "home_team": "예시 홈팀",
      "away_team": "예시 원정팀",
      "start_time": "2026-10-27T23:00:00Z",
      "score": {
        "home": "1",
        "away": "0"
      },
      "clock": {
        "time": "53",
        "state": "set2"
      },
      "info": {
        "score1": "1",
        "score2": "0",
        "current_game_time": "53",
        "current_game_state": "set2"
      },
      "stats": {},
      "live_events": [],
      "status": "active",
      "received_at": "2026-10-28T00:00:00Z",
      "change_id": 201,
      "changed_at": "2026-10-28T00:00:00Z"
    }
  ]
}
snapshot
{
  "type": "snapshot",
  "items": [
    {
      "source": "x10x10s",
      "phase": "live",
      "event_id": "example-event-1",
      "sport_id": "1",
      "sport_name": "Football",
      "region_name": "예시 지역",
      "league_id": "example-league-1",
      "league_name": "예시 리그",
      "home_team": "예시 홈팀",
      "away_team": "예시 원정팀",
      "start_time": "2026-10-27T23:00:00Z",
      "market_id": "example-market-1",
      "market_name": "Match Result",
      "market_type": "P1XP2",
      "line": null,
      "selection_id": "example-selection-home",
      "selection_name": "W1",
      "odds": "2.15",
      "status": "active",
      "received_at": "2026-10-28T00:00:00Z",
      "source_updated_at": null,
      "change_id": 101,
      "changed_at": "2026-10-28T00:00:00Z"
    }
  ]
}
snapshot_end
{
  "type": "snapshot_end",
  "resume_after": "1793145600000-0",
  "count": 1,
  "game_count": 1
}
heartbeat
{
  "type": "heartbeat",
  "id": "1793145602000-0"
}
reset_required
{
  "type": "reset_required",
  "reason": "cursor_unavailable",
  "requested_id": "1793145500000-0",
  "resume_after": "1793145602000-0",
  "snapshot_url": "/api/v1/odds",
  "websocket_snapshot": "/ws?snapshot=true&include_game_state=true"
}

auth_required는 헤더 인증이 없는 연결에서만 받습니다. 인증 후에는 별도의 성공 메시지 없이 초기 목록 또는 변경분이 시작됩니다. heartbeat는 현재 커서를 알려 주는 서버 메시지이며 응답 JSON을 보내지 않습니다. 구독 조건을 바꾸려면 연결 주소의 파라미터를 바꿔 다시 연결하세요.

재접속에는 마지막으로 처리한 odds.id, game_state.id 또는 heartbeat.id를 since로 전달합니다. snapshot_end.resume_after도 초기 목록 완료 후 사용할 수 있습니다. id는 재개 커서이고 change_id는 레코드의 변경 버전입니다. 목록 배당·상세배당·경기 상태의 change_id는 각각의 구독 범위와 종류 안에서 비교합니다. 상세 수집이 해제된 뒤 재시작하면 이전 커서로 이어받는 대신 reset_required에 따라 새 초기 목록을 받아야 할 수 있습니다.

reset_required를 받으면 미완료 초기 목록을 버리고 기존 종목·경기·phase·include_game_state 조건을 유지한 채 since 없이 snapshot=true로 다시 연결하세요. snapshot_url은 배당 REST 조회 경로이고, 경기 상태까지 다시 받으려면 websocket_snapshot 경로로 연결합니다. 필요한 구독 조건은 다시 붙여야 합니다.

연결 종료 코드

인증 실패나 서비스 오류는 별도의 error JSON 대신 WebSocket 종료 이벤트의 code와 reason으로 전달됩니다. 지원하지 않는 구독 파라미터나 잘못된 값은 연결 성립 전에 HTTP 400으로 거절될 수 있습니다.

WebSocket 연결 종료 코드
필드자료형의미값 예시
1000정상 종료정상 종료입니다. reset_required 안내 뒤에는 미완료 초기 목록을 버리고 새 초기 목록으로 연결합니다.reset_required 수신 후 종료
1008인증·정책 제한잘못된 인증 메시지, 인증 제한 시간 초과, 키 만료·폐기, 접근 범위·동시 연결 수 제한 등에 해당합니다. 안내된 종료 사유를 확인합니다.API 키 인증 시간이 초과되었습니다.
1009메시지 크기 초과단일 경기 상태가 전송 크기 한도를 넘은 경우입니다. 일부 내용으로 잘라서 전송하지 않습니다.구독 메시지 크기가 전송 한도를 초과했습니다.
1011서비스 처리 오류인증 저장소·배당 원본 연결·메시지 처리 또는 전송 오류 등으로 연결을 종료합니다.배당 수집기에 연결할 수 없습니다.
1013수집 용량·수신 지연 제한동시 상세 수집 경기 수 상한에 도달하거나 클라이언트 전송 대기가 커졌을 때 종료합니다. 종료 사유를 확인하고 대기 후 다시 요청합니다.동시 상세 수집 경기 수 한도에 도달했습니다.
1006클라이언트가 관측하는 비정상 종료네트워크 단절·Ping/Pong 응답 단절 등으로 종료 프레임 없이 연결이 끊기면 클라이언트에서 관측할 수 있습니다. 서버가 1006 종료 프레임을 보내는 것은 아닙니다.네트워크 연결 끊김

06 종목 ID

⚽축구1
🏀농구3
🏐배구5
🏒아이스하키2
⚾야구11
🎾테니스4
리그 오브 레전드77
도타 276

07 애니메이션 연동

기본 경기장 화면을 제공하는 iframe 위젯과 직접 데이터를 받는 애니메이션 WebSocket을 지원합니다. 배당·스코어·타임라인을 받는 /ws?include_game_state=true와 구분되는 별도 피드입니다.

목록에서 확인한 실제 경기 ID와 종목 ID를 사용하고 키에 live·해당 종목 권한을 허용하세요. 모든 경기에 애니메이션이 제공되는 것은 아니며 원본에 없는 좌표·점수는 생성하지 않습니다.

고객사 서버에서 위젯 발급

고객사 서버가 X-API-Key와 Content-Type: application/json으로 아래 API를 호출합니다. eventId는 경기 ID 문자열, sportId는 종목 ID 숫자, parentOrigin은 위젯이 들어갈 고객 페이지의 정확한 Origin입니다. parentOrigin은 경로 없는 HTTPS 주소이며 로컬 개발 주소만 HTTP를 허용합니다.

POST https://odds-flow.com/api/v1/animation/widget-sessions
// 고객사의 인증을 통과한 서버 요청에서 호출하세요.
// API_KEY는 고객사 서버의 환경변수이며 브라우저로 반환하지 않습니다.
export async function createAnimationWidget(eventId, sportId, parentOrigin) {
  const apiKey = process.env.API_KEY;
  if (!apiKey) throw new Error("API_KEY 환경변수를 설정하세요.");

  const response = await fetch("https://odds-flow.com/api/v1/animation/widget-sessions", {
    method: "POST",
    headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
    body: JSON.stringify({
      eventId, sportId, parentOrigin, ttlSeconds: 900,
    }),
    cache: "no-store",
    signal: AbortSignal.timeout(10_000),
  });
  if (!response.ok) throw new Error("위젯 발급 실패: HTTP " + response.status);
  const widget = await response.json();
  // 고객사 서버의 응답도 Cache-Control: no-store로 전달하세요.
  return { iframeUrl: widget.iframeUrl, expiresAt: widget.expiresAt };
}

// eventId: 목록에서 확인한 실제 경기 ID 문자열
// sportId: 해당 경기의 종목 ID 숫자
// parentOrigin: 위젯을 삽입할 고객 페이지의 정확한 Origin
// 예: https://customer.example (경로·쿼리 제외)

고객 페이지에 iframe 삽입

서버 응답의 iframeUrl을 그대로 src에 지정하세요. 이 주소의 #token=...은 단기 위젯 토큰이며 고객사 API 키가 아닙니다. API 키를 iframe 주소나 공개 HTML에 넣지 않습니다.

<iframe
  src="서버 응답의 iframeUrl을 그대로 입력"
  title="실시간 경기 애니메이션"
  width="100%"
  height="520"
  loading="lazy"
  referrerpolicy="no-referrer"
  style="border:0"
></iframe>

기본 유효 시간은 900초이며 ttlSeconds는 60~3600초로 지정합니다. API 키 만료가 빠르면 그 시각에 맞춥니다. 응답의 expiresAt 이후에는 고객사 서버에서 새 URL을 발급해 iframe을 교체하세요.

연결 주소와 수신 메시지

아래 주소의 EVENT_ID를 실제 경기 ID로 바꾸세요. 애니메이션 주소는 event_id와 sport_id만 받으며 phase·snapshot·since는 사용하지 않습니다.

wss://odds-flow.com/animation/ws?event_id=EVENT_ID&sport_id=1
애니메이션 WebSocket 메시지 필드
필드의미
type메시지 종류. auth_required, authenticated, animation_snapshot, animation_update, animation_status
eventId / sportIdSwarm 경기 ID 문자열 / 종목 ID 숫자. 요청한 경기와 일치하는지 확인합니다
receivedAt데이터·상태 메시지의 수신 ISO 날짜·시간
sequence있을 때 경기별 메시지 순서·중복 확인용 숫자. 재연결 재생 커서가 아닙니다
datasnapshot/update에 담기는 병합된 전체 현재 상태. events도 현재 목록 전체로 교체합니다
status / messagestatus 메시지의 connecting·stale·unavailable 상태 / 한국어 안내
expiresAtauthenticated의 위젯 만료 시각. API 키 직접 인증은 null
timeoutMsauth_required의 인증 제한 시간. 기본 5000밀리초
애니메이션 data 필드
필드의미
homeTeam / awayTeam홈·원정 이름 문자열
score / scoreLabel{home, away} 점수 문자열 또는 null / 종목별 점수 명칭(선택)
clock / period경기 시간·진행 구간 문자열 또는 null. 원본이 없는 값은 만들지 않습니다
ball{x, y} 공 좌표(0~100) 또는 null. 위치가 없으면 대기 화면을 표시합니다
possessionhome, away 또는 null
action현재 상황의 id, type, text, team, occurredAt(Unix 밀리초 또는 null). 선택 필드
events최근 이벤트 배열. 각 항목의 id, type, text, time, team을 사용하며 전체 교체합니다
details / statistics세트·쿼터 점수/부가 지표·서브 정보 / 공급자 통계. 종목에 따른 선택 필드
raw크기·깊이를 제한해 보관하는 원본 참고 데이터. 선택 필드

animation_snapshot과 animation_update의 data는 서버에서 병합한 전체 현재 상태입니다. 이전 객체와 패치를 합치는 대신 data와 events를 교체하세요. score·clock·ball 등이 null이면 대기 상태를 표시합니다.

아래 JSON은 형식 예시이며 실시간 경기 값이 아닙니다. auth_required는 헤더 인증이 없는 연결에서만 받으며, 인증 성공 후 authenticated와 상태·데이터 메시지가 전달됩니다.

{"type":"auth_required","timeoutMs":5000}
{"type":"authenticated","eventId":"실제_경기_ID","sportId":1,"expiresAt":null}
{"type":"animation_snapshot","eventId":"실제_경기_ID","sportId":1,"receivedAt":"2026-10-28T00:00:00Z","sequence":1,"data":{"homeTeam":"홈팀","awayTeam":"원정팀","score":null,"clock":null,"period":null,"ball":null,"possession":null,"events":[]}}
{"type":"animation_status","eventId":"실제_경기_ID","sportId":1,"receivedAt":"2026-10-28T00:00:01Z","status":"unavailable","message":"이 경기의 애니메이션 데이터가 제공되지 않습니다."}

재연결·상태·접근 제한

  • sequence는 숫자가 있을 때 같은 연결에서 순서·중복을 확인하는 값입니다. 배당 id와 다르며 since 재생에 사용할 수 없습니다. 새 연결에서는 비교값을 초기화하고 다시 인증해 최신 캐시를 받습니다.
  • connecting은 연결 준비, stale은 최근 상태 확인·연결 복구, unavailable은 해당 경기의 데이터 미제공 상태입니다. unavailable도 정상적인 제공 상태로 화면에 안내하세요.
  • 배당과 애니메이션 시청 연결은 키의 최대 WebSocket 연결 수를 공유합니다. 키 중지·폐기·만료, 고객사 중지, 권한·허용 IP 제한은 1008, 수신 지연·동시 처리 한도는 1013, 서비스 오류는 1011로 종료될 수 있습니다.
  • IP 제한 키는 위젯 발급 서버와 실제 브라우저 시청자의 접속 IP를 각각 허용해야 합니다. 초기 위젯 메타데이터 조회만으로 실시간 피드를 받을 수는 없습니다.
  • 1011·1013이나 네트워크 단절 후에는 간격을 늘려 재연결하고, 1008은 키·IP·종목·live 권한과 위젯 만료를 확인한 뒤 다시 연결하세요.
연동 가이드 | ODDS FLOW | 스포츠 배당 데이터