OGS Scripting Tips (2025)

I’ve been doing some client coding lately, and wanted to share some tips for anyone who wants to try writing their own client code or script.

Servers

When interacting with OGS, you will mainly interact with the Django server (https://online-go.com/api/...) and the websocket (wss://online-go.com/ws). You may also come across the termination API (https://online-go.com/termination-api) and the AI server (https://ai.online-go.com, wss://ai.online-go.com/)

Since there are many different servers, you need to pass a user JWT around to verify the user. You can see an example of this in the script below where the JWT comes from the login endpoint and is passed to the websocket.

Docs

IMO docs are sparse, and not always up-to-date, but the new WebSocket docs are pretty solid: protocol | goban

Otherwise, I’d avoid docs and look at the Network Tab directly (XHR and WS) to see what kind of traffic the official client is sending and receiving. Reading the source code for online-go.com, goban or any of the other open source clients is also a good resource.

What’s new?

  • Raw websockets instead of socket.io. While the socket.io endpoint still exists (wss://online-go.com/socket.io/), raw websockets are the official way to interact with OGS. This is documented at protocol | goban
  • No need for oauth*

Examples

Short and sweet Python script (76 LOC)
#!/usr/bin/env python3
"""
Minimal OGS API demo - login and make a move
Requires: pip install requests websocket-client
"""

import json
import requests
import websocket
from string import ascii_lowercase

server_url = "https://online-go.com"
ws_url = "wss://online-go.com/ws"

session = requests.Session()

# Step 1: Login
username = input("Username: ")
password = input("Password: ")

login_result = session.post(f"{server_url}/api/v0/login", json={'username': username, 'password': password}).json()
jwt_token = login_result['user_jwt']
current_user_id = login_result['user']['id']
print(f"Logged in as {login_result['user']['username']} (ID: {current_user_id})")

# Step 2: Connect to WebSocket and authenticate
ws = websocket.create_connection(ws_url)
ws.send(json.dumps(['authenticate', {'jwt': jwt_token}]))
print("Connected and authenticated to WebSocket")

# Step 3: Get ongoing games
overview = session.get(f"{server_url}/api/v1/ui/overview").json()
games = overview['active_games']

print(f"\nFound {len(games)} active games:")
for i, game in enumerate(games):
    print(f"{i+1}. Game {game['id']} - {game['black']['username']} vs {game['white']['username']}")

# Step 4: Select a game
game_idx = int(input("\nSelect game number: ")) - 1
game_id = games[game_idx]['id']

# Step 5: Get game state
game_state = session.get(f"{server_url}/termination-api/game/{game_id}/state").json()

print(f"\nGame {game_id}:")

# Print board state
board = game_state['board']
size = len(board)
sgf_coords = ascii_lowercase[:size]

print("\nBoard state:")
print("   " + " ".join(sgf_coords))
for i, row in enumerate(board):
    row_label = sgf_coords[i]
    print(f" {row_label} " + ' '.join('.' if cell == 0 else 'X' if cell == 1 else 'O' for cell in row))

# Check if it's our turn
player_to_move = game_state['player_to_move']

if player_to_move != current_user_id:
    print(f"\nIt's not your turn. Waiting for opponent to move.")
    ws.close()
    exit(0)

# Step 6: Connect to game
ws.send(json.dumps(['game/connect', {'game_id': game_id}]))

# Step 7: Make a move
move = input("\nEnter move (e.g. 'ab' or '..' for pass): ")
move_msg = json.dumps(['game/move', {'game_id': game_id, 'move': move}])
ws.send(move_msg)

ws.close()
print("\nMove sent!")

Dart client implementation for WeiqiHub (more productionized, but still pretty simple): WeiqiHub/lib/game_client/ogs at main · ale64bit/WeiqiHub · GitHub

14 Likes

Thank you for the great post and also linking to it at my question for a live game viewer only Beginner concept question about OGS API

I ask my question regarding move notification without authentication in that thread.

2 Likes