Client and authentication
Use the typed API client with an API key, an installation token or a member token
createCheckCourtClient gives you a client for every endpoint under /api/v1. Paths,
parameters, bodies and responses are typed; your editor suggests them and flags typos
before the request is sent.
import { apiKeyAuth, createCheckCourtClient, unwrap } from "@checkcourt/sdk";
const client = createCheckCourtClient({
auth: apiKeyAuth(process.env.CHECKCOURT_API_KEY!), // ck_mgmt_… or ck_user_…
});
const { courts } = await unwrap(client.GET("/courts"));Options
| Option | Default | Meaning |
|---|---|---|
auth | required | One of the three strategies below |
baseUrl | https://app.checkcourt.de | CheckCourt's address. A trailing /api/v1 is accepted |
tenantId | none | Sent as X-Tenant-Id unless a request sets its own |
retry | { maxRetries: 2 } | Retries on 429, see Errors and retries. false turns them off |
fetch | globalThis.fetch | Custom fetch, for tests or a proxy |
headers | none | Extra headers on every request |
Reading responses
The client is built on openapi-fetch. Every call
returns { data, error, response } and does not throw on HTTP errors. You have two
options:
// 1. Check yourself
const { data, error, response } = await client.GET("/members/{id}", {
params: { path: { id: memberId } },
});
if (error) console.warn(response.status, error.error.code);
// 2. unwrap: data or a CheckCourtApiError
const member = await unwrap(client.GET("/members/{id}", { params: { path: { id: memberId } } }));Path parameters, query and body work as in openapi-fetch:
await unwrap(
client.POST("/me/bookings", {
// Requests accept "18:00"; responses always return "18:00:00".
body: { courtId: 1, date: "2026-10-07", startTime: "18:00", endTime: "19:00" },
headers: { "X-Tenant-Id": tenantId }, // overrides the tenantId option
}),
);Authentication
API key
For your own club or your own account, see the developer overview.
const client = createCheckCourtClient({
auth: apiKeyAuth(process.env.CHECKCOURT_API_KEY!),
tenantId: "…", // only needed for personal keys (ck_user_)
});Installation token (club apps)
installationAuth fetches a cca_ token from POST /api/oauth/installation-token when
needed, caches it and fetches a new one 60 seconds before it expires. When many requests
ask for a token at the same time, only one request goes to CheckCourt.
import { createCheckCourtClient, installationAuth, unwrap } from "@checkcourt/sdk";
export function clientFor(installationId: string) {
return createCheckCourtClient({
auth: installationAuth({
clientId: process.env.CHECKCOURT_CLIENT_ID!,
clientSecret: process.env.CHECKCOURT_CLIENT_SECRET!,
installationId,
}),
});
}
const { courts } = await unwrap(clientFor("inst_…").GET("/courts"));Create one client per installation and reuse it, for example in a Map. A new client
with a new installationAuth starts with an empty cache and fetches a new token.
If the API answers 401, for example after you rotated the client secret, the client
fetches a fresh token once and repeats the request.
Own installation and settings
import { getInstallation } from "@checkcourt/sdk";
const installation = await getInstallation(clientFor("inst_…"));
installation.settings; // the values for your settingsSchemaRead it after app.installed, app.upgraded and app.settings_updated. This works with
an installation token only.
Member token (member apps)
userAuth uses the tokens from the OAuth flow and refreshes
them on its own:
import { createCheckCourtClient, userAuth } from "@checkcourt/sdk";
const stored = await db.tokens.get(memberId); // { accessToken, refreshToken, expiresAt }
const auth = userAuth({
clientId: process.env.CHECKCOURT_CLIENT_ID!,
clientSecret: process.env.CHECKCOURT_CLIENT_SECRET!,
tokens: stored,
onTokens: (tokens) => db.tokens.put(memberId, tokens),
});
const client = createCheckCourtClient({ auth, tenantId });- The access token is refreshed 60 seconds before it expires, and right away after a
401. - Rotation: every refresh returns a new refresh token.
onTokensruns before the new token is used. Persist there: the old refresh token is spent. - One refresh at a time: when several requests hit an expired token at once, the SDK sends a single refresh. Otherwise CheckCourt would treat it as reuse and revoke the whole chain.
- This lock is per process. If your service runs several instances, make sure a member is refreshed in one place at a time, for example with a database lock per member.
How to react to TokenRevokedError and AppTemporarilyUnavailableError is covered in
Errors and retries.
Who connected the app
import { getMe } from "@checkcourt/sdk";
const me = await getMe(createCheckCourtClient({ auth }));
me.user_id; // "usr_…": stable for your app, different in every other app
me.tenants; // [{ id, name }] where the member has your app connectedUse user_id as the key of the member's account in your app. It is a pseudonym: no name,
no email, and it is not the CheckCourt user ID from API objects, webhooks and context
tokens. See GET /me.
Several clubs
A ccu_ token works in every club where the member connected your app. With more than
one, you need X-Tenant-Id. Share one userAuth between several clients so they all
use the same token state and the same lock:
const clubs = await unwrap(createCheckCourtClient({ auth }).GET("/me/tenants"));
for (const club of clubs.tenants ?? []) {
const client = createCheckCourtClient({ auth, tenantId: club.id });
const { bookings } = await unwrap(client.GET("/me/bookings"));
}Notify a member
import { sendNotification } from "@checkcourt/sdk";
const { id } = await sendNotification(client, {
recipient: "psn_…", // or usr_… from getMe(), or the user id with members:read
title: "Deine Ballmaschine ist bereit",
body: "Platz 3 ab 17:30 Uhr.",
url: "/booking?date=2026-05-01",
idempotencyKey: "reservation-8812-ready",
});Needs notifications:send. CheckCourt delivers the message; your app never learns the
member's contact data. Rules and limits: Notifications.
Without the client
The strategies also work on their own if you make your own HTTP calls:
const auth = installationAuth({ clientId, clientSecret, installationId });
const token = await auth.getAccessToken(); // cca_…, cached or fresh