CLI for local development
Log in, forward webhooks and extensions from your sandbox club to localhost, trigger test events and read delivery logs
The checkcourt CLI connects the app running on your machine to your developer
organisation's sandbox club. You do not need a public URL, a tunnel or a deployed
webhook endpoint: CheckCourt hands webhook deliveries and extension requests for the
sandbox to the CLI, and the CLI replays them against localhost.
checkcourt login
checkcourt listen --app demo-app --forward-to http://localhost:4000
checkcourt trigger booking.created --app demo-app
checkcourt logs --app demo-app --followThe relay only ever serves the sandbox club of the organisation that owns the app. Installations in real clubs keep receiving deliveries at your production URLs, whether or not a CLI is connected.
Install
The CLI is open source at CheckCourt/cli. It needs Node.js 20 or later. Install it globally from npm:
npm install -g @checkcourt/cli
checkcourt helpTo run it without a global install, use npx @checkcourt/cli help. Release notes are in
the repository's changelog.
By default the CLI talks to https://app.checkcourt.de. Point it at another platform
with CHECKCOURT_HOST or --host.
Prerequisites
- You are a member of a developer organisation that has a sandbox club. See Apps.
- Your app is installed in the sandbox club.
- You have the app's signing secret, which every app has from its creation on (developer portal → your app → Webhooks, card Signatur-Secret).
- For webhooks, the app has a webhook URL on the same tab. It may be your future
production address; while you listen, the sandbox's deliveries go to the CLI instead.
Only
httpsURLs are accepted in production. Declarative extensions are forwarded with the URLs from your manifest, no webhook URL needed.
Log in
checkcourt loginThe CLI uses the OAuth 2.0 Device Authorization Grant (RFC 8628). It prints a code such
as KCNR-RQRR and opens /developer/cli in your browser. Check that the code matches,
then confirm with Anmelden bestätigen. The page lists the organisations the login
will work for. Meanwhile the CLI polls until you confirm, then stores the token in
~/.checkcourt.json.
- The token starts with
ccd_, is valid for 30 days and is stored hashed on the platform. - It only allows the developer commands on this page, for the apps of organisations you are a member of. It cannot call the REST API and never carries support rights.
- Use
--no-browseron machines without a browser and open the printed URL elsewhere. - For CI, set
CHECKCOURT_CLI_TOKENinstead of logging in.
Your logins are listed in the developer portal under Organisation → CLI-Sitzungen,
where you can end any of them. checkcourt logout ends the current one.
Forward requests: listen
checkcourt listen --app demo-app --forward-to http://localhost:4000| Option | Meaning |
|---|---|
--app | Slug or id of your app |
--forward-to | Origin of your local app |
--webhook-path | Path for webhooks. Default: the path of the app's webhook URL |
--extension-path | Path for all extension requests. Default: the path of each extension URL in the manifest |
Without the path options, the CLI keeps the paths of your production URLs. A webhook
URL https://demo.example/api/checkcourt/webhooks and a manifest extension
https://demo.example/api/checkcourt/extensions/dashboard-widget become
http://localhost:4000/api/checkcourt/webhooks and
http://localhost:4000/api/checkcourt/extensions/dashboard-widget, so your app's real
routes handle them.
Every request prints one line: time, kind, event type or extension point, the status your app returned and the duration.
14:02:11 webhook booking.created -> 200 41 ms http://localhost:4000/api/checkcourt/webhooks
14:02:15 extension dashboard.widget -> 200 12 ms http://localhost:4000/api/checkcourt/extensions/dashboard-widget
14:02:20 webhook booking.cancelled -> fehler ECONNREFUSED 3 msPress Ctrl+C to stop. If the connection drops, the CLI reconnects with exponential backoff (up to 30 seconds). It gives up when the login is invalid, the session was ended in the portal, or the app is not yours.
What is forwarded
| Request | Forwarded while listening | Timeout |
|---|---|---|
| Webhook deliveries to the sandbox installation (club and member installations) | Yes | 10 s |
| Declarative extensions (render and actions) | Yes | 2 s |
| iframe extensions | No, the browser loads them directly. Use a http://localhost URL, see below | |
| Deliveries to real clubs | Never |
Requests carry exactly the headers and body production would send, including
CheckCourt-Signature, CheckCourt-Event-Id, CheckCourt-Installation-Id and, for
extensions, CheckCourt-Context. Signatures use your app's webhook secret, so
verification code works unchanged:
import { SIGNATURE_HEADER, verifyWebhook } from "@checkcourt/sdk";
export async function POST(request: Request) {
const event = await verifyWebhook({
secret: process.env.CHECKCOURT_WEBHOOK_SECRET!,
rawBody: await request.text(),
signatureHeader: request.headers.get(SIGNATURE_HEADER),
});
// ...
return new Response(null, { status: 200 });
}Webhook deliveries keep their normal retry schedule: if your local app is down or
returns an error, the delivery is retried later, through the CLI if it is still
listening and over HTTP otherwise. Deliveries that went through the CLI are marked
via: "cli" in checkcourt logs.
localhost URLs in the sandbox
Two kinds of addresses may point at http://localhost or http://127.0.0.1 even on
the production platform:
- iframe extensions in the manifest (for example
app.settings), and - OAuth redirect URIs.
They only take effect in your organisation's sandbox club. Real clubs never load such
an iframe, and the consent screen only offers the sandbox club when the redirect URI is
local. Declarative extensions and the webhook URL must stay https; locally they reach
you through listen.
A version or listing that still contains local addresses cannot be submitted for review.
Upload a version with public https URLs and remove local redirect URIs first, see
Review and marketplace.
Trigger test events: trigger
checkcourt trigger booking.created --app demo-app
checkcourt trigger --listtrigger makes a real change in your sandbox club through the same code paths a club
member would use, so the event is produced by the normal emission and delivered like any
other:
| Event | What happens |
|---|---|
booking.created | Books the next free court for an invented sandbox member |
booking.cancelled | Cancels an upcoming booking (books one first if there is none) |
member.updated | Changes an invented member's phone number |
court.locked | Locks a court that is not locked |
court.unlocked | Unlocks a locked court (locks one first if needed) |
app.settings_updated | Reports changed settings for your sandbox installation without changing them |
The delivery starts right away instead of waiting for the next worker run. Your app only receives events it subscribes to in the manifest and has the scopes for.
Delivery logs: logs
checkcourt logs --app demo-app
checkcourt logs --app demo-app --followShows the most recent webhook deliveries to your app's sandbox installations: time, event
type, status, attempts, whether the last attempt went through the CLI, and your app's
HTTP status or the error. --follow keeps polling and prints changes. Deliveries to real
clubs are in the developer portal under your app → Webhooks.
How routing works
listen keeps a WebSocket connection to the platform open, authenticated with your CLI
token. When CheckCourt is about to call your app for an installation in your
organisation's sandbox club and a listen session for the app is connected, it sends the
request through that connection instead of over HTTP. The CLI calls your local app and
returns the response, and the platform waits with the normal timeout. Without a connected
session, requests go over HTTP as usual.
With several listen processes for the same app, the most recently started one
receives the traffic.
Limits
- Sessions send a heartbeat every 30 seconds. Without one for 90 seconds, routing falls back to HTTP; after 30 minutes the session is ended.
- Ending a CLI session in the portal, logging out, or leaving the organisation ends the relay at the next heartbeat.
- Timeouts are the production ones: 10 seconds for webhooks, 2 seconds for declarative extensions. A slow local app fails the same way it would in production.
- Responses larger than 64 KB fail for extensions as in production; webhook response bodies are stored truncated.
API commands
Besides the app development commands, the CLI is a small example client for the REST API.
It uses a personal API key (ck_user_…, see Which key can do what?)
instead of checkcourt login:
export CHECKCOURT_API_KEY="ck_user_..."
checkcourt tenants # your clubs and their ids
export CHECKCOURT_TENANT_ID="<club id>"
checkcourt slots --date 2026-10-14 --duration 60
checkcourt bookings
checkcourt book --court 1 --date 2026-10-14 --start 10:00 --end 11:00 --guest "Max"
checkcourt cancel <booking id>Run checkcourt without arguments to start the interactive terminal app: it asks for your
key once, then lets you pick a club, browse free slots, book and cancel.
Configuration
| Variable | Meaning |
|---|---|
CHECKCOURT_HOST | Platform origin for login, listen, trigger and logs. Default https://app.checkcourt.de. Also --host |
CHECKCOURT_CLI_TOKEN | Developer token for CI instead of checkcourt login |
CHECKCOURT_API_KEY | Personal API key for the API commands |
CHECKCOURT_TENANT_ID | Club id for club-scoped API commands |
CHECKCOURT_BASE_URL | API base URL for the API commands. Default https://app.checkcourt.de/api/v1 |
Environment variables override the config file ~/.checkcourt.json.
Security
- The CLI token is bound to the person who confirmed it in the browser and only works for organisations that person currently belongs to.
- Only the sandbox club of the app's own organisation is ever relayed. There is no way to route deliveries of a real club to a developer machine.
- Request contents are the sandbox's invented data. Do not copy real member data into your sandbox.
- Relay sessions, CLI logins and triggered events are written to the audit log.