{
  "openapi": "3.1.0",
  "info": {
    "title": "InkPGN",
    "version": "1.0.0",
    "summary": "Handwritten chess scoresheet to PGN, with the player's own thinking in it.",
    "description": "Photograph a handwritten chess scoresheet and get a PGN you can trust. Every move read from the sheet is replayed on a board; a move the position cannot account for is flagged for the player to confirm, never silently guessed. Once the player confirms the moves, an engine finds the moments where the game turned and asks what the player was thinking there. Their answers are saved into the PGN as comments.\n\nTypical flow: readSheet, then poll getReading until it is done. Show the player the flagged moves and fix them with editMoves. Then confirmMoves, and poll getScan until it is done. Put each moment's question to the player and save their words with addNote; revealMove shows the engine's view only after that. Finish with saveSummary, and fetch the file with getPgn.\n\nThe questions are for the player. Relay them and record the player's own words; do not answer on their behalf. The value of the PGN is what they were thinking.\n\nNo key or account. A game is addressed by its id, which is the only access control: treat an id like a private link. Send a User-Agent that names your client, because Python's default Python-urllib one is refused at the edge. A refusal (no such game, an illegal move, the daily limit) comes back as HTTP 200 with an `error` field.",
    "termsOfService": "https://inkpgn.com/terms",
    "contact": { "name": "InkPGN", "email": "hello@inkpgn.com", "url": "https://inkpgn.com" }
  },
  "servers": [{ "url": "https://inkpgn.com/api" }],
  "externalDocs": { "description": "The same API, described for language models", "url": "https://inkpgn.com/llms.txt" },
  "paths": {
    "/read": {
      "post": {
        "operationId": "readSheet",
        "summary": "Start reading a scoresheet from photographs or typed moves",
        "description": "Starts a background reading and returns a job id at once. Poll getReading with it. Give either `images` or `text`. A photograph usually takes 10 to 60 seconds. Photographed sheets count against a limit of 20 a day per connection, and there can be at most 3 calls in 10 seconds.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object",
            "properties": {
              "images": {
                "type": "array", "maxItems": 6,
                "description": "The sheet's pages in order, each a base64 data URL such as `data:image/jpeg;base64,...` (jpeg, png, webp or heic). The whole request must stay under 12 MB.",
                "items": { "type": "string" }
              },
              "text": { "type": "string", "description": "Typed movetext instead of photographs, e.g. `1. e4 e5 2. Nf3 Nc6`. No model is used and it does not count against the daily limit." },
              "color": { "type": "string", "enum": ["white", "black"], "description": "Whose game this is, so the questions are asked from their side." },
              "hint": { "type": "string", "maxLength": 300, "description": "A sentence about the sheet for the reader, e.g. 'two pages, the second starts at move 41'." }
            }
          } } }
        },
        "responses": { "200": { "description": "The job id, or an error", "content": { "application/json": { "schema": {
          "type": "object",
          "properties": { "job": { "type": "string" }, "error": { "type": "string" } }
        } } } } }
      }
    },
    "/reading": {
      "get": {
        "operationId": "getReading",
        "summary": "Poll a reading started by readSheet",
        "description": "Call every one to two seconds until `state` is `done` or `failed`. Jobs are forgotten ten minutes after they finish.",
        "parameters": [{ "name": "job", "in": "query", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "The reading's state, and the result once it is done", "content": { "application/json": { "schema": {
          "type": "object",
          "properties": {
            "state": { "type": "string", "description": "`done` or `failed` when finished, `none` for an unknown job; anything else means it is still working." },
            "waited": { "type": "integer", "description": "Seconds since it started." },
            "error": { "type": "string", "description": "Why it failed, e.g. no moves could be read from the photograph." },
            "result": {
              "type": "object",
              "properties": {
                "session": { "$ref": "#/components/schemas/Session" },
                "plies": { "type": "array", "items": { "$ref": "#/components/schemas/Ply" } },
                "flagged": { "type": "integer", "description": "How many moves the player should look at." }
              }
            }
          }
        } } } } }
      }
    },
    "/edit": {
      "post": {
        "operationId": "editMoves",
        "summary": "Save corrected moves and game details without analysing",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Edit" } } } },
        "responses": { "200": { "$ref": "#/components/responses/Edited" } }
      }
    },
    "/confirm": {
      "post": {
        "operationId": "confirmMoves",
        "summary": "Confirm the moves as the player's and start the engine scan",
        "description": "Does what editMoves does, then marks the moves as confirmed and starts the scan. It refuses, with `error` and `bad_at`, until every move is legal. Only confirm moves the player has checked.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Edit" } } } },
        "responses": { "200": { "$ref": "#/components/responses/Edited" } }
      }
    },
    "/scan": {
      "get": {
        "operationId": "getScan",
        "summary": "Poll the engine scan and get the questions",
        "description": "Call every few seconds after confirmMoves until `state` is `done`. The engine's verdict is deliberately left out; it is only shown by revealMove.",
        "parameters": [{ "$ref": "#/components/parameters/Id" }],
        "responses": { "200": { "description": "The scan's state, and the moments once it is done", "content": { "application/json": { "schema": {
          "type": "object",
          "properties": {
            "state": { "type": "string", "enum": ["none", "queued", "running", "done", "failed"] },
            "done": { "type": "integer" },
            "total": { "type": "integer" },
            "moments": { "type": "array", "items": { "$ref": "#/components/schemas/Moment" } },
            "error": { "type": "string" }
          }
        } } } } }
      }
    },
    "/note": {
      "post": {
        "operationId": "addNote",
        "summary": "Save the player's own words about a move as a PGN comment",
        "description": "Any move can carry a note, not only the moments. Empty text deletes the note.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": {
          "type": "object", "required": ["id", "ply", "text"],
          "properties": {
            "id": { "type": "string" },
            "ply": { "type": "integer", "description": "0-based half-move index, as in a moment's `i`." },
            "text": { "type": "string" }
          }
        } } } },
        "responses": { "200": { "description": "The updated game", "content": { "application/json": { "schema": {
          "type": "object",
          "properties": { "ok": { "type": "boolean" }, "pgn": { "type": "string" }, "session": { "$ref": "#/components/schemas/Session" }, "error": { "type": "string" } }
        } } } } }
      }
    },
    "/reveal": {
      "post": {
        "operationId": "revealMove",
        "summary": "What the engine thought of a move, after the player has answered",
        "description": "Call only once the player has written their note for this move. The reply takes their note into account.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": {
          "type": "object", "required": ["id", "ply"],
          "properties": { "id": { "type": "string" }, "ply": { "type": "integer" } }
        } } } },
        "responses": { "200": { "description": "The engine's view", "content": { "application/json": { "schema": {
          "type": "object",
          "properties": {
            "text": { "type": "string", "description": "An answer to the player's note, in words." },
            "numbers": { "description": "The engine's evaluations behind it." },
            "san": { "type": "string" }, "no": { "type": "integer" }, "side": { "type": "string" }, "fen": { "type": "string" },
            "error": { "type": "string" }
          }
        } } } } }
      }
    },
    "/summary": {
      "post": {
        "operationId": "saveSummary",
        "summary": "Save a closing comment for the whole game and mark it finished",
        "requestBody": { "required": true, "content": { "application/json": { "schema": {
          "type": "object", "required": ["id"],
          "properties": {
            "id": { "type": "string" },
            "text": { "type": "string", "description": "The player's summary of the game, in their words." },
            "done": { "type": "boolean", "description": "true marks the game reviewed." }
          }
        } } } },
        "responses": { "200": { "description": "The updated game", "content": { "application/json": { "schema": {
          "type": "object",
          "properties": { "ok": { "type": "boolean" }, "pgn": { "type": "string" }, "error": { "type": "string" } }
        } } } } }
      }
    },
    "/pgn": {
      "get": {
        "operationId": "getPgn",
        "summary": "The game as a PGN file: headers, every move and every comment",
        "parameters": [{ "$ref": "#/components/parameters/Id" }],
        "responses": { "200": { "description": "The PGN", "content": { "application/json": { "schema": {
          "type": "object",
          "properties": { "pgn": { "type": "string" }, "error": { "type": "string" } }
        } } } } }
      }
    },
    "/session": {
      "get": {
        "operationId": "getGame",
        "summary": "Everything about a game: moves, notes, moments and PGN",
        "parameters": [{ "$ref": "#/components/parameters/Id" }],
        "responses": { "200": { "description": "The game", "content": { "application/json": { "schema": {
          "type": "object",
          "properties": {
            "session": { "$ref": "#/components/schemas/Session" },
            "plies": { "type": "array", "items": { "$ref": "#/components/schemas/Ply" } },
            "pgn": { "type": "string" },
            "error": { "type": "string" }
          }
        } } } } }
      }
    },
    "/forget": {
      "post": {
        "operationId": "deleteGame",
        "summary": "Delete a game, its PGN and its photographs for good",
        "description": "Cannot be undone. Only on the player's request.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": {
          "type": "object", "required": ["id"], "properties": { "id": { "type": "string" } }
        } } } },
        "responses": { "200": { "description": "What was deleted", "content": { "application/json": { "schema": {
          "type": "object",
          "properties": { "ok": { "type": "boolean" }, "deleted": { "type": "array", "items": { "type": "string" } }, "error": { "type": "string" } }
        } } } } }
      }
    }
  },
  "components": {
    "parameters": {
      "Id": { "name": "id", "in": "query", "required": true, "description": "The game's id, from readSheet's result.", "schema": { "type": "string" } }
    },
    "schemas": {
      "Edit": {
        "type": "object", "required": ["id", "sans"],
        "properties": {
          "id": { "type": "string" },
          "sans": { "type": "array", "items": { "type": "string" }, "description": "Every move of the game in order, in SAN (e.g. `Nf3`, `O-O`)." },
          "headers": { "type": "object", "additionalProperties": { "type": "string" }, "description": "PGN tags such as White, Black, Event, Site, Date, Round, Result." },
          "color": { "type": "string", "enum": ["white", "black"] }
        }
      },
      "Session": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "description": "The game's id. A person can open it at https://inkpgn.com/#<id>." },
          "headers": { "type": "object", "additionalProperties": { "type": "string" } },
          "sans": { "type": "array", "items": { "type": "string" } },
          "color": { "type": "string" },
          "notes": { "type": "object", "additionalProperties": { "type": "string" }, "description": "The player's notes, keyed by 0-based ply." },
          "summary": { "type": "string" },
          "confirmed": { "type": "boolean" },
          "finished": { "type": "string" },
          "unreadable": { "type": "string", "description": "What the reader could not make out, in its own words." },
          "moments": { "type": "array", "items": { "$ref": "#/components/schemas/Moment" } }
        }
      },
      "Ply": {
        "type": "object",
        "properties": {
          "san": { "type": "string" },
          "status": { "type": "string", "description": "`sure`: what the sheet says. `fixed`: repaired into a legal move. `unsure`: a doubtful repair. `guess`: a move missing from the sheet, inserted. `spare`: something on the sheet that was left out. `extra` and `after`: cells past the end of the game. Show the player everything that is not `sure`." }
        }
      },
      "Moment": {
        "type": "object",
        "properties": {
          "i": { "type": "integer", "description": "0-based ply, the value addNote and revealMove take." },
          "no": { "type": "integer", "description": "Move number." },
          "side": { "type": "string", "enum": ["w", "b"] },
          "san": { "type": "string" },
          "before": { "type": "string", "description": "FEN of the position before the move." },
          "question": { "type": "string", "description": "What to ask the player." },
          "who": { "type": "string", "enum": ["you", "opponent"], "description": "Whether the player or their opponent made the move." },
          "note": { "type": "string", "description": "The player's answer so far." }
        }
      }
    },
    "responses": {
      "Edited": {
        "description": "The moves checked on a board, and the updated game",
        "content": { "application/json": { "schema": {
          "type": "object",
          "properties": {
            "bad_at": { "type": ["integer", "null"], "description": "0-based index of the first illegal move, or null when every move is legal." },
            "outcome": { "description": "Checkmate or stalemate at the end, if any." },
            "fen": { "type": "string", "description": "The final position." },
            "pgn": { "type": "string" },
            "session": { "$ref": "#/components/schemas/Session" },
            "scanning": { "type": "boolean", "description": "confirmMoves only: the scan has started." },
            "error": { "type": "string" }
          }
        } } }
      }
    }
  }
}
