CheckCourt
TypeScript SDK

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

OptionDefaultMeaning
authrequiredOne of the three strategies below
baseUrlhttps://app.checkcourt.deCheckCourt's address. A trailing /api/v1 is accepted
tenantIdnoneSent as X-Tenant-Id unless a request sets its own
retry{ maxRetries: 2 }Retries on 429, see Errors and retries. false turns them off
fetchglobalThis.fetchCustom fetch, for tests or a proxy
headersnoneExtra 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 settingsSchema

Read 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. onTokens runs 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 connected

Use 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

On this page