CheckCourt
App platform

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

manifest.json
{
  "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:

manifest.ts
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

FieldRequiredValuesMeaning
manifestVersionno1Version of the manifest format, default 1
installTargetsyes"tenant", "user", at least oneClub app, member app or both
userLinkingno"none" (default), "optional", "required"Members also link their account to a club installation
tenantScopesnoScopes, default []Permissions of the club installation
userScopesnoScopes, default []Permissions the app asks a member for
eventsnoEvent types, default []Events to deliver via webhook
extensionsnoup to 20 entriesUI extensions
settingsSchemanosee belowSettings the club fills in at installation
permissionsnoup to 10 entriesRights the app offers the club's roles, see Permissions
dataProcessingyessee belowData processing, as shown to the club and the member
sharesno{ "metadata": [...] }, up to 20 entriesValues on bookings, courts or members that other apps may read
readsno{ "metadata": [...] }, up to 50 entriesValues of other apps this app reads
emitsnoup to 20 entriesEvents this app publishes to other apps
subscribesnoup to 50 entriesEvents of other apps this app receives

Rules between fields

  • tenantScopes is allowed only with "tenant" in installTargets.
  • userScopes is allowed only with "user" in installTargets or with user linking (userLinking not "none").
  • userLinking is allowed only for club apps ("tenant" in installTargets). 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 your connectUrl from 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:

ScopeReason
roles:read, roles:manageWho has which permissions is decided by people, not by an integration
apps:read, apps:manageAn app may not see, install or extend other apps
webhooks:manageApps receive events through their manifest. Their own endpoints would bypass consent
api_keys:manageKeys can't create keys, and neither can apps
billing:read, billing:manageThe CheckCourt subscription is a matter between the club and CheckCourt
compliance:manageSigning 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.

ScopeWhat it allows
notifications:sendNotify 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:

ScopeNotice in the consent dialog
bookings:readThe app can use it to book, cancel and check in on your behalf
teams:readIf you are a team leader, the app can use it to edit your team and book for it
posts:readThe app can use it to delete your own posts
work_hours:writeThe 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:

EventRequires scope
booking.created, booking.updated, booking.cancelled, booking.checked_inbookings:read
member.joined, member.updated, member.leftmembers:read
court.locked, court.unlockedcourts: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:

FieldRequiredMeaning
pointyesThe extension point
kindyes"declarative" or "iframe"
urlyeshttps URL of your service
labelfor booking.action, booking_plan.action, sidebar.action and nav.page; optional elsewhereLabel, see below
iconfor nav.page; optional for booking_plan.action and sidebar.action; not allowed elsewhereIcon of the entry, see below
heightno; iframe onlyInitial 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
requiresno; nav.page onlyKey of one of your permissions. Only people holding it see the entry and can open the page, see Permissions
PointWhereFordeclarativeiframeRequires scope
app.settingsApp page under Admin → AppsClub appsyesyesnone
booking.detail.panelBooking detailbothyesyesbookings:read
member.profile.sectionMember page in the admin area and own settingsbothyesyesmembers:read
dashboard.widgetDashboardbothyesnonone
booking.actionButton in the booking detailbothyesnobookings:read
kiosk.tileHome screen of the kiosk terminalClub appsyesnonone
court.annotationBadges in the court headers of the booking plan and on the kioskClub appsyesnocourts:read
member.list.columnColumn in the member list under Admin → MitgliederClub appsyesnomembers:read
member.settings.sectionCard on the member's own settings pageMember appsyesyesnone
booking.hintHint in the booking dialogClub appsyesnobookings:read
booking_plan.actionButton in the header of the booking planClub appsyesnocourts:read
sidebar.actionEntry at the bottom of the sidebar and mobile navigationbothyesnonone
nav.pageEntry in the main navigation with a page of its ownbothyesyesnone

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 tenantScopes for club apps, in userScopes for 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.action and sidebar.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 shows column.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" }
  ]
}
FieldRequiredMeaning
keyyes^[a-z][a-z0-9_]{0,39}$, unique in the manifest. Stays the same across versions
labelyes1 to 60 characters, German, shown in the role editor
descriptionnoUp to 200 characters, shown when the club opens the area

Rules:

  • At most 10 permissions, only for club apps ("tenant" in installTargets).
  • 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.page with requires is 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), optionally description (up to 300 characters).
  • Every name in required must be listed under properties.
typeFurther 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)

FieldValuesMeaning
categories1 or more entries, each up to 80 charactersWhich data the app processes, in German, such as „Name“, „Buchungszeiten“
purposeup to 500 charactersWhat 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
avvRequiredtrue or falseWhether 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" in installTargets.
  • Each entry of shares.metadata and reads.metadata needs the object's read scope in tenantScopes (bookings:read, courts:read or members: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.

A new version requires consent from the club or the member if, compared to the installed version, it adds:

  • a new scope in tenantScopes or userScopes
  • 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 iframe instead of declarative
  • storageLocation changes from "EU" to "non-EU"
  • avvRequired changes from false to true

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.

On this page