CheckCourt
TypeScript SDK

OAuth for member apps

PKCE, authorize URL, exchanging the code, refreshing and revoking tokens with the SDK

Member apps and user linking get their tokens through OAuth 2.1 with PKCE. The flow and all its rules are described in Authentication; this page shows how to implement it with the SDK. The example uses Express; the pattern fits any framework.

import { buildAuthorizeUrl, createPkcePair, createState } from "@checkcourt/sdk";

const CLIENT_ID = process.env.CHECKCOURT_CLIENT_ID!;
const CLIENT_SECRET = process.env.CHECKCOURT_CLIENT_SECRET!;
const REDIRECT_URI = "https://calendar.example.com/callback";

app.get("/connect", async (req, res) => {
  const pkce = await createPkcePair();
  const state = createState();
  req.session.oauth = { verifier: pkce.verifier, state };

  res.redirect(
    buildAuthorizeUrl({
      clientId: CLIENT_ID,
      redirectUri: REDIRECT_URI,
      state,
      codeChallenge: pkce.challenge,
      // prompt: "consent",  // force the dialog, e.g. to connect another club
      // tenant: "…",        // preselect a club
    }),
  );
});

createPkcePair creates a 43 character verifier and its S256 challenge.

Exchange the code for tokens

import { exchangeCode, parseAuthorizeCallback } from "@checkcourt/sdk";

app.get("/callback", async (req, res) => {
  const { verifier, state } = req.session.oauth;
  // checks state; throws OAuthError if CheckCourt sent an error instead (e.g. access_denied)
  const { code } = parseAuthorizeCallback(new URL(req.originalUrl, REDIRECT_URI), state);

  const tokens = await exchangeCode({
    clientId: CLIENT_ID,
    clientSecret: CLIENT_SECRET,
    code,
    redirectUri: REDIRECT_URI,
    codeVerifier: verifier,
  });

  await db.tokens.put(req.user.id, tokens);
  res.redirect("/connected");
});

tokens looks like this:

{
  accessToken: "ccu_…",
  refreshToken: "ccr_…",
  expiresAt: 1790003600000, // epoch milliseconds
  scope: ["bookings:read", "teams:read"],
  installations: [{ installationId: "inst_…", tenantId: "…" }],
}

The code is valid for 60 seconds and only once.

Use the tokens

Pass the stored tokens to userAuth, see Client and authentication. The SDK then refreshes them and calls onTokens with every new set.

Refreshing yourself

If you need a refresh without the client, for example in a background job:

import { refreshTokens, TokenRevokedError, AppTemporarilyUnavailableError } from "@checkcourt/sdk";

try {
  const next = await refreshTokens({ clientId, clientSecret, refreshToken: stored.refreshToken });
  await db.tokens.put(memberId, next); // persist right away, the old refresh token is spent
} catch (err) {
  if (err instanceof AppTemporarilyUnavailableError) {
    // connection paused: the token stays valid, retry later with the same one
  } else if (err instanceof TokenRevokedError) {
    // dead: delete the tokens, ask the member to connect again
  } else {
    throw err;
  }
}

Never run two refreshes with the same refresh token at the same time. CheckCourt treats that as reuse and revokes every token from that consent.

Revoking

import { revokeToken } from "@checkcourt/sdk";

await revokeToken({ clientId, clientSecret, token: stored.refreshToken });

A refresh token takes its whole chain with it. The connection itself stays; only the member or the club can end it.

Functions

FunctionReturns
createPkcePair()Promise<{ verifier, challenge, method: "S256" }>
pkceChallenge(verifier)Promise<string>, the S256 challenge for your own verifier
createState()A random state
buildAuthorizeUrl({ clientId, redirectUri, state, codeChallenge, prompt?, tenant?, baseUrl? })The redirect URL
parseAuthorizeCallback(url, expectedState){ code } or an OAuthError
exchangeCode({ clientId, clientSecret, code, redirectUri, codeVerifier })Promise<UserTokenSet>
refreshTokens({ clientId, clientSecret, refreshToken })Promise<UserTokenSet>
revokeToken({ clientId, clientSecret, token })Promise<void>
fetchInstallationToken({ clientId, clientSecret, installationId })Promise<InstallationToken>

All functions send the client ID and secret via HTTP Basic and accept optional baseUrl and fetch.

On this page