This is the full developer documentation for KRKplaysTennis API # KRKplaysTennis API > The KRKplaysTennis API rates amateur tennis players in Kraków from the results of the matches they play. The KRKplaysTennis API rates amateur tennis players in Kraków from the results of league and tournament matches. Use it to show players their rating, explain why it changed, suggest fair opponents, or submit results for a competition you run. A fictional API KRKplaysTennis is a fictional API, built as a technical writing portfolio. The players, venues, and competitions are invented. You can run every example in these docs against a local mock server. ## How ratings work [Section titled “How ratings work”](#how-ratings-work) * A new player picks a starting level on the [NTRP self-rating scale](https://www.usta.com/en/home/coach-organize/tennis-tool-center/run-usta-programs/national/understanding-ntrp-ratings.html), from 1.5 to 5.5. The API converts it to a starting rating between 1000 and 1800. * Each rated match moves the rating up or down, based on the result and on the opponent’s rating. * A confidence value from 0 to 100 shows how settled the rating is. A new rating moves quickly, and a settled rating moves slowly. The rating comes from results, not from the self-assessment. The self-assessed level is only the starting point. For the full model, with worked examples, see [How ratings work](/krk-plays-tennis-api/concepts/how-ratings-work/). ## What you can do [Section titled “What you can do”](#what-you-can-do) | Task | Guide | Operations | | ------------------------------------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Read player profiles and ratings | [Quickstart](/krk-plays-tennis-api/quickstart/) | [List players](/krk-plays-tennis-api/reference/operations/listplayers/), [Get a player’s rating](/krk-plays-tennis-api/reference/operations/getplayerrating/) | | See why a rating changed | [Explain a rating change](/krk-plays-tennis-api/guides/explain-a-rating-change/) | [List a player’s rating history](/krk-plays-tennis-api/reference/operations/listplayerratinghistory/) | | Find opponents for a player | [Find opponents for a player](/krk-plays-tennis-api/guides/find-opponents/) | [List match suggestions](/krk-plays-tennis-api/reference/operations/listmatchsuggestions/) | | Browse competitions, venues, and results | | [List competitions](/krk-plays-tennis-api/reference/operations/listcompetitions/), [List venues](/krk-plays-tennis-api/reference/operations/listvenues/), [List matches](/krk-plays-tennis-api/reference/operations/listmatches/) | | Submit a match result, as a competition organizer | [Submit a match result](/krk-plays-tennis-api/guides/submit-a-match-result/) | [Submit a match result](/krk-plays-tennis-api/reference/operations/creatematch/) | ## Which credential you need [Section titled “Which credential you need”](#which-credential-you-need) | To | You need | Guide | | -------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------- | | Read any data | An API key | [Authenticate with an API key](/krk-plays-tennis-api/authentication/api-key/) | | Submit match results | An OAuth 2.0 access token with the `matches:write` scope | [Get an access token](/krk-plays-tennis-api/authentication/access-token/) | ## Conventions [Section titled “Conventions”](#conventions) * **Base URL:** `https://api.krkplaystennis.example/v1` * **Format:** requests and responses are JSON. * **Pagination:** list operations return `data` and a `next_cursor`. Pass `next_cursor` as `cursor` to get the next page. * **Errors:** every error is an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details object, with a `type` URL for each kind of error. See [Errors](/krk-plays-tennis-api/errors/). * **Times:** timestamps follow RFC 3339. Availability windows use local time in Kraków (`Europe/Warsaw`). ## Next steps [Section titled “Next steps”](#next-steps) [Quickstart](/krk-plays-tennis-api/quickstart/)Start the mock and make your first request in under 5 minutes. [Authenticate with an API key](/krk-plays-tennis-api/authentication/api-key/)Send your API key with every read request. [Submit a match result](/krk-plays-tennis-api/guides/submit-a-match-result/)Send a result as a competition organizer and check that it's rated. [API reference](/krk-plays-tennis-api/reference/)Every operation, parameter, and schema, generated from the OpenAPI spec. # Make your first request > Start a local mock of the KRKplaysTennis API and read a player's rating in under 5 minutes. In this quickstart, you start a local mock of the KRKplaysTennis API, list players, and read one player’s rating. It takes less than 5 minutes. KRKplaysTennis is fictional, so there’s no live server. The mock is [Prism](https://github.com/stoplightio/prism): it reads the OpenAPI spec of the API and answers each request with the examples in it. ## Before you begin [Section titled “Before you begin”](#before-you-begin) * [Node.js](https://nodejs.org/en) 22 or later. You need it to run the mock, and for the JavaScript examples. * A macOS or Linux terminal. On Windows, use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install). * For the Python examples, Python 3.10 or later with the `requests` package (`pip install requests`). ## Steps [Section titled “Steps”](#steps) 1. Start the mock: ```sh npx @stoplight/prism-cli@5 mock https://saverio-cirillo.com/krk-plays-tennis-api/openapi.yaml ``` The first run downloads Prism, which takes a few seconds. When the mock is ready, the last line of the output is: ```text Prism is listening on http://127.0.0.1:4010 ``` Keep this terminal open. 2. In a new terminal, set the base URL and an API key: ```sh export KRK_API_URL=http://127.0.0.1:4010 export KRK_API_KEY=quickstart-key ``` The mock accepts any API key, as long as you send one. Every example in these docs reads these two variables, so the same code works with the mock and with a real key. To check the variables, run `echo $KRK_API_URL`. The output is `http://127.0.0.1:4010`. 3. List the players: * curl ```sh curl --silent "$KRK_API_URL/players" \ --header "X-API-Key: $KRK_API_KEY" ``` * JavaScript Save this code as `list-players.mjs`, then run `node list-players.mjs`: ```js const response = await fetch(`${process.env.KRK_API_URL}/players`, { headers: { 'X-API-Key': process.env.KRK_API_KEY }, }); if (!response.ok) throw new Error(`Request failed: ${response.status}`); console.log(JSON.stringify(await response.json(), null, 2)); ``` * Python Save this code as `list_players.py`, then run `python3 list_players.py`: ```python import json import os import requests response = requests.get( f"{os.environ['KRK_API_URL']}/players", headers={"X-API-Key": os.environ["KRK_API_KEY"]}, ) response.raise_for_status() print(json.dumps(response.json(), indent=2)) ``` The response lists three players: Kasia W., Marta Z., and Ola K. Each player has a `rating` with a `value` and a `confidence`. `next_cursor` is `null`, because there are no more pages. The start of the response looks like this: ```json { "data": [ { "id": "plr_9fz3tr6wn1", "display_name": "Kasia W.", "self_assessed_ntrp": 4, "rating": { "value": 1499, "confidence": 70 }, ``` 4. Get the rating of Ola K., whose player ID is `plr_2b7xq9mhk4`: * curl ```sh curl --silent "$KRK_API_URL/players/plr_2b7xq9mhk4/rating" \ --header "X-API-Key: $KRK_API_KEY" ``` * JavaScript Save this code as `get-rating.mjs`, then run `node get-rating.mjs`: ```js const playerId = 'plr_2b7xq9mhk4'; const response = await fetch(`${process.env.KRK_API_URL}/players/${playerId}/rating`, { headers: { 'X-API-Key': process.env.KRK_API_KEY }, }); if (!response.ok) throw new Error(`Request failed: ${response.status}`); console.log(JSON.stringify(await response.json(), null, 2)); ``` * Python Save this code as `get_rating.py`, then run `python3 get_rating.py`: ```python import json import os import requests player_id = "plr_2b7xq9mhk4" response = requests.get( f"{os.environ['KRK_API_URL']}/players/{player_id}/rating", headers={"X-API-Key": os.environ["KRK_API_KEY"]}, ) response.raise_for_status() print(json.dumps(response.json(), indent=2)) ``` The response shows Ola’s rating after one rated match: ```json { "player_id": "plr_2b7xq9mhk4", "value": 1432, "confidence": 30, "k_factor": 48, "rated_matches": 1, "last_rated_match_at": "2026-10-10T18:00:00+02:00", "updated_at": "2026-10-10T20:43:12Z" } ``` Ola started at 1400, from a self-assessed NTRP level of 3.5. Winning that first match raised the rating by 32 and the confidence from 20 to 30. You’ve made your first two requests to the KRKplaysTennis API. ## Next steps [Section titled “Next steps”](#next-steps) * [How ratings work](/krk-plays-tennis-api/concepts/how-ratings-work/): why Ola’s rating went from 1400 to 1432. * [Explain a rating change](/krk-plays-tennis-api/guides/explain-a-rating-change/): reproduce that change from the API data. * [Find opponents for a player](/krk-plays-tennis-api/guides/find-opponents/): suggest fair matches. * [Authenticate with an API key](/krk-plays-tennis-api/authentication/api-key/): how API keys work, and how to keep yours out of your code. * [API reference](/krk-plays-tennis-api/reference/): the parameters and responses of every operation. # How ratings work > How the KRKplaysTennis API turns a self-assessed level and real match results into a rating with a confidence value. A KRKplaysTennis rating is a number, usually between 1000 and 1800 for amateur players, that predicts how a player does against other rated players. The rating starts from the player’s own assessment, then each rated match moves it toward what the results show. A confidence value says how far you can trust the current number. The model is a variant of [Elo](https://en.wikipedia.org/wiki/Elo_rating_system), the system chess uses, with two changes: a confidence value that controls how fast the rating moves, and partial credit for matches that end in a retirement. ## The starting rating [Section titled “The starting rating”](#the-starting-rating) A new player picks a level on the [NTRP self-rating scale](https://www.usta.com/en/home/coach-organize/tennis-tool-center/run-usta-programs/national/understanding-ntrp-ratings.html), from 1.5 to 5.5. The API converts it to a starting rating: ```text starting rating = 1000 + (NTRP - 1.5) × 200 ``` | NTRP level | Starting rating | | ---------- | --------------- | | 1.5 | 1000 | | 2.0 | 1100 | | 2.5 | 1200 | | 3.0 | 1300 | | 3.5 | 1400 | | 4.0 | 1500 | | 4.5 | 1600 | | 5.0 | 1700 | | 5.5 | 1800 | Every new player starts with a confidence of 20, the lowest value. Self-assessments are often too optimistic or too modest, so the API treats the starting rating as a rough guess and lets the first results correct it quickly. The rating isn’t an NTRP rating. NTRP only sets the starting point, and it never changes after that. ## What happens after a match [Section titled “What happens after a match”](#what-happens-after-a-match) After each rated match, the API compares what happened with what the two ratings predicted. 1. **Expected score.** The rating difference gives each player’s chance of winning, from 0 to 1: ```text expected score = 1 / (1 + 10 ^ ((opponent rating - player rating) / 400)) ``` Equal ratings give 0.5. A player rated 120 points lower has an expected score of 0.334. 2. **Actual score.** A win is 1 and a loss is 0. 3. **Change.** The rating moves by the gap between the two, scaled by the K-factor: ```text change = K × (actual score - expected score) ``` The API rounds the expected and actual scores to 3 decimal places, and the change to the nearest whole number. A player who wins against the odds gains a lot. A favorite who wins as expected gains little. ## The K-factor and confidence [Section titled “The K-factor and confidence”](#the-k-factor-and-confidence) The K-factor is the most a rating can move in one match. It depends on the player’s confidence: | Confidence | K-factor | Meaning | | ---------- | -------- | ------------------------------------------- | | 0 to 39 | 48 | The rating is still a guess. It moves fast. | | 40 to 69 | 32 | The rating is taking shape. | | 70 to 95 | 16 | The rating is settled. It moves slowly. | Confidence changes like this: * Each rated match adds 10, up to a maximum of 95. Confidence never reaches 100, because a rating is always an estimate. * After 60 days without a rated match, confidence drops by 5 every 30 days, down to 20. The rating itself doesn’t change. A player who comes back after a break gets a higher K-factor, so the rating adapts quickly to their current form. Each player uses their own K-factor. A match between a new player and a settled one changes the two ratings by different amounts, so the system isn’t zero-sum. ## Worked example: a completed match [Section titled “Worked example: a completed match”](#worked-example-a-completed-match) Ola K. and Kasia W. play in the Kraków Autumn League. Ola has a self-assessed NTRP level of 3.5 and no rated matches yet. Kasia has played 4 rated matches. Ola wins 6-4 3-6 \[10-7]. | | Ola K. | Kasia W. | | ----------------- | -------------------------- | -------------------------- | | Rating before | 1400 | 1520 | | Confidence before | 20 | 60 | | K-factor | 48 | 32 | | Expected score | 0.334 | 0.666 | | Actual score | 1 (win) | 0 (loss) | | Change | 48 × (1 - 0.334) = **+32** | 32 × (0 - 0.666) = **-21** | | Rating after | **1432** | **1499** | | Confidence after | 30 | 70 | Ola was the underdog and won, so Ola gains more than Kasia loses. Ola’s K-factor is also higher, because one match says more about a new rating than about a settled one. These are the numbers you get from the API in the [quickstart](/krk-plays-tennis-api/quickstart/). ## Worked example: a retired match [Section titled “Worked example: a retired match”](#worked-example-a-retired-match) Tomek B. and Marta Z. are both new, with confidence 20. Marta retires injured at 6-2 2-1 for Tomek. Tomek won 8 of the 11 games played. For a retired match, the actual score isn’t 1 or 0: it’s each player’s share of the games played. This way, a result that stops early still counts, but only for what the players showed on court. | | Tomek B. | Marta Z. | | -------------- | ------------------------------ | ------------------------------ | | Rating before | 1300 | 1400 | | K-factor | 48 | 48 | | Expected score | 0.360 | 0.640 | | Actual score | 8 / 11 = 0.727 | 3 / 11 = 0.273 | | Change | 48 × (0.727 - 0.360) = **+18** | 48 × (0.273 - 0.640) = **-18** | | Rating after | **1318** | **1382** | In a match tiebreak format, a completed match tiebreak counts as one game for its winner. ## Matches that don’t change ratings [Section titled “Matches that don’t change ratings”](#matches-that-dont-change-ratings) * **Walkovers.** When a player doesn’t show up or withdraws before the match, nothing was played, so there’s nothing to rate. The match gets `rating_status: not_rated`, and neither rating nor confidence changes. * **Matches waiting to be rated.** The API rates matches a few minutes after they’re submitted. Until then, the match has `rating_status: pending`. ## See every change for yourself [Section titled “See every change for yourself”](#see-every-change-for-yourself) Every change to a rating is recorded in the player’s rating history, with the reason and, for matches, every number used in the calculation. To reproduce a change from the API data, follow [Explain a rating change](/krk-plays-tennis-api/guides/explain-a-rating-change/). # Use this API with AI agents > Give an AI agent the KRKplaysTennis docs as plain text, or connect it to the API through a read-only MCP server. AI agents can work with the KRKplaysTennis API in two ways: they can read the docs, or they can call the API through tools. This page covers both. ## Give an agent the docs [Section titled “Give an agent the docs”](#give-an-agent-the-docs) Every page in these docs is also published as plain text. Plain text leaves out the navigation and styling that agents don’t need. Point an agent at the file that fits the task: | File | What it contains | Use it when | | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | [`llms.txt`](/krk-plays-tennis-api/llms.txt) | A short description of the API and links to everything else, following the [llms.txt convention](https://llmstxt.org/). | The agent needs to find its way around. Start here. | | [`llms-full.txt`](/krk-plays-tennis-api/llms-full.txt) | Every page in one file. | The agent has a large context and needs everything. | | [`llms-small.txt`](/krk-plays-tennis-api/llms-small.txt) | Every page, with notes, tips, and extra whitespace removed. | The agent has a small context. | | A page URL ending in `.md` | One page as Markdown, such as [`quickstart.md`](/krk-plays-tennis-api/quickstart.md). Every page links to its Markdown version at the bottom. | The agent needs one task or concept. | | [`openapi.yaml`](/krk-plays-tennis-api/openapi.yaml) | The OpenAPI 3.1 spec: every operation, parameter, schema, and example. | The agent writes code that calls the API. The spec is more precise than any page. | `llms.txt` also links to smaller sets, such as only the guides, so an agent can load part of the docs. ## Connect an agent with the MCP server [Section titled “Connect an agent with the MCP server”](#connect-an-agent-with-the-mcp-server) The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) lets an AI app call external tools. The KRKplaysTennis MCP server turns the API into six tools that an agent can call while it answers a question: | Tool | What it does | | ----------------------- | -------------------------------------------------------------------------------------------------- | | `find_players` | Lists players with their rating, confidence, and availability. Use it to find a player ID by name. | | `get_player_rating` | Gets a player’s current rating, confidence, and K-factor. | | `explain_rating_change` | Explains why a rating changed after a match, with the calculation redone step by step. | | `find_opponents` | Suggests opponents with a similar rating and overlapping availability. | | `list_matches` | Lists match results, filtered by player, competition, status, or date. | | `list_competitions` | Lists leagues and tournaments, with their match format. | `explain_rating_change` does the math in code, so the agent quotes the result instead of computing it. A language model can get arithmetic wrong, and a rating explanation that doesn’t add up is worse than none. ### Why the server is read-only [Section titled “Why the server is read-only”](#why-the-server-is-read-only) The server has no tool to submit match results, on purpose. A submitted result changes two players’ ratings. If an agent misreads a score or invents one, that mistake becomes data that other people see. Submitting results stays with the organizer’s own integration, where a person checks each result. Every tool is marked with `readOnlyHint: true`, so MCP clients know the tools don’t change anything. ### Set up the server in Claude Code [Section titled “Set up the server in Claude Code”](#set-up-the-server-in-claude-code) These steps connect the server to [Claude Code](https://code.claude.com/docs/en/overview), using the local mock as the API. Other MCP clients use the same command and environment variables. 1. Start the mock, as in step 1 of the [quickstart](/krk-plays-tennis-api/quickstart/), and keep it running. The last line of the output is `Prism is listening on http://127.0.0.1:4010`. 2. In a new terminal, clone the repository and build the server: ```sh git clone https://github.com/saveriocirillo/krk-plays-tennis-docs.git cd krk-plays-tennis-docs/mcp-server npm install npm run build ``` The build creates `dist/index.js`, the server. 3. Add the server to Claude Code. The server reads the API URL and key from environment variables: ```sh claude mcp add krk-plays-tennis \ -e KRK_API_URL=http://127.0.0.1:4010 \ -e KRK_API_KEY=any-value \ -- node "$PWD/dist/index.js" ``` To check it, run `claude mcp list`. The output lists `krk-plays-tennis` as connected. 4. Start Claude Code and ask a question about a player: ```text Why did Ola K.'s rating change after the last match? ``` Claude typically calls `find_players` to get Ola’s player ID, then `explain_rating_change`. The answer explains that Ola won against a higher-rated opponent, with the calculation 48 × (1 - 0.334) = 32. To use the server in a client that reads a JSON configuration file, such as Claude Desktop, add an entry like this, with the absolute path to `dist/index.js`: ```json { "mcpServers": { "krk-plays-tennis": { "command": "node", "args": ["/absolute/path/to/krk-plays-tennis-docs/mcp-server/dist/index.js"], "env": { "KRK_API_URL": "http://127.0.0.1:4010", "KRK_API_KEY": "any-value" } } } } ``` ## What to expect from the mock [Section titled “What to expect from the mock”](#what-to-expect-from-the-mock) The mock answers every request with the examples from the OpenAPI spec, so the agent always sees the same three players, three matches, and two competitions. It’s enough to see how an agent uses the tools, but filters don’t change the results. See [Errors on the mock](/krk-plays-tennis-api/errors/#errors-on-the-mock). # Get an access token > Exchange your client credentials for an OAuth 2.0 access token, then use it to submit match results. To submit match results with `POST /matches`, you need an OAuth 2.0 access token with the `matches:write` scope. You get the token from the authorization server with the client credentials grant, then send it in the `Authorization` header. Only competition organizers get client credentials, and a token can only submit results for competitions its organizer runs. To read data, use an [API key](/krk-plays-tennis-api/authentication/api-key/) instead. ## Before you begin [Section titled “Before you begin”](#before-you-begin) * A client ID and a client secret for your organizer account. With the [local mock](/krk-plays-tennis-api/quickstart/), any non-empty values work. * `KRK_API_URL` set to the API base URL. For the mock, that’s `http://127.0.0.1:4010`. ## Steps [Section titled “Steps”](#steps) 1. Store the authorization server URL and your client credentials in environment variables: ```sh export KRK_AUTH_URL=http://127.0.0.1:4010 export KRK_CLIENT_ID=your-client-id export KRK_CLIENT_SECRET=your-client-secret ``` For the mock, the authorization server runs at the same address as the API. In production, `KRK_AUTH_URL` is `https://auth.krkplaystennis.example`. To check the variables, run `echo $KRK_AUTH_URL`. The output is the URL you set. 2. Request a token. Send your client ID and client secret with HTTP Basic authentication, and the grant type and scope as form fields: * curl ```sh curl --silent "$KRK_AUTH_URL/oauth/token" \ --user "$KRK_CLIENT_ID:$KRK_CLIENT_SECRET" \ --data grant_type=client_credentials \ --data scope=matches:write ``` * JavaScript Save this code as `get-token.mjs`, then run `node get-token.mjs`: ```js const credentials = Buffer.from( `${process.env.KRK_CLIENT_ID}:${process.env.KRK_CLIENT_SECRET}`, ).toString('base64'); const response = await fetch(`${process.env.KRK_AUTH_URL}/oauth/token`, { method: 'POST', headers: { Authorization: `Basic ${credentials}` }, body: new URLSearchParams({ grant_type: 'client_credentials', scope: 'matches:write' }), }); if (!response.ok) throw new Error(`Request failed: ${response.status}`); console.log(await response.json()); ``` * Python Save this code as `get_token.py`, then run `python3 get_token.py`: ```python import os import requests response = requests.post( f"{os.environ['KRK_AUTH_URL']}/oauth/token", auth=(os.environ["KRK_CLIENT_ID"], os.environ["KRK_CLIENT_SECRET"]), data={"grant_type": "client_credentials", "scope": "matches:write"}, ) response.raise_for_status() print(response.json()) ``` The response has status `200` and contains the token: ```json { "access_token": "eyJhbGciOiJSUzI1NiJ9.fictional-token-for-docs.c2lnbmF0dXJl", "token_type": "Bearer", "expires_in": 3600, "scope": "matches:write" } ``` 3. Copy the `access_token` value from the response in step 2, and store it in an environment variable: ```sh export KRK_ACCESS_TOKEN='' ``` To check it, run `echo $KRK_ACCESS_TOKEN`. The output is the token. 4. Send the token as `Authorization: Bearer ` to submit a match result. Each new result also needs a unique `Idempotency-Key`: * curl ```sh curl --silent "$KRK_API_URL/matches" \ --header "Authorization: Bearer $KRK_ACCESS_TOKEN" \ --header "Idempotency-Key: $(uuidgen)" \ --header "Content-Type: application/json" \ --data '{ "competition_id": "cmp_6tw2hn8rxe", "venue_id": "ven_7hk2p9wq4m", "played_at": "2026-10-10T18:00:00+02:00", "player_ids": ["plr_2b7xq9mhk4", "plr_9fz3tr6wn1"], "status": "completed", "score": { "sets": [{ "games": [6, 4] }, { "games": [3, 6] }], "match_tiebreak": [10, 7] } }' ``` * JavaScript Save this code as `submit-match.mjs`, then run `node submit-match.mjs`: ```js const response = await fetch(`${process.env.KRK_API_URL}/matches`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.KRK_ACCESS_TOKEN}`, 'Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json', }, body: JSON.stringify({ competition_id: 'cmp_6tw2hn8rxe', venue_id: 'ven_7hk2p9wq4m', played_at: '2026-10-10T18:00:00+02:00', player_ids: ['plr_2b7xq9mhk4', 'plr_9fz3tr6wn1'], status: 'completed', score: { sets: [{ games: [6, 4] }, { games: [3, 6] }], match_tiebreak: [10, 7], }, }), }); if (!response.ok) throw new Error(`Request failed: ${response.status}`); console.log(await response.json()); ``` * Python Save this code as `submit_match.py`, then run `python3 submit_match.py`: ```python import os import uuid import requests response = requests.post( f"{os.environ['KRK_API_URL']}/matches", headers={ "Authorization": f"Bearer {os.environ['KRK_ACCESS_TOKEN']}", "Idempotency-Key": str(uuid.uuid4()), }, json={ "competition_id": "cmp_6tw2hn8rxe", "venue_id": "ven_7hk2p9wq4m", "played_at": "2026-10-10T18:00:00+02:00", "player_ids": ["plr_2b7xq9mhk4", "plr_9fz3tr6wn1"], "status": "completed", "score": { "sets": [{"games": [6, 4]}, {"games": [3, 6]}], "match_tiebreak": [10, 7], }, }, ) response.raise_for_status() print(response.json()) ``` The response has status `201` and the new match, with `"rating_status": "pending"`. The API rates the match a few minutes later. ## Renew the token [Section titled “Renew the token”](#renew-the-token) A token expires `expires_in` seconds after you get it, which is 1 hour. There’s no refresh token. When a token expires, the API returns status `401`, and you request a new token with the same client credentials. To avoid failed requests, keep the token and its expiry time in memory, and request a new token a minute before it expires. ## If the request fails [Section titled “If the request fails”](#if-the-request-fails) | Status | `error` or `type` | Cause | Fix | | ------------------------- | --------------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------- | | `401` from `/oauth/token` | `invalid_client` | The client ID or client secret is wrong. | Check both values. Don’t send the client secret as a form field: use HTTP Basic. | | `400` from `/oauth/token` | `invalid_scope` | The scope doesn’t exist. | Request `matches:write`, the only scope. | | `401` from `/matches` | `unauthenticated` | The token is missing or expired. | Request a new token. | | `403` from `/matches` | `insufficient-scope` | The token doesn’t have the `matches:write` scope. | Request a token with `scope=matches:write`. | | `403` from `/matches` | `not-competition-organizer` | Another organizer runs the competition. | Submit results only for your own competitions. | The token endpoint follows the OAuth 2.0 standard ([RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2)), so its errors have an `error` field. The rest of the API uses problem details, with a `type` field. # Authenticate with an API key > Send an API key in the X-API-Key header to read data from the KRKplaysTennis API. Every read operation in the KRKplaysTennis API needs an API key. You send the key in the `X-API-Key` header of each request. An API key can only read data. To submit match results, you need an access token instead. See [Get an access token](/krk-plays-tennis-api/authentication/access-token/). ## Before you begin [Section titled “Before you begin”](#before-you-begin) * An API key. With the [local mock](/krk-plays-tennis-api/quickstart/), any non-empty value works. * `KRK_API_URL` set to the API base URL. For the mock, that’s `http://127.0.0.1:4010`. ## Steps [Section titled “Steps”](#steps) 1. Store the key in an environment variable, so it stays out of your code: ```sh export KRK_API_KEY=your-api-key ``` To check it, run `echo $KRK_API_KEY`. The output is your key. 2. Send the key in the `X-API-Key` header. This request gets a player’s profile: * curl ```sh curl --silent "$KRK_API_URL/players/plr_2b7xq9mhk4" \ --header "X-API-Key: $KRK_API_KEY" ``` * JavaScript Save this code as `get-player.mjs`, then run `node get-player.mjs`: ```js const response = await fetch(`${process.env.KRK_API_URL}/players/plr_2b7xq9mhk4`, { headers: { 'X-API-Key': process.env.KRK_API_KEY }, }); if (!response.ok) throw new Error(`Request failed: ${response.status}`); console.log(await response.json()); ``` * Python Save this code as `get_player.py`, then run `python3 get_player.py`: ```python import os import requests response = requests.get( f"{os.environ['KRK_API_URL']}/players/plr_2b7xq9mhk4", headers={"X-API-Key": os.environ["KRK_API_KEY"]}, ) response.raise_for_status() print(response.json()) ``` The response has status `200` and the profile of Ola K., with `"display_name": "Ola K."` and a `rating` object. ## If the key is missing or wrong [Section titled “If the key is missing or wrong”](#if-the-key-is-missing-or-wrong) When a request has no `X-API-Key` header, or the key isn’t valid, the API returns status `401` with this body: ```json { "type": "https://saverio-cirillo.com/krk-plays-tennis-api/errors/#unauthenticated", "title": "Authentication required", "status": 401, "detail": "Send a valid API key in the X-API-Key header.", "instance": "/v1/players/plr_2b7xq9mhk4" } ``` Check that the header name is exactly `X-API-Key` and that the environment variable is set in the terminal where you run the request. ## Keep your key secret [Section titled “Keep your key secret”](#keep-your-key-secret) * Keep the key on a server. Don’t put it in a browser app or a mobile app, where anyone can read it. * Don’t commit the key to a repository. Load it from an environment variable or a secrets manager. * If a key leaks, replace it and stop using the old one. # Errors > Every error the KRKplaysTennis API returns, what causes it, and how to fix it. When a request fails, the API returns a `4xx` status and a body in the [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details format, with the `application/problem+json` media type: ```json { "type": "https://saverio-cirillo.com/krk-plays-tennis-api/errors/#invalid-score", "title": "Invalid score", "status": 422, "detail": "The score isn't valid for the competition's match format.", "instance": "/v1/matches", "errors": [ { "pointer": "/score/sets/0/games", "detail": "In this competition, a set ends at 6 games with a 2-game lead, at 7-5, or at 7-6 with a tiebreak. 6-5 isn't a final set score." } ] } ``` | Field | Description | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `type` | A URL that identifies the kind of error. It links to the matching section of this page. | | `title` | A short summary. It’s the same for every error of the same `type`. | | `status` | The HTTP status code. | | `detail` | What went wrong in this request. | | `instance` | The request path. | | `errors` | For `invalid-request`, `invalid-score`, and `validation-failed` only: one item per invalid field. `pointer` is a JSON Pointer to the field in the request body, or `#/` followed by the name of a query parameter. | In your code, branch on `type`, not on `title` or `detail`. The wording of `title` and `detail` can change, and `type` doesn’t. The token endpoint, `POST /oauth/token`, is the one exception: it follows OAuth 2.0 and uses a different format. See [Token endpoint errors](#token-endpoint-errors). ## Error types [Section titled “Error types”](#error-types) ### `invalid-request` [Section titled “invalid-request”](#invalid-request) **Status:** `400` · **Title:** `Invalid request` The request is malformed: a query parameter has the wrong type or is out of range, or the body isn’t valid JSON. The `errors` array lists each invalid parameter. **Fix:** check each item in `errors`. For example, `limit` must be between 1 and 100. ### `unauthenticated` [Section titled “unauthenticated”](#unauthenticated) **Status:** `401` · **Title:** `Authentication required` The request has no credentials, or they aren’t valid: * For a read, the `X-API-Key` header is missing or the key isn’t valid. * For `POST /matches`, the access token is missing or expired. **Fix:** see [Authenticate with an API key](/krk-plays-tennis-api/authentication/api-key/), or [get a new access token](/krk-plays-tennis-api/authentication/access-token/). Tokens expire after 1 hour. ### `insufficient-scope` [Section titled “insufficient-scope”](#insufficient-scope) **Status:** `403` · **Title:** `Insufficient scope` The access token is valid but doesn’t have the `matches:write` scope. **Fix:** request a new token with `scope=matches:write`. ### `not-competition-organizer` [Section titled “not-competition-organizer”](#not-competition-organizer) **Status:** `403` · **Title:** `Not the competition organizer` You tried to submit a result for a competition that another organizer runs. A token can only submit results for its own organizer’s competitions. **Fix:** check `competition_id`. To see who runs a competition, get it and read `organizer`. ### `not-found` [Section titled “not-found”](#not-found) **Status:** `404` · **Title:** `Not found` No resource exists with the ID in the path. **Fix:** check the ID and its prefix: `plr_` for players, `mat_` for matches, `cmp_` for competitions, and `ven_` for venues. ### `duplicate-match` [Section titled “duplicate-match”](#duplicate-match) **Status:** `409` · **Title:** `Duplicate match` The API already has a match with the same two players, in the same competition, at the same `played_at` time. `detail` includes the ID of the existing match. **Fix:** if you’re submitting the same result again, get the existing match instead. If it’s a different match, check `played_at`. ### `idempotency-conflict` [Section titled “idempotency-conflict”](#idempotency-conflict) **Status:** `409` · **Title:** `Idempotency key conflict` You sent an `Idempotency-Key` that was already used in the last 24 hours, with a different request body. **Fix:** use a new key for each new result. Reuse a key only to retry the exact same request. ### `invalid-score` [Section titled “invalid-score”](#invalid-score) **Status:** `422` · **Title:** `Invalid score` The score doesn’t fit the competition’s `match_format`. For example, a set ends 6-5, or there’s a match tiebreak in a competition that plays a full third set. The `errors` array points to each invalid set. **Fix:** get the competition and compare each set with its `match_format`. See [Submit a match result](/krk-plays-tennis-api/guides/submit-a-match-result/). ### `validation-failed` [Section titled “validation-failed”](#validation-failed) **Status:** `422` · **Title:** `Validation failed` The body is valid JSON, but a field breaks a rule. For example, the same player appears twice in `player_ids`, or `retired_player_id` isn’t one of the two players. The `errors` array points to each invalid field. **Fix:** check each item in `errors`. ### `rate-limited` [Section titled “rate-limited”](#rate-limited) **Status:** `429` · **Title:** `Rate limit reached` You sent more than 60 requests in a minute with the same API key or token. **Fix:** wait for the number of seconds in the `Retry-After` header, then retry. ## Token endpoint errors [Section titled “Token endpoint errors”](#token-endpoint-errors) `POST /oauth/token` follows the OAuth 2.0 standard ([RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2)). Its errors are JSON objects with an `error` code and an `error_description`: ```json { "error": "invalid_client", "error_description": "The client ID or client secret is invalid." } ``` * **`invalid_client`** (status `401`): the client ID or client secret is wrong. Check both values, and send them with HTTP Basic authentication. * **`invalid_scope`** (status `400`): the requested scope doesn’t exist. Request `matches:write`, the only scope. * **`unsupported_grant_type`** (status `400`): `grant_type` isn’t `client_credentials`. Send `grant_type=client_credentials`. * **`invalid_request`** (status `400`): a required form field is missing. Send `grant_type` as a form field. ## Errors on the mock [Section titled “Errors on the mock”](#errors-on-the-mock) The [local mock](/krk-plays-tennis-api/quickstart/) checks that each request matches the OpenAPI spec, but it doesn’t run the business rules of the API. When a request is invalid, the mock returns the operation’s first error example from the spec, whatever the actual problem is: `422` with `validation-failed` for `POST /matches`, and `400` with `invalid-request` for read operations. It never returns errors that depend on data, such as `invalid-score` or `duplicate-match`. To see the example response for a specific status, add a `Prefer` header to the request, such as `Prefer: code=404`. # Explain a rating change > Read a player's rating history, find the change caused by a match, and reproduce the calculation from the API data. When a player wants to know why their rating went up by 32, the rating history has the answer. Each entry records what changed, why, and, for a match, every number used in the calculation. You can show players the explanation, or reproduce the change yourself to check it. This guide explains the change to the rating of Ola K. (`plr_2b7xq9mhk4`) after a win against Kasia W. To learn the model behind the numbers, see [How ratings work](/krk-plays-tennis-api/concepts/how-ratings-work/). ## Before you begin [Section titled “Before you begin”](#before-you-begin) * An API key in `KRK_API_KEY`. See [Authenticate with an API key](/krk-plays-tennis-api/authentication/api-key/). * `KRK_API_URL` set to the API base URL. For the [local mock](/krk-plays-tennis-api/quickstart/), that’s `http://127.0.0.1:4010`. ## Steps [Section titled “Steps”](#steps) 1. List the player’s rating history: * curl ```sh curl --silent "$KRK_API_URL/players/plr_2b7xq9mhk4/rating-history" \ --header "X-API-Key: $KRK_API_KEY" ``` * JavaScript Save this code as `rating-history.mjs`, then run `node rating-history.mjs`: ```js const response = await fetch( `${process.env.KRK_API_URL}/players/plr_2b7xq9mhk4/rating-history`, { headers: { 'X-API-Key': process.env.KRK_API_KEY } }, ); if (!response.ok) throw new Error(`Request failed: ${response.status}`); const { data } = await response.json(); for (const entry of data) { console.log(entry.occurred_at, entry.reason.code, entry.change); } ``` * Python Save this code as `rating_history.py`, then run `python3 rating_history.py`: ```python import os import requests response = requests.get( f"{os.environ['KRK_API_URL']}/players/plr_2b7xq9mhk4/rating-history", headers={"X-API-Key": os.environ["KRK_API_KEY"]}, ) response.raise_for_status() for entry in response.json()["data"]: print(entry["occurred_at"], entry["reason"]["code"], entry["change"]) ``` The history has two entries, newest first: * A `match_result` entry with a `change` of `32`. * An `initial_assessment` entry with a `change` of `null`. It records the starting rating of 1400, from a self-assessed NTRP level of 3.5. 2. Find the entry for the match. Its `reason.match_id` is the match, and its `calculation` object has the inputs: ```json { "reason": { "code": "match_result", "match_id": "mat_4nc8xq2vbt", "explanation": "Won against Kasia W. (1520). Expected score 0.334, actual score 1. K-factor 48 because confidence was 20." }, "value_before": 1400, "value_after": 1432, "change": 32, "confidence_before": 20, "confidence_after": 30, "calculation": { "opponent_id": "plr_9fz3tr6wn1", "opponent_rating": 1520, "expected_score": 0.334, "actual_score": 1, "k_factor": 48 } } ``` `calculation` is `null` for entries that don’t come from a match, such as `initial_assessment` and `inactivity`. 3. Reproduce the change from the entry. Compute the expected score from the two ratings, then the change from the K-factor. This step needs code, so there’s no curl example: * JavaScript Save this code as `explain-change.mjs`, then run `node explain-change.mjs`: ```js const response = await fetch( `${process.env.KRK_API_URL}/players/plr_2b7xq9mhk4/rating-history`, { headers: { 'X-API-Key': process.env.KRK_API_KEY } }, ); if (!response.ok) throw new Error(`Request failed: ${response.status}`); const { data } = await response.json(); const entry = data.find((e) => e.reason.match_id === 'mat_4nc8xq2vbt'); const { opponent_rating, actual_score, k_factor } = entry.calculation; const round3 = (x) => Math.round(x * 1000) / 1000; const expected = round3(1 / (1 + 10 ** ((opponent_rating - entry.value_before) / 400))); const change = Math.round(k_factor * (actual_score - expected)); console.log(`expected score: ${expected}`); console.log(`change: ${change}, from the API: ${entry.change}`); ``` * Python Save this code as `explain_change.py`, then run `python3 explain_change.py`: ```python import os import requests response = requests.get( f"{os.environ['KRK_API_URL']}/players/plr_2b7xq9mhk4/rating-history", headers={"X-API-Key": os.environ["KRK_API_KEY"]}, ) response.raise_for_status() entry = next( e for e in response.json()["data"] if e["reason"]["match_id"] == "mat_4nc8xq2vbt" ) calc = entry["calculation"] expected = round(1 / (1 + 10 ** ((calc["opponent_rating"] - entry["value_before"]) / 400)), 3) change = round(calc["k_factor"] * (calc["actual_score"] - expected)) print(f"expected score: {expected}") print(f"change: {change}, from the API: {entry['change']}") ``` The output shows that your calculation matches the API: ```text expected score: 0.334 change: 32, from the API: 32 ``` Ola was rated 120 points lower than Kasia, so Ola’s chance of winning was 0.334. Ola won, so the actual score is 1, and the change is 48 × (1 - 0.334) = 31.968, which rounds to 32. The K-factor is 48 because Ola’s confidence was 20, under 40. 4. Show the player the explanation. Each entry has a `reason.explanation` sentence, written for players: ```text Won against Kasia W. (1520). Expected score 0.334, actual score 1. K-factor 48 because confidence was 20. ``` Display it next to the change, so the player sees both the number and the reason. For the full model, link players to [How ratings work](/krk-plays-tennis-api/concepts/how-ratings-work/). ## Other reasons for a change [Section titled “Other reasons for a change”](#other-reasons-for-a-change) | `reason.code` | What changed | `calculation` | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | | `initial_assessment` | The starting rating, from the self-assessed NTRP level. `value_before`, `change`, and `confidence_before` are `null`. | `null` | | `match_result` | The rating and confidence, after a rated match. | The inputs to the calculation | | `inactivity` | Only confidence, which drops by 5 after a period without rated matches. The rating value stays the same, so `change` is `0`. | `null` | # Find opponents for a player > Get suggested opponents with a similar rating and overlapping availability, and narrow them to a venue. Match suggestions help a player find a fair game: opponents with a similar rating who are free at the same time. The API ranks them by how close their rating is, and every suggestion shares at least one weekly time window with the player. This guide finds opponents for Ola K. (`plr_2b7xq9mhk4`), rated 1432, who is free on Tuesday evenings and Saturday mornings. ## Before you begin [Section titled “Before you begin”](#before-you-begin) * An API key in `KRK_API_KEY`. See [Authenticate with an API key](/krk-plays-tennis-api/authentication/api-key/). * `KRK_API_URL` set to the API base URL. For the [local mock](/krk-plays-tennis-api/quickstart/), that’s `http://127.0.0.1:4010`. ## Steps [Section titled “Steps”](#steps) 1. Get the suggestions for the player. `max_rating_gap` sets how far an opponent’s rating can be from the player’s, in either direction: * curl ```sh curl --silent "$KRK_API_URL/players/plr_2b7xq9mhk4/match-suggestions?max_rating_gap=150&limit=5" \ --header "X-API-Key: $KRK_API_KEY" ``` * JavaScript Save this code as `suggestions.mjs`, then run `node suggestions.mjs`: ```js const params = new URLSearchParams({ max_rating_gap: '150', limit: '5' }); const response = await fetch( `${process.env.KRK_API_URL}/players/plr_2b7xq9mhk4/match-suggestions?${params}`, { headers: { 'X-API-Key': process.env.KRK_API_KEY } }, ); if (!response.ok) throw new Error(`Request failed: ${response.status}`); const { data } = await response.json(); for (const suggestion of data) { const windows = suggestion.overlapping_availability .map((w) => `${w.day} ${w.start}-${w.end}`) .join(', '); console.log(`${suggestion.player.display_name}: gap ${suggestion.rating_gap}, ${windows}`); } ``` * Python Save this code as `suggestions.py`, then run `python3 suggestions.py`: ```python import os import requests response = requests.get( f"{os.environ['KRK_API_URL']}/players/plr_2b7xq9mhk4/match-suggestions", params={"max_rating_gap": 150, "limit": 5}, headers={"X-API-Key": os.environ["KRK_API_KEY"]}, ) response.raise_for_status() for suggestion in response.json()["data"]: windows = ", ".join( f"{w['day']} {w['start']}-{w['end']}" for w in suggestion["overlapping_availability"] ) print(f"{suggestion['player']['display_name']}: gap {suggestion['rating_gap']}, {windows}") ``` The response suggests two opponents, closest rating first. The JavaScript and Python examples print: ```text Marta Z.: gap 50, tuesday 19:00-21:00 Kasia W.: gap 67, saturday 10:00-13:00 ``` Tomek B. isn’t suggested. The rating gap is within 150, but Tomek is only free on Thursdays, which don’t overlap with Ola’s windows. 2. Read each suggestion to plan the match: * `rating_gap`: the difference between the two ratings. A smaller gap means a closer match. * `overlapping_availability`: the weekly time windows when both players are free, in Kraków local time (`Europe/Warsaw`). Propose a match inside one of them. * `shared_venue_ids`: venues both players prefer. It can be empty. Both suggestions here share Riverside Courts Podgórze (`ven_7hk2p9wq4m`). You have an opponent, a day and time, and a venue to propose. 3. To suggest only opponents who play at a specific venue, add `venue_id`: * curl ```sh curl --silent "$KRK_API_URL/players/plr_2b7xq9mhk4/match-suggestions?venue_id=ven_7hk2p9wq4m" \ --header "X-API-Key: $KRK_API_KEY" ``` * JavaScript Save this code as `suggestions-at-venue.mjs`, then run `node suggestions-at-venue.mjs`: ```js const params = new URLSearchParams({ venue_id: 'ven_7hk2p9wq4m' }); const response = await fetch( `${process.env.KRK_API_URL}/players/plr_2b7xq9mhk4/match-suggestions?${params}`, { headers: { 'X-API-Key': process.env.KRK_API_KEY } }, ); if (!response.ok) throw new Error(`Request failed: ${response.status}`); const { data } = await response.json(); console.log(data.map((s) => s.player.display_name)); ``` * Python Save this code as `suggestions_at_venue.py`, then run `python3 suggestions_at_venue.py`: ```python import os import requests response = requests.get( f"{os.environ['KRK_API_URL']}/players/plr_2b7xq9mhk4/match-suggestions", params={"venue_id": "ven_7hk2p9wq4m"}, headers={"X-API-Key": os.environ["KRK_API_KEY"]}, ) response.raise_for_status() print([s["player"]["display_name"] for s in response.json()["data"]]) ``` The response lists only opponents who prefer Riverside Courts Podgórze. Both Marta Z. and Kasia W. do, so the result is the same as in step 1. ## If there are no suggestions [Section titled “If there are no suggestions”](#if-there-are-no-suggestions) An empty `data` array means no player fits both the rating gap and the availability. Try one of these: * Raise `max_rating_gap`, up to 500. The default is 150. * Remove `venue_id`, if you set it. * Ask the player to add more availability windows to their profile. # Submit a match result > Submit the result of a singles match in a competition you organize, then check that the API has rated it. As a competition organizer, you submit each match result with `POST /matches`. The API records the result immediately and updates both players’ ratings a few minutes later. This guide submits the result of a match in the Kraków Autumn League: Ola K. beat Kasia W. 6-4 3-6 \[10-7]. ## Before you begin [Section titled “Before you begin”](#before-you-begin) * An access token with the `matches:write` scope in `KRK_ACCESS_TOKEN`. See [Get an access token](/krk-plays-tennis-api/authentication/access-token/). * An API key in `KRK_API_KEY`, so you can read the competition and the match. See [Authenticate with an API key](/krk-plays-tennis-api/authentication/api-key/). * `KRK_API_URL` set to the API base URL. For the [local mock](/krk-plays-tennis-api/quickstart/), that’s `http://127.0.0.1:4010`. ## Steps [Section titled “Steps”](#steps) 1. Get the competition, to check its match format: * curl ```sh curl --silent "$KRK_API_URL/competitions/cmp_6tw2hn8rxe" \ --header "X-API-Key: $KRK_API_KEY" ``` * JavaScript Save this code as `get-competition.mjs`, then run `node get-competition.mjs`: ```js const response = await fetch(`${process.env.KRK_API_URL}/competitions/cmp_6tw2hn8rxe`, { headers: { 'X-API-Key': process.env.KRK_API_KEY }, }); if (!response.ok) throw new Error(`Request failed: ${response.status}`); const competition = await response.json(); console.log(competition.match_format); ``` * Python Save this code as `get_competition.py`, then run `python3 get_competition.py`: ```python import os import requests response = requests.get( f"{os.environ['KRK_API_URL']}/competitions/cmp_6tw2hn8rxe", headers={"X-API-Key": os.environ["KRK_API_KEY"]}, ) response.raise_for_status() print(response.json()["match_format"]) ``` The response includes the competition’s `match_format`: ```json "match_format": { "games_per_set": 6, "no_ad": false, "final_set": "match_tiebreak" } ``` In this league, a set ends at 6 games with a 2-game lead, at 7-5, or at 7-6 with a tiebreak. At one set all, a match tiebreak to 10 points replaces the third set. The API rejects a score that doesn’t fit these rules. 2. Build the request body. List the two players in `player_ids`, then write every score pair in the same order: ```json { "competition_id": "cmp_6tw2hn8rxe", "venue_id": "ven_7hk2p9wq4m", "court_number": 2, "played_at": "2026-10-10T18:00:00+02:00", "player_ids": ["plr_2b7xq9mhk4", "plr_9fz3tr6wn1"], "status": "completed", "score": { "sets": [{ "games": [6, 4] }, { "games": [3, 6] }], "match_tiebreak": [10, 7] } } ``` Ola (`plr_2b7xq9mhk4`) is first, so `[6, 4]` means Ola won the first set 6-4. You don’t send the winner: the API works it out from the score. For a match that ended in a retirement or a walkover, see [Retirements and walkovers](#retirements-and-walkovers). 3. Submit the result with a new `Idempotency-Key`: * curl ```sh curl --silent "$KRK_API_URL/matches" \ --header "Authorization: Bearer $KRK_ACCESS_TOKEN" \ --header "Idempotency-Key: $(uuidgen)" \ --header "Content-Type: application/json" \ --data '{ "competition_id": "cmp_6tw2hn8rxe", "venue_id": "ven_7hk2p9wq4m", "court_number": 2, "played_at": "2026-10-10T18:00:00+02:00", "player_ids": ["plr_2b7xq9mhk4", "plr_9fz3tr6wn1"], "status": "completed", "score": { "sets": [{ "games": [6, 4] }, { "games": [3, 6] }], "match_tiebreak": [10, 7] } }' ``` * JavaScript Save this code as `submit-result.mjs`, then run `node submit-result.mjs`: ```js const response = await fetch(`${process.env.KRK_API_URL}/matches`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.KRK_ACCESS_TOKEN}`, 'Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json', }, body: JSON.stringify({ competition_id: 'cmp_6tw2hn8rxe', venue_id: 'ven_7hk2p9wq4m', court_number: 2, played_at: '2026-10-10T18:00:00+02:00', player_ids: ['plr_2b7xq9mhk4', 'plr_9fz3tr6wn1'], status: 'completed', score: { sets: [{ games: [6, 4] }, { games: [3, 6] }], match_tiebreak: [10, 7], }, }), }); if (!response.ok) throw new Error(`Request failed: ${response.status}`); const match = await response.json(); console.log(match.id, match.rating_status); ``` * Python Save this code as `submit_result.py`, then run `python3 submit_result.py`: ```python import os import uuid import requests response = requests.post( f"{os.environ['KRK_API_URL']}/matches", headers={ "Authorization": f"Bearer {os.environ['KRK_ACCESS_TOKEN']}", "Idempotency-Key": str(uuid.uuid4()), }, json={ "competition_id": "cmp_6tw2hn8rxe", "venue_id": "ven_7hk2p9wq4m", "court_number": 2, "played_at": "2026-10-10T18:00:00+02:00", "player_ids": ["plr_2b7xq9mhk4", "plr_9fz3tr6wn1"], "status": "completed", "score": { "sets": [{"games": [6, 4]}, {"games": [3, 6]}], "match_tiebreak": [10, 7], }, }, ) response.raise_for_status() match = response.json() print(match["id"], match["rating_status"]) ``` The response has status `201`. The new match has the ID `mat_4nc8xq2vbt`, `"winner_id": "plr_2b7xq9mhk4"`, and `"rating_status": "pending"`. If the request fails with a network error, retry it with the **same** `Idempotency-Key`. The API returns the original response instead of recording the match twice. In a real integration, generate the key once per result and store it with the result. 4. A few minutes later, get the match to check that it’s rated: * curl ```sh curl --silent "$KRK_API_URL/matches/mat_4nc8xq2vbt" \ --header "X-API-Key: $KRK_API_KEY" ``` * JavaScript Save this code as `get-match.mjs`, then run `node get-match.mjs`: ```js const response = await fetch(`${process.env.KRK_API_URL}/matches/mat_4nc8xq2vbt`, { headers: { 'X-API-Key': process.env.KRK_API_KEY }, }); if (!response.ok) throw new Error(`Request failed: ${response.status}`); const match = await response.json(); console.log(match.rating_status); ``` * Python Save this code as `get_match.py`, then run `python3 get_match.py`: ```python import os import requests response = requests.get( f"{os.environ['KRK_API_URL']}/matches/mat_4nc8xq2vbt", headers={"X-API-Key": os.environ["KRK_API_KEY"]}, ) response.raise_for_status() print(response.json()["rating_status"]) ``` The match has `"rating_status": "rated"`. Both players’ ratings are updated: Ola’s rating went from 1400 to 1432. To see why, follow [Explain a rating change](/krk-plays-tennis-api/guides/explain-a-rating-change/). ## Retirements and walkovers [Section titled “Retirements and walkovers”](#retirements-and-walkovers) The fields you send depend on how the match ended: | `status` | When to use it | Also send | Rated? | | ----------- | ----------------------------------------------------- | ----------------------------------------------------- | ------------------------ | | `completed` | The match was played to the end. | `score` | Yes | | `retired` | A player stopped during the match. | `score` up to the retirement, and `retired_player_id` | Yes, on the games played | | `walkover` | A player didn’t show up or withdrew before the match. | `winner_id`, and no `score` | No | For a retired match, the last set can be unfinished. In this example, Marta Z. retired at 6-2 2-1 for Tomek B.: ```json { "competition_id": "cmp_6tw2hn8rxe", "venue_id": "ven_3xr8tn5cva", "court_number": 1, "played_at": "2026-10-08T19:00:00+02:00", "player_ids": ["plr_5jd8vk2lc7", "plr_8mq4ws1yd6"], "status": "retired", "retired_player_id": "plr_8mq4ws1yd6", "score": { "sets": [{ "games": [6, 2] }, { "games": [2, 1] }] } } ``` For a walkover, send the player who advances. The response has `"rating_status": "not_rated"`: ```json { "competition_id": "cmp_6tw2hn8rxe", "venue_id": "ven_7hk2p9wq4m", "played_at": "2026-10-05T10:00:00+02:00", "player_ids": ["plr_9fz3tr6wn1", "plr_5jd8vk2lc7"], "status": "walkover", "winner_id": "plr_9fz3tr6wn1" } ``` To learn how a retirement affects the two ratings, see [How ratings work](/krk-plays-tennis-api/concepts/how-ratings-work/#worked-example-a-retired-match). ## If the request fails [Section titled “If the request fails”](#if-the-request-fails) | Status | `type` | Cause | Fix | | ------ | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | `401` | [`unauthenticated`](/krk-plays-tennis-api/errors/#unauthenticated) | The token is missing or expired. | [Get a new token](/krk-plays-tennis-api/authentication/access-token/). | | `403` | [`not-competition-organizer`](/krk-plays-tennis-api/errors/#not-competition-organizer) | Another organizer runs this competition. | Check `competition_id`. | | `409` | [`duplicate-match`](/krk-plays-tennis-api/errors/#duplicate-match) | The API already has a match with these players in this competition at this time. | Get the existing match instead of submitting it again. | | `409` | [`idempotency-conflict`](/krk-plays-tennis-api/errors/#idempotency-conflict) | You reused an `Idempotency-Key` with a different body. | Use a new key for each new result. | | `422` | [`invalid-score`](/krk-plays-tennis-api/errors/#invalid-score) | The score doesn’t fit the competition’s match format. | Compare each set with `match_format`, from step 1. | | `422` | [`validation-failed`](/krk-plays-tennis-api/errors/#validation-failed) | A field is missing or invalid. | Check the `errors` array in the response, which points to each invalid field. | # Changelog > Changes to the KRKplaysTennis API, newest first, with migration steps for breaking changes. This page lists every change to the KRKplaysTennis API, newest first. The API follows [semantic versioning](https://semver.org/): from 1.0.0, a breaking change only happens in a new major version, with a new base URL such as `/v2`. ## 1.0.1 [Section titled “1.0.1”](#101) *October 17, 2026* ### Changed [Section titled “Changed”](#changed) * **The docs moved to `saverio-cirillo.com/krk-plays-tennis-api/`.** Problem `type` URLs changed with them, for example from `…/portfolio/errors/#invalid-score` to `…/krk-plays-tennis-api/errors/#invalid-score`. The error type names after `#` didn’t change. Old URLs redirect to the new ones. If your code compares `type` with a stored URL, update the stored URL, or compare only the part after `#`. ## 1.0.0 [Section titled “1.0.0”](#100) *October 16, 2026* The first stable version. There are no changes to endpoints or schemas since 0.9.0. ### Changed [Section titled “Changed”](#changed-1) * The v1 contract is now stable. Fields, parameters, and error types in v1 won’t be removed or renamed. New optional fields and new operations can still appear, so ignore fields you don’t recognize. ## 0.9.0 [Section titled “0.9.0”](#090) *September 18, 2026 · Beta* ### Breaking changes [Section titled “Breaking changes”](#breaking-changes) * **`match_format` on competitions is now an object.** It was a string enum with the values `best_of_3` and `best_of_3_match_tiebreak`. It’s now an object with three independent rules, so it can describe short sets and no-ad scoring: ```json "match_format": { "games_per_set": 6, "no_ad": false, "final_set": "match_tiebreak" } ``` To migrate, map the old values like this: | Old value | New object | | -------------------------- | ----------------------------------------------------------------------- | | `best_of_3` | `{ "games_per_set": 6, "no_ad": false, "final_set": "full_set" }` | | `best_of_3_match_tiebreak` | `{ "games_per_set": 6, "no_ad": false, "final_set": "match_tiebreak" }` | If your code reads `match_format`, read the three fields instead of comparing strings. If it validates scores before you submit them, use `games_per_set` to check where a set ends. ### Added [Section titled “Added”](#added) * **Short sets.** Competitions can use sets to 4 games (`"games_per_set": 4`), with a tiebreak at 4-4. A set ends at 4 games with a 2-game lead, at 5-3, or at 5-4 with a tiebreak. * **No-ad scoring.** `no_ad` shows whether a game at deuce is decided by a single point. The API records games, not points, so it doesn’t affect score validation or ratings. * **`POST /oauth/token` in the OpenAPI spec.** The token endpoint on the authorization server is now documented in the [API reference](/krk-plays-tennis-api/reference/operations/getaccesstoken/), and the mock serves it, so you can test the whole client credentials flow locally.