이벤트 스트림 (SSE)
curl --request GET \
--url https://sajuapi.dev/v1/events \
--header 'X-API-Key: <api-key>'import requests
url = "https://sajuapi.dev/v1/events"
headers = {"X-API-Key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-API-Key': '<api-key>'}};
fetch('https://sajuapi.dev/v1/events', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://sajuapi.dev/v1/events",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://sajuapi.dev/v1/events"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-API-Key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://sajuapi.dev/v1/events")
.header("X-API-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://sajuapi.dev/v1/events")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-API-Key"] = '<api-key>'
response = http.request(request)
puts response.read_body시스템
이벤트 스트림 (SSE)
GET
/
v1
/
events
이벤트 스트림 (SSE)
curl --request GET \
--url https://sajuapi.dev/v1/events \
--header 'X-API-Key: <api-key>'import requests
url = "https://sajuapi.dev/v1/events"
headers = {"X-API-Key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-API-Key': '<api-key>'}};
fetch('https://sajuapi.dev/v1/events', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://sajuapi.dev/v1/events",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://sajuapi.dev/v1/events"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-API-Key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://sajuapi.dev/v1/events")
.header("X-API-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://sajuapi.dev/v1/events")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-API-Key"] = '<api-key>'
response = http.request(request)
puts response.read_bodyv1 Enterprise API (Coming Soon)이 엔드포인트는 Enterprise 버전에서 제공될 예정입니다.
현재는 v0 API를 사용하세요.
SSE는 단방향 스트리밍입니다. 양방향 통신이 필요하면 웹훅을 사용하세요.
Query 파라미터
string
수신할 이벤트 유형입니다. 쉼표로 구분합니다. 생략하면 모든 이벤트를 수신합니다.
string
특정 프로필의 이벤트만 수신합니다.
string
마지막으로 수신한 이벤트 ID입니다. 연결이 끊어진 후 재연결 시 사용합니다.
Response
성공
연결에 성공하면 SSE 스트림이 시작됩니다. 이벤트는text/event-stream 형식으로 전송됩니다.
실패
| 상태 코드 | 에러 타입 | 설명 |
|---|---|---|
| 401 | authentication_error | API 키가 유효하지 않음 |
| 429 | rate_limited | 동시 연결 수 초과 |
요청 예시
# 모든 이벤트 수신
curl -N -H "X-API-Key: bs_live_xxx" \
"https://api.sajuapi.dev/v1/events"
# 특정 이벤트만 수신
curl -N -H "X-API-Key: bs_live_xxx" \
"https://api.sajuapi.dev/v1/events?events=fortune.generated,fortune.cached"
# 특정 프로필 이벤트만 수신
curl -N -H "X-API-Key: bs_live_xxx" \
"https://api.sajuapi.dev/v1/events?profile_id=prf_abc123def456"
// EventSource 사용
const eventSource = new EventSource(
'https://api.sajuapi.dev/v1/events?events=fortune.generated',
{
headers: {
'X-API-Key': 'bs_live_xxx'
}
}
);
eventSource.onopen = () => {
console.log('연결됨');
};
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('이벤트 수신:', data);
};
eventSource.addEventListener('fortune.generated', (event) => {
const fortune = JSON.parse(event.data);
console.log('운세 생성됨:', fortune.fortune_id);
});
eventSource.onerror = (error) => {
console.error('SSE 에러:', error);
// 자동 재연결 시도됨
};
// 연결 종료
// eventSource.close();
import requests
import json
def stream_events(api_key, events=None):
params = {}
if events:
params['events'] = ','.join(events)
response = requests.get(
'https://api.sajuapi.dev/v1/events',
headers={'X-API-Key': api_key},
params=params,
stream=True
)
for line in response.iter_lines():
if line:
line = line.decode('utf-8')
if line.startswith('data:'):
data = json.loads(line[5:].strip())
yield data
# 사용 예시
for event in stream_events('bs_live_xxx', ['fortune.generated']):
print(f"이벤트: {event['type']}")
이벤트 형식
SSE 이벤트는 다음 형식으로 전송됩니다.event: fortune.generated
id: evt_abc123def456
data: {"type":"fortune.generated","fortune_id":"ftn_xyz789","profile_id":"prf_abc123","score":85,"timestamp":"2025-01-16T09:00:00Z"}
event: heartbeat
id: hb_1705395600
data: {"type":"heartbeat","timestamp":"2025-01-16T09:00:00Z"}
이벤트 유형
| 이벤트 | 설명 | 데이터 |
|---|---|---|
fortune.generated | 운세가 생성됨 | fortune_id, profile_id, score, model |
fortune.cached | 캐시된 운세 반환 | fortune_id, profile_id |
profile.created | 프로필이 생성됨 | profile_id, day_master |
profile.updated | 프로필이 수정됨 | profile_id |
profile.deleted | 프로필이 삭제됨 | profile_id |
daily.reset | 일일 운세 리셋 (자정) | date, profiles_count |
heartbeat | 연결 유지 확인 | timestamp |
응답 예시
fortune.generated
{
"type": "fortune.generated",
"fortune_id": "ftn_abc123def456",
"profile_id": "prf_xyz789abc123",
"fortune_type": "daily",
"fortune_date": "2025-01-16",
"score": 85,
"model": "haiku",
"cached": false,
"timestamp": "2025-01-16T09:00:00Z"
}
daily.reset
{
"type": "daily.reset",
"date": "2025-01-17",
"profiles_count": 5420,
"cache_cleared": true,
"timestamp": "2025-01-17T00:00:00+09:00"
}
heartbeat
{
"type": "heartbeat",
"timestamp": "2025-01-16T09:05:00Z"
}
React 통합 예시
import { useEffect, useState } from 'react';
function useEventStream(apiKey, events = []) {
const [lastEvent, setLastEvent] = useState(null);
const [connected, setConnected] = useState(false);
useEffect(() => {
const params = new URLSearchParams();
if (events.length > 0) {
params.set('events', events.join(','));
}
const eventSource = new EventSource(
`https://api.sajuapi.dev/v1/events?${params}`,
{ headers: { 'X-API-Key': apiKey } }
);
eventSource.onopen = () => setConnected(true);
eventSource.onerror = () => setConnected(false);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type !== 'heartbeat') {
setLastEvent(data);
}
};
return () => eventSource.close();
}, [apiKey, events.join(',')]);
return { lastEvent, connected };
}
// 사용 예시
function FortuneMonitor() {
const { lastEvent, connected } = useEventStream(
'bs_live_xxx',
['fortune.generated']
);
return (
<div>
<div>상태: {connected ? '연결됨' : '연결 끊김'}</div>
{lastEvent && (
<div>
최근 이벤트: {lastEvent.type} - {lastEvent.fortune_id}
</div>
)}
</div>
);
}
재연결 처리
연결이 끊어지면Last-Event-ID 헤더를 사용하여 누락된 이벤트를 복구할 수 있습니다.
let lastEventId = null;
function connect() {
const url = new URL('https://api.sajuapi.dev/v1/events');
if (lastEventId) {
url.searchParams.set('last_event_id', lastEventId);
}
const eventSource = new EventSource(url, {
headers: { 'X-API-Key': 'bs_live_xxx' }
});
eventSource.onmessage = (event) => {
lastEventId = event.lastEventId;
// 이벤트 처리
};
eventSource.onerror = () => {
eventSource.close();
// 5초 후 재연결
setTimeout(connect, 5000);
};
}
동시 SSE 연결 수는 API 키당 5개로 제한됩니다. 초과 시 가장 오래된 연결이 종료됩니다.
Heartbeat
서버는 30초마다heartbeat 이벤트를 전송하여 연결을 유지합니다. 60초 이상 이벤트가 없으면 연결이 끊어진 것으로 간주하고 재연결하세요.
이벤트는 최대 5분간 버퍼링됩니다. 5분 이상 연결이 끊어지면 일부 이벤트가 손실될 수 있습니다. 중요한 이벤트는 웹훅을 사용하세요.
⌘I