Get an access token
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 instead.
Before you begin
Section titled “Before you begin”- A client ID and a client secret for your organizer account. With the local mock, any non-empty values work.
KRK_API_URLset to the API base URL. For the mock, that’shttp://127.0.0.1:4010.
-
Store the authorization server URL and your client credentials in environment variables:
Terminal window export KRK_AUTH_URL=http://127.0.0.1:4010export KRK_CLIENT_ID=your-client-idexport KRK_CLIENT_SECRET=your-client-secretFor the mock, the authorization server runs at the same address as the API. In production,
KRK_AUTH_URLishttps://auth.krkplaystennis.example.To check the variables, run
echo $KRK_AUTH_URL. The output is the URL you set. -
Request a token. Send your client ID and client secret with HTTP Basic authentication, and the grant type and scope as form fields:
Terminal window curl --silent "$KRK_AUTH_URL/oauth/token" \--user "$KRK_CLIENT_ID:$KRK_CLIENT_SECRET" \--data grant_type=client_credentials \--data scope=matches:writeSave this code as
get-token.mjs, then runnode get-token.mjs: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());Save this code as
get_token.py, then runpython3 get_token.py:import osimport requestsresponse = 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
200and contains the token:{"access_token": "eyJhbGciOiJSUzI1NiJ9.fictional-token-for-docs.c2lnbmF0dXJl","token_type": "Bearer","expires_in": 3600,"scope": "matches:write"} -
Copy the
access_tokenvalue from the response in step 2, and store it in an environment variable:Terminal window export KRK_ACCESS_TOKEN='<access_token from step 2>'To check it, run
echo $KRK_ACCESS_TOKEN. The output is the token. -
Send the token as
Authorization: Bearer <token>to submit a match result. Each new result also needs a uniqueIdempotency-Key:Terminal window 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]}}'Save this code as
submit-match.mjs, then runnode submit-match.mjs: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());Save this code as
submit_match.py, then runpython3 submit_match.py:import osimport uuidimport requestsresponse = 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
201and the new match, with"rating_status": "pending". The API rates the match a few minutes later.
Renew the token
Section titled “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”| 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), so its errors have an error field. The rest of the API uses problem details, with a type field.
© 2026 Saverio Cirillo · KRKplaysTennis is a fictional API · Built with Starlight