Quickstart
From developer access through the sandbox club to your first pilot club, in four steps
In this quickstart you build a small club app: it reads a club's courts and gets a
message whenever someone books or cancels. Then you enable it for a first pilot club.
You need about half an hour, a CheckCourt account and a service that is reachable at a
public https address.
The four steps match the Erste Schritte (Getting started) card in your organization's overview in the developer portal. If you already have an organization, you can start directly with step 1.
Preparation
Access to the developer portal
The developer portal is at app.checkcourt.de/developer. You sign in with your regular CheckCourt account. You don't need a club, but you do need a verified email address.
What you see there depends on whether CheckCourt has opened the portal for sign-ups:
- Self sign-up: The page shows „Apps für Tennisvereine entwickeln“ (build apps for tennis clubs). Enter the Name der Organisation (organization name), accept the developer terms and click Organisation anlegen (Create organization). You become its owner, and your sandbox club is created right away. Each person can create up to three organizations; for more, contact support. If very many organizations are created at once, sign-up pauses for a while.
- Access by invitation: If sign-up is closed, the page shows „Zugang auf Einladung“ (access by invitation). Click Zugang anfragen (Request access) or write to [email protected]. Support then creates an organization and makes you its owner. Your CheckCourt account must already exist for this.
Provisional developer terms
The developer terms currently apply in a provisional version („Vorläufige Fassung vom 7. Oktober 2026“). Whoever signs up accepts this version. Once the final version is available, you will be notified and asked to accept it again.
Set up your organization
Under Organisation (Organization) you maintain the Profil (Profile) that clubs will see: name, Firmenname laut Impressum (company name as in the legal notice), Kontakt-E-Mail (contact email), Website and Datenschutzerklärung (privacy policy). Company name, contact email and privacy policy are required to publish in the marketplace.
Under Mitglieder (Members) you invite your team. Enter the email address of an existing CheckCourt account and choose a role:
| Role | Can |
|---|---|
| Inhaber (Owner) | Everything, plus maintain the profile and AVV template, invite and remove members, change roles, delete apps and remove them from the marketplace |
| Entwickler (Developer) | Create and edit apps: versions, secrets, webhooks, listing, install links, submissions |
The invited person accepts the invitation in the developer portal under Einladungen (Invitations). Invitations are valid for 14 days. An organization always needs at least one owner.
Get to know the sandbox club
Every organization gets its own sandbox club. Open it from the organization overview with Sandbox öffnen (Open sandbox). It is a regular CheckCourt club with test data:
- four courts, eight fictional members (such as „Anna Beispiel“ and „Ben Probe“) and a few bookings around today
- every member of your organization as Administrator, so you can try out every feature
- webhooks and management API keys are already enabled
To keep anything from leaking out of the sandbox, a few rules apply there:
- No new members. Members only join through your developer organization: whoever
joins the organization is automatically an Administrator in the sandbox club, and
whoever leaves it is removed there too. Nobody else can get in by any route: no
invitations (not even without an email address or through support), no CSV import, no
POST /api/v1/members, no invite links (existing ones stop working) and no approval of pending memberships. Instead of these actions, the members page shows a notice with a link to the organization in the developer portal, and the API responds with422and „Im Sandbox-Verein kommen Mitglieder nur über deine Developer-Organisation dazu.“ (In the sandbox club, members only join through your developer organization.) - Emails only go to members of your organization. Anything addressed to the fictional members is only logged, never sent.
- No real payments and no billing.
- No data processing agreement needed. If your manifest requires an AVV, you can still install the app in the sandbox, even before you have uploaded a template. The install and update dialogs don't ask for the AVV there. They only show „Im Sandbox-Verein ist kein Auftragsverarbeitungsvertrag nötig.“ (No data processing agreement is needed in the sandbox club.)
- Volume limit: at most 200 members and 5,000 bookings per sandbox club.
- Nothing is cached. Every page load calls your extensions afresh, so you see each change at once (see Caching).
View the club as a member. To see what an ordinary member sees, for example a member
app's extensions, a nav.page with requires or a booking without admin rights, open one
of the fictional members under Admin → Mitglieder (members) and click Als dieses
Mitglied ansehen (view as this member). CheckCourt switches to that member's session for
up to one hour; the banner at the top ends it with Beenden (end). This works only in the
sandbox club and only for the invented demo members, not for members of your organization.
Start and end appear in the club's activity log.
In the sandbox club's sidebar, Zurück zum Developer-Portal (Back to the developer portal) takes you back to your app.
The four steps
Create an app and upload a manifest
Create an app
In the organization overview, click Neue App (New app) and give it a name. The app starts in status Entwurf (Draft), so it can only be installed in the sandbox club.
On the Allgemein (General) tab you find the Client-ID (client ID, cca_app_…).
It is public and identifies your app.
Upload a manifest
Open the Versionen (Versions) tab and paste this manifest (or upload it as a file):
{
"installTargets": ["tenant"],
"tenantScopes": ["bookings:read", "courts:read"],
"events": ["booking.created", "booking.cancelled"],
"dataProcessing": {
"categories": ["Buchungszeiten", "Platz"],
"purpose": "Flutlicht für gebuchte Zeiten schalten",
"storageLocation": "EU",
"avvRequired": false
}
}Als neue Version speichern (Save as new version) turns it into version 1. Every
further upload creates a new, immutable version. If the manifest is invalid, the portal
shows each error with its path, for example events.0 with the message „Ereignis
booking.created braucht den Scope bookings:read“ (event booking.created requires the
scope bookings:read). The manifest reference explains
every field.
Create a client secret and enter the webhook URL
Create a client secret
Open the Zugangsdaten (Credentials) tab and click Neues Secret (New secret). The
secret starts with ccas_ and is shown only once. Store it in your service's
configuration, for example as an environment variable:
CHECKCOURT_CLIENT_ID=cca_app_…
CHECKCOURT_CLIENT_SECRET=ccas_…Up to five secrets can be active at the same time. This lets you rotate a secret without downtime: create a new one, put it in your service, revoke the old one.
Enter the webhook URL
Open the Webhooks tab, enter the address where your service receives events, and
save. On the first save, CheckCourt shows the app's Signatur-Secret (signing secret,
whsec_…), also only once. Every app has this secret from its creation on; it signs
webhooks and extension context tokens. An app without a webhook URL, for example one
with only an iframe page, gets it with Anzeigen (show) in the Signatur-Secret card
of the same tab:
CHECKCOURT_WEBHOOK_SECRET=whsec_…Enter the URL before you install the app, otherwise you miss the app.installed
event. The address must start with https:// and be publicly reachable. For development
on your own machine, enter your future production URL and run
checkcourt listen: it forwards your sandbox club's deliveries to
localhost.
Install and try it in the sandbox club
Install
Open the sandbox club and go to Admin → Apps → App hinzufügen (Admin → Apps → Add app). Your app is listed under Für euren Verein freigeschaltet (Enabled for your club) with the label Sandbox-App. Click Installieren (Install), review the permissions and data processing, and confirm.
Two things happen next:
- Your service receives the
app.installedevent. It contains theinstallation_id, which you store together with thetenant_id. - The app appears under Admin → Apps. The installation ID is also in the URL of its
page:
/admin/apps/inst_….
{
"id": "evt_…",
"type": "app.installed",
"created_at": "2026-10-06T09:30:00.000Z",
"tenant_id": "…",
"installation_id": "inst_…",
"object": { "type": "app_installation", "id": "inst_…" },
"data": { "installation_id": "inst_…", "app_id": "app_…", "version": 1 }
}Get an installation token
With the client ID, secret and installation ID, your service gets an access token for exactly this installation. It is valid for one hour.
curl -X POST "https://app.checkcourt.de/api/oauth/installation-token" \
-H "Content-Type: application/json" \
-d '{
"client_id": "'"$CHECKCOURT_CLIENT_ID"'",
"client_secret": "'"$CHECKCOURT_CLIENT_SECRET"'",
"installation_id": "inst_…"
}'const BASE = "https://app.checkcourt.de";
const cache = new Map(); // installation_id -> { token, expiresAt }
export async function installationToken(installationId) {
const cached = cache.get(installationId);
if (cached && cached.expiresAt > Date.now() + 60_000) return cached.token;
const res = await fetch(`${BASE}/api/oauth/installation-token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
client_id: process.env.CHECKCOURT_CLIENT_ID,
client_secret: process.env.CHECKCOURT_CLIENT_SECRET,
installation_id: installationId,
}),
});
if (!res.ok) {
const { error, error_description } = await res.json();
throw new Error(`${res.status} ${error}: ${error_description}`);
}
const body = await res.json();
cache.set(installationId, {
token: body.access_token,
expiresAt: Date.now() + body.expires_in * 1000,
});
return body.access_token;
}With the TypeScript SDK, installationAuth handles caching and
renewal:
import { createCheckCourtClient, installationAuth, type CheckCourtClient } from "@checkcourt/sdk";
const clients = new Map<string, CheckCourtClient>();
export function clientFor(installationId: string): CheckCourtClient {
let client = clients.get(installationId);
if (!client) {
client = createCheckCourtClient({
auth: installationAuth({
clientId: process.env.CHECKCOURT_CLIENT_ID!,
clientSecret: process.env.CHECKCOURT_CLIENT_SECRET!,
installationId,
}),
});
clients.set(installationId, client);
}
return client;
}The response:
{
"access_token": "cca_…",
"token_type": "Bearer",
"expires_in": 3600,
"installation_id": "inst_…",
"scopes": ["bookings:read", "courts:read"]
}First API call
Send the token as a bearer token to the REST API. It belongs to exactly one club, so you
don't need an X-Tenant-Id header.
curl "https://app.checkcourt.de/api/v1/courts" \
-H "Authorization: Bearer cca_…"import { installationToken } from "./token.js";
export async function listCourts(installationId) {
const token = await installationToken(installationId);
const res = await fetch("https://app.checkcourt.de/api/v1/courts", {
headers: { Authorization: `Bearer ${token}` },
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
return body.courts;
}import { unwrap } from "@checkcourt/sdk";
import { clientFor } from "./checkcourt";
export async function listCourts(installationId: string) {
const { courts } = await unwrap(clientFor(installationId).GET("/courts"));
return courts;
}Receive a webhook
Book a court in the sandbox club. Within about a minute, your service receives a
booking.created. Verify the signature and acknowledge with 200:
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.CHECKCOURT_WEBHOOK_SECRET;
const seen = new Set(); // in production: a table keyed by event id
function isFromCheckCourt(secret, rawBody, header) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const given = String(parts.v1 ?? "");
return (
given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))
);
}
app.post("/webhooks/checkcourt", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body.toString("utf8");
if (!isFromCheckCourt(SECRET, rawBody, req.get("CheckCourt-Signature") ?? "")) {
return res.status(401).end();
}
const event = JSON.parse(rawBody);
res.status(200).end();
if (seen.has(event.id)) return;
seen.add(event.id);
switch (event.type) {
case "app.installed":
// store event.installation_id and event.tenant_id
break;
case "app.uninstalled":
// delete all data for event.installation_id
break;
case "booking.created":
case "booking.cancelled":
console.log(event.type, event.data.court_id, event.data.date, event.data.start_time);
break;
}
});
app.listen(3000);import express from "express";
import { SIGNATURE_HEADER, verifyWebhook } from "@checkcourt/sdk";
const app = express();
const seen = new Set<string>(); // in production: a table keyed by event id
app.post("/webhooks/checkcourt", express.raw({ type: "application/json" }), async (req, res) => {
const event = await verifyWebhook({
secret: process.env.CHECKCOURT_WEBHOOK_SECRET!,
rawBody: req.body,
signatureHeader: req.get(SIGNATURE_HEADER),
}).catch(() => null);
if (!event) return res.status(401).end();
res.status(200).end();
if (seen.has(event.id)) return;
seen.add(event.id);
switch (event.type) {
case "app.installed":
// store event.installation_id and event.tenant_id
break;
case "app.uninstalled":
// delete all data for event.installation_id
break;
case "booking.created":
case "booking.cancelled":
console.log(event.type, event.data.court_id, event.data.date, event.data.start_time);
break;
}
});
app.listen(3000);You can see every delivery in the developer portal on the Webhooks tab under Zustellungen (Deliveries), with status, HTTP response and Erneut senden (Resend).
Set the status to private and enable pilot clubs
So far your app only runs in your own sandbox club. You enable it for real clubs one by one, without the marketplace or review.
Set the status to private
Open the Allgemein tab and under Status choose Privat (Private, „In
freigeschalteten Vereinen installierbar“: installable in enabled clubs). This requires
the app to have an uploaded manifest. If the manifest requires a data processing
agreement ("avvRequired": true), your organization must first have uploaded an AVV
template as a PDF under Organisation. If you later switch back to Entwurf, the
app can no longer be newly installed outside your sandbox. Existing installations keep
running.
Create an install link
Open the Freigaben (Access) tab and, under Installationslinks (Install links), click Link erstellen (Create link):
- Bezeichnung (optional) (Label): only for you, for example the pilot club's name, up to 80 characters
- Gültig für Tage (optional) (Valid for days): 1 to 365, empty means no expiry
- Vorabversion (Pre-release): The club then gets every new version immediately, including unreviewed ones. Without this checkbox, after CheckCourt's first approval it only gets reviewed versions, and until then the latest one
The link is shown only once and has the form
https://app.checkcourt.de/admin/apps/link/ccil_…. Copy it.
Send the link to the pilot club
Send the link to a person in the club who is allowed to Apps verwalten (manage apps)
there, usually an Administrator. They open it, see the provider, permissions and data
processing, and click Für unseren Verein freischalten und installieren (Enable and
install for our club). The installation then works as in your sandbox: your service
receives app.installed with the new installation_id, and you get installation tokens
for it.
Pure member apps are not installed this way: the club enables them and allows them for its members with Für Mitglieder freigeben (Allow for members).
Manage access
Under Freigeschaltete Vereine (Enabled clubs) you see which clubs have enabled the app through a link. You can widerrufen (revoke) a link, after which it can no longer be used. Clubs that already opened it keep their access until you lift it with Entfernen (Remove). Existing installations keep running even then, the club just can't newly install the app.
How to get from here into the marketplace is covered in Review and marketplace. Before you submit, go through the design guidelines.
What's next?
- A user interface directly in CheckCourt: UI extensions
- Let members connect their own account: OAuth for member apps
- Into the marketplace: Review and marketplace