Play API

Anything a player can do in a DipGo game room, a program can do through this API: find and join games, move, pass, resign, agree on dead stones, and use the public and the team chat. It is for computer players and for people building their own front end.

Getting a token

The API authenticates with a personal access token. A person creates one, signed in, at /dashboard/api-tokens; it is shown once, and can be revoked there at any time. Every token the account has is also revoked when its password or email address changes, so a program needs a new one after that. That page is not linked from anywhere else on the site; this guide, llms.txt and the OpenAPI description are the way to it. Send it on every request:

Authorization: Bearer dg_…

A token has every right the account has, so give each program an account of its own and a token of its own, and treat the token like a password. There are no bot accounts and no bot flag: to everyone else on the site, a program is simply the player whose token it holds.

Everything lives under https://dipgo.net/api/v1, takes and returns JSON, and never sets a cookie. Cross-origin requests are allowed, so a front end hosted elsewhere can call it from a browser.

Quick start

Who am I?

curl -H "Authorization: Bearer $DIPGO_TOKEN" https://dipgo.net/api/v1/me

Is it my turn anywhere?

curl -H "Authorization: Bearer $DIPGO_TOKEN" "https://dipgo.net/api/v1/games?awaitingMe=true"

Read a game, then play at column 3, row 4 (counted from 0, from the top-left corner):

curl -H "Authorization: Bearer $DIPGO_TOKEN" https://dipgo.net/api/v1/games/4275

curl -H "Authorization: Bearer $DIPGO_TOKEN" -H "Content-Type: application/json" \
     -d '{"action":"play","x":3,"y":4}' https://dipgo.net/api/v1/games/4275/moves

Every action answers with {"result": "played", "game": {…}}: what happened, and the game as it now stands, so one round trip is enough to act and see the result.

Find a public game to join, and take a seat on team 2:

curl -H "Authorization: Bearer $DIPGO_TOKEN" https://dipgo.net/api/v1/games/open

curl -H "Authorization: Bearer $DIPGO_TOKEN" -H "Content-Type: application/json" \
     -d '{"team":2}' https://dipgo.net/api/v1/games/4300/join

Endpoints

The full description, with every request and response schema, is the OpenAPI document.

MethodPathWhat it does
GET/meThe account behind the token.
GET/tokensThe account's live tokens (names and prefixes, never secrets).
DELETE/tokens/{id}Revoke a token.
GET/gamesThe account's games. ?status=waiting|confirming|playing|scoring|finished; ?awaitingMe=true keeps only games that need the account to act.
GET/games/openPublic rooms still looking for players.
POST/gamesCreate a room: name, numberOfTeams (2–9), playersPerTeam (1–4), boardSize (5–25), secondsPerMove, captures, removeStones, komi, allowedTimeouts, unlisted.
GET/games/{id}The game state (below).
GET/games/{id}/sgfThe record as SGF.
POST/games/{id}/joinJoin a room; {"team": 2} also takes a seat. A private room needs {"key": "…"}, the key from its link (its players see it as joinKey on the game), unless the caller was invited. Without either, the room does not exist as far as the caller is concerned: the join, GET /games/{id}, its record and its chat are all 404 until the game starts, so ids cannot be walked to find one. The join that fills a private room starts the game, so there a taken seat refuses the join.
POST/games/{id}/team{"team": n}: move to a team in a room already joined.
POST/games/{id}/confirmConfirm participation once a public room is full. A private room starts on the join that fills it and needs no confirmation.
POST/games/{id}/leaveLeave a room that has not started.
POST/games/{id}/kickCreator only: {"accountId": n} removes a player.
POST/games/{id}/cancelCreator only: cancel an unstarted room.
POST/games/{id}/invite{"to": "handle or email"}.
POST/games/{id}/moves{"action":"play","x":3,"y":4}, {"action":"pass"} or {"action":"resign"}.
POST/games/{id}/scoring/toggle{"x":3,"y":4}: mark the group there dead, or alive again.
POST/games/{id}/scoring/acceptAccept the dead-stone marking for your team. The game ends when every team present has.
POST/games/{id}/scoring/resumeDisagree: go back to playing. The passes that ended play still stand, so a pass, or a missed turn, from the player to move ends play again at once; play a stone to go on.
GET, POST/games/{id}/chatThe public chat, open to spectators too. {"message": "…"}, up to 200 characters.
GET, POST/games/{id}/team-chatThe private chat, players only. {"message": "…", "recipients": [1, 3]}; your own team is always included.

How a game proceeds: a room is waiting until every seat is taken, then a public room is confirming until every player confirms, then playing. A private (unlisted) room skips the confirmations and is playing as soon as it fills. When every remaining team passes in turn the game pauses for scoring, where teams mark dead stones and accept the marking; when all have accepted it is finished. A team whose players have all resigned or timed out is out, and its turns are skipped.

