Stream Incoming Events
curl --request GET \
--url https://api.example.com/api/stream/event \
--header 'Authorization: <authorization>'import requests
url = "https://api.example.com/api/stream/event"
headers = {"Authorization": "<authorization>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: '<authorization>'}};
fetch('https://api.example.com/api/stream/event', 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://api.example.com/api/stream/event",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: <authorization>"
],
]);
$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://api.example.com/api/stream/event"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "<authorization>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.example.com/api/stream/event")
.header("Authorization", "<authorization>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/stream/event")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = '<authorization>'
response = http.request(request)
puts response.read_body{
"type": "<string>",
"game": {
"gameId": "<string>",
"fullId": "<string>",
"color": "<string>",
"fen": "<string>",
"hasMoved": true,
"isMyTurn": true,
"lastMove": "<string>",
"opponent": {},
"perf": "<string>",
"rated": true,
"speed": "<string>",
"variant": {},
"status": {},
"winner": "<string>",
"source": "<string>",
"compat": {}
},
"challenge": {
"id": "<string>",
"url": "<string>",
"status": "<string>",
"challenger": {},
"destUser": {},
"variant": {},
"rated": true,
"speed": "<string>",
"timeControl": {},
"color": "<string>",
"perf": {},
"declineReason": "<string>"
}
}TV & Streaming
Stream Incoming Events
Stream personal events including game starts, game finishes, and challenge notifications
GET
/
api
/
stream
/
event
Stream Incoming Events
curl --request GET \
--url https://api.example.com/api/stream/event \
--header 'Authorization: <authorization>'import requests
url = "https://api.example.com/api/stream/event"
headers = {"Authorization": "<authorization>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: '<authorization>'}};
fetch('https://api.example.com/api/stream/event', 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://api.example.com/api/stream/event",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: <authorization>"
],
]);
$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://api.example.com/api/stream/event"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "<authorization>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.example.com/api/stream/event")
.header("Authorization", "<authorization>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/stream/event")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = '<authorization>'
response = http.request(request)
puts response.read_body{
"type": "<string>",
"game": {
"gameId": "<string>",
"fullId": "<string>",
"color": "<string>",
"fen": "<string>",
"hasMoved": true,
"isMyTurn": true,
"lastMove": "<string>",
"opponent": {},
"perf": "<string>",
"rated": true,
"speed": "<string>",
"variant": {},
"status": {},
"winner": "<string>",
"source": "<string>",
"compat": {}
},
"challenge": {
"id": "<string>",
"url": "<string>",
"status": "<string>",
"challenger": {},
"destUser": {},
"variant": {},
"rated": true,
"speed": "<string>",
"timeControl": {},
"color": "<string>",
"perf": {},
"declineReason": "<string>"
}
}Overview
Stream events for the authenticated user in real-time. This endpoint pushes notifications for:- Games starting
- Games finishing
- Incoming challenges
- Challenge cancellations and declinations
- Rematch offers
Endpoint
GET https://lichess.org/api/stream/event
Authentication
Required. You must authenticate with an OAuth2 token that has one of the following scopes:bot:play- For bot accountsboard:play- For board/external applicationschallenge:read- For reading challenges only
string
required
Bearer token for authentication
Authorization: Bearer lip_yourtokenhere
Response Format
The stream returns newline-delimited JSON (NDJSON). Each line is a complete JSON object representing an event. Keep-alive messages (empty lines or objects with notype field) are sent periodically to prevent connection timeouts.
Example Requests
cURL
curl https://lichess.org/api/stream/event \
-H "Authorization: Bearer lip_yourtokenhere"
JavaScript
const token = 'lip_yourtokenhere';
const url = 'https://lichess.org/api/stream/event';
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const lines = decoder.decode(value).split('\n');
for (const line of lines) {
if (line.trim()) {
const event = JSON.parse(line);
console.log('Event:', event);
}
}
}
Python
import requests
import json
token = 'lip_yourtokenhere'
url = 'https://lichess.org/api/stream/event'
headers = {'Authorization': f'Bearer {token}'}
with requests.get(url, headers=headers, stream=True) as response:
for line in response.iter_lines():
if line:
event = json.loads(line)
if 'type' in event:
print(f"{event['type']} event:", event)
Python - python-chess Library
import berserk
session = berserk.TokenSession('lip_yourtokenhere')
client = berserk.Client(session)
for event in client.board.stream_incoming_events():
if event['type'] == 'gameStart':
game_id = event['game']['gameId']
print(f"Game started: {game_id}")
elif event['type'] == 'challenge':
challenge_id = event['challenge']['id']
print(f"Challenge received: {challenge_id}")
Event Types
Game Start Event
Sent when a game starts involving the authenticated user:{
"type": "gameStart",
"game": {
"gameId": "5IrD6Gzz",
"fullId": "5IrD6Gzz5IrD",
"color": "white",
"fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
"hasMoved": false,
"isMyTurn": true,
"lastMove": "",
"opponent": {
"id": "opponent_username",
"username": "OpponentName",
"rating": 1500
},
"perf": "blitz",
"rated": true,
"secondsLeft": 300,
"source": "lobby",
"status": {
"id": 20,
"name": "started"
},
"variant": {
"key": "standard",
"name": "Standard"
},
"speed": "blitz",
"id": "5IrD6Gzz",
"compat": {
"bot": true,
"board": true
}
}
}
Game Finish Event
Sent when a game involving the authenticated user finishes:{
"type": "gameFinish",
"game": {
"gameId": "5IrD6Gzz",
"fullId": "5IrD6Gzz5IrD",
"color": "white",
"fen": "r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 4 4",
"hasMoved": true,
"isMyTurn": false,
"lastMove": "f6e4",
"opponent": {
"id": "opponent_username",
"username": "OpponentName",
"rating": 1500
},
"perf": "blitz",
"rated": true,
"source": "lobby",
"status": {
"id": 30,
"name": "mate"
},
"variant": {
"key": "standard",
"name": "Standard"
},
"speed": "blitz",
"winner": "black",
"id": "5IrD6Gzz",
"compat": {
"bot": true,
"board": true
}
}
}
Challenge Event
Sent when someone challenges the authenticated user:{
"type": "challenge",
"challenge": {
"id": "pClLeFUy",
"url": "https://lichess.org/pClLeFUy",
"status": "created",
"challenger": {
"id": "challenger_id",
"name": "ChallengerName",
"title": "GM",
"rating": 2500,
"provisional": false,
"online": true
},
"destUser": {
"id": "your_user_id",
"name": "YourName",
"title": "IM",
"rating": 2400,
"provisional": false,
"online": true
},
"variant": {
"key": "standard",
"name": "Standard",
"short": "Std"
},
"rated": true,
"speed": "blitz",
"timeControl": {
"type": "clock",
"limit": 300,
"increment": 3,
"show": "5+3"
},
"color": "random",
"perf": {
"icon": ")",
"name": "Blitz"
}
},
"compat": {
"bot": true,
"board": true
}
}
Challenge Declined Event
Sent when a challenge you created is declined:{
"type": "challengeDeclined",
"challenge": {
"id": "pClLeFUy",
"url": "https://lichess.org/pClLeFUy",
"status": "declined",
"challenger": {
"id": "your_user_id",
"name": "YourName"
},
"destUser": {
"id": "opponent_id",
"name": "OpponentName"
},
"variant": {
"key": "standard",
"name": "Standard"
},
"rated": true,
"speed": "blitz",
"timeControl": {
"type": "clock",
"limit": 300,
"increment": 3,
"show": "5+3"
},
"declineReason": "generic"
}
}
Challenge Canceled Event
Sent when a challenge is canceled:{
"type": "challengeCanceled",
"challenge": {
"id": "pClLeFUy",
"url": "https://lichess.org/pClLeFUy",
"status": "canceled",
"challenger": {
"id": "challenger_id",
"name": "ChallengerName"
},
"destUser": {
"id": "your_user_id",
"name": "YourName"
},
"variant": {
"key": "standard",
"name": "Standard"
}
}
}
Response Fields
string
Event type:
gameStart, gameFinish, challenge, challengeDeclined, challengeCanceledobject
Game object (for
gameStart and gameFinish events)Show Game Fields
Show Game Fields
string
Game ID (8 characters)
string
Full game ID including player token (12 characters)
string
Your color in the game (“white” or “black”)
string
Current position in FEN notation
boolean
Whether any moves have been played
boolean
Whether it’s your turn to move
string
Last move in UCI notation (e.g., “e2e4”)
object
Opponent information including id, username, and rating
string
Performance category (“bullet”, “blitz”, “rapid”, etc.)
boolean
Whether the game is rated
string
Game speed category
object
Chess variant information
object
Game status with id and name
string
Winner color (“white” or “black”) - only in gameFinish events
string
How the game was created (“lobby”, “friend”, “ai”, etc.)
object
Compatibility flags for bot and board API
object
Challenge object (for challenge-related events)
Show Challenge Fields
Show Challenge Fields
string
Challenge ID
string
URL to view/accept the challenge
string
Challenge status: “created”, “declined”, “canceled”, “accepted”
object
User who created the challenge
object
User who was challenged
object
Chess variant for the challenge
boolean
Whether the game will be rated
string
Game speed category
object
Time control settings (clock, correspondence, or unlimited)
string
Color preference: “white”, “black”, or “random”
object
Performance category information
string
Reason for decline (only in challengeDeclined events)
Connection Management
- Keep-alive: The stream sends periodic keep-alive messages (empty objects or newlines) every 7 seconds to prevent timeout
- User presence: Maintaining an active connection marks the user as online
- Single connection: Only one active event stream is recommended per token. New connections with the same bearer token will terminate previous ones
- Reconnection: If disconnected, implement exponential backoff for reconnection attempts
Rate Limiting
- Event streams are rate-limited per bearer token
- Do not poll this endpoint - it’s designed as a persistent stream
- Excessive connection attempts will result in temporary blocking
- Maximum concurrent connections are limited per account
Important Notes
Do not poll this endpoint. This endpoint is designed to be held open as a persistent streaming connection. Repeatedly connecting and disconnecting will result in rate limiting.
- The stream includes both games you initiated and games started through matchmaking
- When receiving a
gameStartevent, you should immediately connect to the game stream using the game ID - The
fullIdfield contains the authentication token needed for making moves - Challenge events include a
compatfield indicating if the challenge is compatible with bot/board APIs - Games with
"compat": {"bot": false}cannot be played via the Bot API (e.g., casual games for verified bots)
Typical Workflow
- Connect to the event stream on application startup
- Receive gameStart event with game details
- Connect to game stream using
/api/board/game/stream/{gameId}to play moves - Handle challenges by accepting or declining via challenge endpoints
- Maintain connection indefinitely, reconnecting on disconnection
Error Handling
- 401 Unauthorized: Invalid or expired token - refresh your OAuth token
- 429 Too Many Requests: Rate limited - implement backoff before reconnecting
- Connection drops: Implement automatic reconnection with exponential backoff
Related Endpoints
- Stream Game State - Stream moves and state for a specific game
- Accept Challenge - Accept incoming challenges
- Get Current TV Channels - Get current TV games
- Stream TV Feed - Stream public TV games
Source Code Reference
- Controller:
app/controllers/Api.scala:292-303(eventStream method) - Event Stream Implementation:
modules/api/src/main/EventStream.scala - Routes:
conf/routes:767(GET /api/stream/event)

