Manifest reference
All fields of the app manifest, allowed values, rules and what requires new consent
The manifest describes what a version of your app may do and does: which permissions it needs in the club and from members, which events it receives, where it appears in the UI and which data it processes for what purpose. Clubs and members consent to exactly this manifest.
You upload it in the developer portal on the Versionen (versions) tab, as text or as a JSON file.
Complete example
{
"manifestVersion": 1,
"installTargets": ["tenant"],
"userLinking": "optional",
"tenantScopes": ["bookings:read", "courts:read", "members:read"],
"userScopes": ["bookings:read"],
"events": ["booking.created", "booking.updated", "booking.cancelled", "court.locked", "court.unlocked"],
"extensions": [
{ "point": "app.settings", "kind": "iframe", "url": "https://tuer.example.de/checkcourt/settings" },
{ "point": "booking.detail.panel", "kind": "declarative", "url": "https://tuer.example.de/checkcourt/booking" },
{ "point": "booking.action", "kind": "declarative", "url": "https://tuer.example.de/checkcourt/open", "label": "Tür öffnen" },
{ "point": "kiosk.tile", "kind": "declarative", "url": "https://tuer.example.de/checkcourt/kiosk" }
],
"settingsSchema": {
"type": "object",
"properties": {
"lead_minutes": {
"type": "number",
"title": "Vorlauf in Minuten",
"description": "So lange vor Buchungsbeginn öffnet die Tür",
"default": 10,
"minimum": 0,
"maximum": 60
},
"door": {
"type": "string",
"title": "Tür",
"enum": ["Haupteingang", "Seiteneingang"],
"default": "Haupteingang"
},
"notify_admins": {
"type": "boolean",
"title": "Admins bei Störungen benachrichtigen",
"default": true
}
},
"required": ["door"],
"additionalProperties": false
},
"dataProcessing": {
"categories": ["Name", "Buchungszeiten", "Platz"],
"purpose": "Freischaltung der Platztür für gebuchte Zeiten",
"storageLocation": "EU",
"avvRequired": true
}
}With the TypeScript SDK you write the manifest in
TypeScript. defineManifest suggests scopes, events and extension points and reports, for
example, reserved scopes or an iframe on the dashboard at compile time:
import { defineManifest } from "@checkcourt/sdk";
export default defineManifest({
installTargets: ["tenant"],
tenantScopes: ["bookings:read", "courts:read"],
events: ["booking.created", "booking.cancelled"],
extensions: [
{ point: "booking.action", kind: "declarative", url: "https://door.example.de/ext/open", label: "Tür öffnen" },
],
dataProcessing: {
categories: ["Buchungsdaten"],
purpose: "Öffnet die Platztür zur gebuchten Zeit",
storageLocation: "EU",
avvRequired: true,
},
});You still upload JSON, for example via JSON.stringify(manifest, null, 2).
Unknown fields are an error everywhere. Scopes and events are sorted and deduplicated on save.
Fields
| Field | Required | Values | Meaning |
|---|---|---|---|
manifestVersion | no | 1 | Version of the manifest format, default 1 |
installTargets | yes | "tenant", "user", at least one | Club app, member app or both |
userLinking | no | "none" (default), "optional", "required" | Members also link their account to a club installation |
tenantScopes | no | Scopes, default [] | Permissions of the club installation |
userScopes | no | Scopes, default [] | Permissions the app asks a member for |
events | no | Event types, default [] | Events to deliver via webhook |
extensions | no | up to 20 entries | UI extensions |
settingsSchema | no | see below | Settings the club fills in at installation |
permissions | no | up to 10 entries | Rights the app offers the club's roles, see Permissions |
dataProcessing | yes | see below | Data processing, as shown to the club and the member |
shares | no | { "metadata": [...] }, up to 20 entries | Values on bookings, courts or members that other apps may read |
reads | no | { "metadata": [...] }, up to 50 entries | Values of other apps this app reads |
emits | no | up to 20 entries | Events this app publishes to other apps |
subscribes | no | up to 50 entries | Events of other apps this app receives |
Rules between fields
tenantScopesis allowed only with"tenant"ininstallTargets.userScopesis allowed only with"user"ininstallTargetsor with user linking (userLinkingnot"none").userLinkingis allowed only for club apps ("tenant"ininstallTargets). Members can link their account as soon as the club has installed the app."optional": the app works for the club even without connected members. At installation, the club reads "Mitglieder können ihr Konto verbinden." (members can connect their account)."required": member extensions and actions on behalf of a member work only for members who are connected. The club reads "Mitglieder müssen ihr Konto verbinden, damit die App für sie funktioniert." (members must connect their account for the app to work for them). Members who are not connected yet see a prompt with Zur App (go to app) under Einstellungen → Verbundene Apps (Settings → Connected apps) and Apps entdecken (discover apps), which leads to yourconnectUrlfrom the marketplace listing. So make sure to set it.
Scopes
Scopes come from the same catalog as the clubs' roles. GET /api/v1/permissions returns
the full catalog with German labels. At installation, clubs and members never see the
technical name, only the German label, such as „Belegung sehen“ (view occupancy) for
bookings:read.
Scopes blocked for apps
CheckCourt rejects these scopes in the manifest:
| Scope | Reason |
|---|---|
roles:read, roles:manage | Who has which permissions is decided by people, not by an integration |
apps:read, apps:manage | An app may not see, install or extend other apps |
webhooks:manage | Apps receive events through their manifest. Their own endpoints would bypass consent |
api_keys:manage | Keys can't create keys, and neither can apps |
billing:read, billing:manage | The CheckCourt subscription is a matter between the club and CheckCourt |
compliance:manage | Signing a data processing agreement (AVV) binds the club contractually and requires a person |
All other scopes are allowed. Still, only ask for what your app really needs: the review checks that the permissions fit the purpose.
App-only scopes
Some scopes describe what an integration does, not what a person may do. No club role holds them, not even Administrator, so whoever installs or connects your app does not need them, and member apps get them from the member's consent alone.
| Scope | What it allows |
|---|---|
notifications:send | Notify members through CheckCourt without learning their contact data, see Notifications |
What a scope means for member apps
A member app acts on behalf of its member. Some scopes allow more than their name suggests. The consent dialog states this explicitly:
| Scope | Notice in the consent dialog |
|---|---|
bookings:read | The app can use it to book, cancel and check in on your behalf |
teams:read | If you are a team leader, the app can use it to edit your team and book for it |
posts:read | The app can use it to delete your own posts |
work_hours:write | The app can use it to sign you up for work shifts and log hours for you |
Events
events lists the events to be sent to your app via webhook. You can subscribe to:
| Event | Requires scope |
|---|---|
booking.created, booking.updated, booking.cancelled, booking.checked_in | bookings:read |
member.joined, member.updated, member.left | members:read |
court.locked, court.unlocked | courts:read |
The required scope must be in tenantScopes or userScopes, otherwise the manifest is
rejected. app.installed, app.upgraded, app.settings_updated and app.uninstalled
don't belong in the manifest; they are always sent. All event fields are in the
event catalog.
Extensions
Every entry in extensions has these fields:
| Field | Required | Meaning |
|---|---|---|
point | yes | The extension point |
kind | yes | "declarative" or "iframe" |
url | yes | https URL of your service |
label | for booking.action, booking_plan.action, sidebar.action and nav.page; optional elsewhere | Label, see below |
icon | for nav.page; optional for booking_plan.action and sidebar.action; not allowed elsewhere | Icon of the entry, see below |
height | no; iframe only | Initial frame height in whole pixels, 120 to 2,000. Without it the frame starts at 240 pixels. A nav.page iframe always fills the page and ignores it. See iframe extensions |
requires | no; nav.page only | Key of one of your permissions. Only people holding it see the entry and can open the page, see Permissions |
| Point | Where | For | declarative | iframe | Requires scope |
|---|---|---|---|---|---|
app.settings | App page under Admin → Apps | Club apps | yes | yes | none |
booking.detail.panel | Booking detail | both | yes | yes | bookings:read |
member.profile.section | Member page in the admin area and own settings | both | yes | yes | members:read |
dashboard.widget | Dashboard | both | yes | no | none |
booking.action | Button in the booking detail | both | yes | no | bookings:read |
kiosk.tile | Home screen of the kiosk terminal | Club apps | yes | no | none |
court.annotation | Badges in the court headers of the booking plan and on the kiosk | Club apps | yes | no | courts:read |
member.list.column | Column in the member list under Admin → Mitglieder | Club apps | yes | no | members:read |
member.settings.section | Card on the member's own settings page | Member apps | yes | yes | none |
booking.hint | Hint in the booking dialog | Club apps | yes | no | bookings:read |
booking_plan.action | Button in the header of the booking plan | Club apps | yes | no | courts:read |
sidebar.action | Entry at the bottom of the sidebar and mobile navigation | both | yes | no | none |
nav.page | Entry in the main navigation with a page of its own | both | yes | yes | none |
Rules:
- Each point may appear only once, except
booking.action. This includes all six host surface points. - The point must match one of the app's installation types.
- If a point requires a scope, it must be present for every matching installation type:
in
tenantScopesfor club apps, inuserScopesfor member apps. - Dashboard, kiosk and actions stay declarative. They sit on paths every member sees, and third-party JavaScript there would be too big a risk for load time and appearance. The same goes for court badges, member list columns, booking hints and static actions: CheckCourt draws them itself.
label
booking.action: required, 1 to 40 characters. The button label in the booking details.booking_plan.actionandsidebar.action: required, 1 to 24 characters. These are static actions: CheckCourt draws the button or entry from the manifest alone, before your app is ever called, so the label has to be in the manifest and short enough for the plan header and the sidebar.nav.page: required, 1 to 24 characters. The navigation entry and the title of your app's page.member.list.column: optional, 1 to 40 characters. CheckCourt uses it as the column header until your app's first answer arrives, then showscolumn.title(up to 20 characters). Use the same text for both so the header doesn't change.- Panels (
booking.detail.panel,member.profile.section,dashboard.widget,kiosk.tile,member.settings.section,app.settings): optional, 1 to 40 characters. The title of the card your extension sits in. Without it the card has no title. CheckCourt never shows your app's name or icon there instead.
icon
nav.page requires an icon. booking_plan.action and sidebar.action take an
optional one; on any other point it is an error. The value is one of these Lucide icon
names, a fixed list so the navigation keeps one consistent icon set:
bell, calendar, calendar-check, camera, chart-column, circle-help,
clipboard-list, clock, cloud-rain, door-open, file-text, flag, heart-pulse,
info, key-round, lightbulb, link, list-checks, lock-open, map-pin,
megaphone, message-square, receipt, send, sparkles, star, sun,
thermometer, ticket, triangle-alert, trophy, user-round, users, wallet,
wrench
{
"point": "sidebar.action",
"kind": "declarative",
"url": "https://wetter.example.de/ext/sidebar",
"label": "Platzwetter",
"icon": "sun"
}How extensions work is described in UI extensions.
Permissions for the club's roles (permissions)
Your app can offer its own rights, for example who may manage a ladder or see statistics. The club decides in its role editor (Admin → Rollen und Gruppen, roles and groups) which roles hold them: each app with permissions appears there as its own area next to CheckCourt's areas, and in the role overview as a row under Installierte Apps (installed apps). The app's page under Admin → Apps lists them in the card Rechte für Rollen (rights for roles). When a club installs your app, only administrators hold its permissions; the club grants them to other roles, or to the base role Mitglied for everyone. Like CheckCourt's own rights, a person can only hand out a role whose app rights they hold themselves.
{
"permissions": [
{ "key": "manage_ladder", "label": "Rangliste verwalten", "description": "Darf Ergebnisse korrigieren" },
{ "key": "view_stats", "label": "Statistik sehen" }
]
}| Field | Required | Meaning |
|---|---|---|
key | yes | ^[a-z][a-z0-9_]{0,39}$, unique in the manifest. Stays the same across versions |
label | yes | 1 to 60 characters, German, shown in the role editor |
description | no | Up to 200 characters, shown when the club opens the area |
Rules:
- At most 10 permissions, only for club apps (
"tenant"ininstallTargets). - Permissions grant your app nothing. They tell you what the person in front of your
extension may do. Each extension request carries the flags in
viewer.capabilities.permissions, and your app decides what to show. - A
nav.pagewithrequiresis only listed and opened for people holding that permission. CheckCourt enforces this for the page and its actions. - New permissions in a version need no new consent. A key a new version no longer declares is dropped from the club's roles when the installation updates. Uninstalling the app removes all of its permissions from the club's roles.
Settings (settingsSchema)
With settingsSchema you describe settings the club fills in at installation and later on
the app's page. CheckCourt builds a form from it in its own design. A small subset of JSON
Schema is allowed:
{
"type": "object",
"properties": { "…": "…" },
"required": ["…"],
"additionalProperties": false
}- Up to 30 fields. Field names start with a letter and consist of up to 40 letters, digits and underscores.
- Every field needs
title(up to 80 characters), optionallydescription(up to 300 characters). - Every name in
requiredmust be listed underproperties.
type | Further options |
|---|---|
"string" | default (up to 1,000 characters), maxLength (1 to 1,000) or enum (1 to 50 distinct values, then shown as a dropdown) |
"number" | default, minimum, maximum |
"boolean" | default (shown as a switch) |
enum and maxLength are mutually exclusive. default must fit enum, maxLength,
minimum and maximum. CheckCourt validates the stored values against the schema and
rejects unknown fields.
Your app reads the stored values with its installation token via
GET /api/v1/app/installation, see API for apps.
When the club changes the settings, you receive the app.settings_updated event and fetch
the new values there.
Data processing (dataProcessing)
| Field | Values | Meaning |
|---|---|---|
categories | 1 or more entries, each up to 80 characters | Which data the app processes, in German, such as „Name“, „Buchungszeiten“ |
purpose | up to 500 characters | What for, in one sentence for club boards |
storageLocation | "EU" or "non-EU" | Where the data is stored. Outside the EU is clearly flagged to the club and the member |
avvRequired | true or false | Whether the club signs a data processing agreement (AVV) with you |
With "avvRequired": true, your organization must have uploaded an AVV template before
you set the app to Privat (private), create installation links or submit it for
review. In the sandbox club you don't need one. More in
Privacy and obligations.
Connections to other apps (shares, reads, emits, subscribes)
Club apps can share values on bookings, courts and members with other club apps and publish events to them. CheckCourt passes the data on only after the club approves the connection. The fields, their limits and the matching API are described in Connections between apps.
- All four fields need
"tenant"ininstallTargets. - Each entry of
shares.metadataandreads.metadataneeds the object's read scope intenantScopes(bookings:read,courts:readormembers:read). - New entries in a version don't require new consent for the installation. Instead, they wait for the club to approve the connection again.
URLs in development and production
Declarative extension URLs must start with https:// and be publicly reachable. Internal
addresses such as private IP ranges are blocked; the same rules apply as for
webhook URLs. iframe extensions may
point at http://localhost during development; they then load only in your sandbox club.
Locally, declarative extensions reach you through the CLI. Only a
manifest without local URLs can be submitted for review.
Versions
- Every upload is a new version, numbered sequentially. A version can't be changed afterwards. This matters because clubs and members consented to exactly this version.
- The portal labels the newest version Aktuell (current) and the one approved for the marketplace Im Marktplatz (in marketplace).
- For each version, the portal shows whether existing installations get it automatically (Automatisches Update, automatic update) or whether it requires new consent (Braucht neue Zustimmung, needs new consent).
- Suspended apps don't get new versions.
Which version a club actually gets depends on how it got the app. This is explained in Review and marketplace.
When new consent is required
A new version requires consent from the club or the member if, compared to the installed version, it adds:
- a new scope in
tenantScopesoruserScopes - a new event in
events - a new data category in
dataProcessing.categories(case doesn't matter) - a new extension point
- a point that now appears as
iframeinstead of declarative storageLocationchanges from"EU"to"non-EU"avvRequiredchanges fromfalsetotrue
Everything else, such as fewer permissions, a changed purpose or new URLs for existing
extensions, is applied to existing installations automatically, and they receive
app.upgraded.
If a version requires consent, the installation keeps running with its previous version
and permissions. The club sees Update wartet (update pending) and consents under
Admin → Apps; members see Neue Rechte (new permissions) under Einstellungen →
Verbundene Apps. Only then does the new version run and app.upgraded is sent.
If the new version adds required settings without a default value, the club must fill
them in during the update. So give new required fields a default.