The game state

GET /games/{id} is one document with everything in it. Abridged:

{
  "id": 4275, "name": "The Hive Five", "status": "playing",
  "boardSize": 21, "numberOfTeams": 5, "playersPerTeam": 1,
  "rules": {"captures": false, "removeStones": false, "allowedTimeouts": 5,
            "secondsPerMove": 86400, "komi": {}, "scoring": "area",
            "superko": "positional", "suicide": false},
  "teams": [{"number": 1, "out": false, "captures": 0,
             "players": [{"accountId": 12, "handle": "alice", "seat": 1,
                          "confirmed": true, "resigned": false, "timedOut": false, "bot": false}],
             "score": {"points": 115, "komi": 0, "captures": 0, "total": 115}}, …],
  "turn": {"team": 2, "accountId": 15, "handle": "bob", "deadline": "2026-10-15T12:00:00Z", "isYou": false},
  "you":  {"accountId": 12, "team": 1, "seat": 1, "confirmed": true, "resigned": false, "mustAct": false},
  "board": [".....", "..1..", ".2.3.", …],
  "lastMove": {"number": 545, "team": 5, "accountId": 19, "type": "stone", "x": 3, "y": 3, "at": "…"},
  "moves": [ … ],
  "scoring": null,
  "result": null,
  "sgf": "(;FF[4]GM[1]…)",
  "links": {"web": "https://dipgo.net/dashboard/game/4275", "self": "https://dipgo.net/api/v1/games/4275"}
}
  • Coordinates. x is the column, 0 at the left; y is the row, 0 at the top. The same as SGF. The labels on the web page ("G15") skip the letter I and count rows from the top.
  • board is one string per row, top row first: . for an empty point, the team number for a stone. Dead stones stay on the board during and after scoring; scoring.deadStones lists them.
  • moves[].type is stone, pass, resign, timeout (the player was removed) or missedTurn (a forgiven timeout: the player stays, the turn is lost).
  • turn is present only while the game is playing. you.mustAct is true when the game is waiting on you: your move, your confirmation, or your team's acceptance of the marking.
  • scoring carries deadStones, acceptedTeams and presentTeams during the agreement; result carries winners, annulled and every team's score once the game is finished.
  • score per team is DipGo's area score as it stands: stones plus territory, plus captures if the game counts them, plus komi.

Errors

Every error is {"error": "code", "message": "a sentence"}. Switch on the code; show the message.

StatusCodes
400validation, bad_request, off_board, empty_point
401unauthorised: no token, or one that was revoked
403forbidden: not a player in the game, or not the creator
404not_found: no such game, or no such route
405method_not_allowed, with an Allow header
406not_acceptable: the Accept header rules out JSON
415unsupported_media_type: the body was not sent as application/json
409not_your_turn, occupied, suicide, ko, already_resigned, game_not_started, game_finished, in_scoring_phase, not_in_scoring_phase, cannot_score, cannot_join, team_full, cannot_leave, cannot_confirm, cannot_kick, cannot_cancel, cannot_invite, no_team, rejected
429rate_limited, with a Retry-After header: the account's budget for the minute is spent, or the flood defence in front of the API has held this token (or, for calls with no token, this address) to a few requests a second; the message says which

Limits and manners

  • Each account may make 120 requests a minute, across all its tokens. The reply carries X-RateLimit-Remaining; a 429 carries Retry-After.
  • There is no push channel. GET /games/{id} answers 304 Not Modified to If-None-Match with the ETag it last gave, so polling an unchanged game costs almost nothing to transfer. To learn whether anything needs you, poll GET /games?awaitingMe=true rather than every game.
  • Poll a live game no more than once a second, and a correspondence game far less often: the state carries rules.secondsPerMove and turn.deadline for exactly this.
  • Send a User-Agent that names your program, so that we can get in touch if something goes wrong.
  • A program is a player. It is bound by the same rules of conduct as everyone else; the chat is for people.

SGF

Every game state carries the record as SGF in its sgf field, and GET /games/{id}/sgf serves it on its own. A two-team game is ordinary FF[4]: B and W moves, team-mates in PB and PW separated by & , KM, RE. Any SGF tool can open it.

Go with three or more colours has no standard notation, so games with three or more teams use SGF-MC, a profile of FF[4] designed for DipGo. Its moves are X[colour:point] with the colour's number, a seat number in XS when teams have more than one player, a player leaving in XE[colour:seat:R|T|M] (resigned, timed out, missed a turn), an AE setup node when an eliminated team's stones are removed, and the result in RX and XW. A tool that knows only two colours shows an empty board rather than a wrong one. Captures are computed on replay: every adjacent enemy group left without liberties is removed together.

Points are two letters, column then row, a = 0, from the top-left; the letter i is not skipped.

For language models: /llms.txt says the same in brief.