Connections between apps
Share values on bookings, courts and members with other apps and publish events to them, with CheckCourt in between and the club's approval
Apps never talk to each other directly. CheckCourt passes data from one club installation to another, logs it and enforces what the club approved. There are two ways to share:
- Shared data on objects: your app attaches a small value to a booking, court or member (for example a video link to a booking). Other apps read it through the API.
- App events: your app publishes an event (for example
door_opened). Other apps receive it through their regular, signed webhook.
Both work in three steps:
- The producing app declares in its manifest what it offers (
shares,emits), with a description and data categories. - The consuming app declares what it wants (
reads,subscribes), naming the producer by its app slug. - The club approves the connection under Admin → Apps → (app) → Verbindungen. Nothing flows before that, and nothing flows after the club revokes it.
Connections exist only between club installations in the same club. Member installations
(installTargets: ["user"]) can neither share nor read, and member tokens (ccu_) receive
403 on every endpoint on this page.
Manifest fields
{
"installTargets": ["tenant"],
"tenantScopes": ["bookings:read", "courts:read"],
"shares": {
"metadata": [
{
"key": "video_url",
"object": "booking",
"description": "Videolink",
"data_categories": ["Videoaufzeichnung"]
}
]
},
"emits": [
{
"name": "match_recorded",
"description": "Ein Spiel wurde aufgezeichnet",
"data_categories": ["Videoaufzeichnung"],
"schema": {
"type": "object",
"properties": {
"court_id": { "type": "integer", "minimum": 1 },
"duration_minutes": { "type": "integer", "minimum": 0 }
},
"required": ["court_id"],
"additionalProperties": false
}
}
]
}{
"installTargets": ["tenant"],
"tenantScopes": ["bookings:read"],
"reads": {
"metadata": [{ "app": "wingfield", "key": "video_url", "object": "booking" }]
},
"subscribes": [{ "app": "wingfield", "event": "match_recorded" }]
}| Field | Entries | Fields of an entry |
|---|---|---|
shares.metadata | up to 20 | key, object (booking, court, member), description (1 to 200 characters), data_categories (1 to 10) |
reads.metadata | up to 50 | app (producer slug), key, object |
emits | up to 20 | name, description, data_categories, optional schema |
subscribes | up to 50 | app (producer slug), event (the producer's name) |
Rules:
- All four fields are allowed only with
"tenant"ininstallTargets. - Keys and event names match
^[a-z][a-z0-9_]{0,39}$. Eachobjectandkeypair, each event name and eachreadsorsubscribesentry may appear only once. - Sharing or reading metadata on an object type needs its read scope in
tenantScopes:bookings:readforbooking,courts:readforcourt,members:readformember. schemais a flat JSON Schema subset likesettingsSchema:type: "object", up to 30propertiesof typestring(enumormaxLength),numberorinteger(minimum,maximum) andboolean, plusrequiredandadditionalProperties. Each property may have adescription.- The app slug is the one shown in the developer portal; it never changes.
- References to apps that are not installed are fine: the connection appears once both apps are installed in a club.
The club sees description and data_categories in German when it approves, so write them in
German and keep them in line with your dataProcessing.
Approval by the club
On the installation page of either app, the club sees under Verbindungen what is asked for, for example "Belegung Demo möchte von Wingfield lesen: Videolink an Buchungen", with the data categories. A person with the permission to manage apps approves or revokes it:
- Per direction and kind. "B reads metadata of A" and "B receives events of A" are approved separately, and so is the opposite direction.
- The approval covers the items shown. If a new version of either app adds a key or an event to the connection, the new item waits until the club approves again (status Erweiterung angefragt). Items that were approved keep flowing meanwhile.
- Revoking stops everything at once:
GET …/metadatano longer returns the producer's values to the consumer, no new events are delivered, and queued deliveries are dropped. - Uninstalling either app deletes its approvals. A reinstallation starts unapproved.
- Approvals and revocations appear in the club's activity log.
Sandbox
In your developer sandbox club, connections between apps of your own developer organization are approved automatically, so you can test both sides without clicking through the admin area. The installation page shows them as Automatisch freigegeben (Sandbox). Revoking still works and also stops the automatic approval. Apps of other organizations and every real club always need an approval.
Shared data on objects
All endpoints need a club installation token (cca_) and the read scope of the object type.
The object must exist in the token's club; otherwise the answer is 404.
Write a value
curl -X PUT "https://app.checkcourt.de/api/v1/bookings/bk_123/metadata/video_url" \
-H "Authorization: Bearer cca_…" \
-H "Content-Type: application/json" \
-d '{ "value": "https://video.example/abc" }'valueis any JSON value exceptnull, at most 4096 bytes serialized.- Only keys your manifest declares under
shares.metadatafor that object type (403otherwise). - Writing a different value notifies connected readers (see below). Writing the same value again does not.
- Paths:
/bookings/{id}/metadata/{key},/courts/{id}/metadata/{key}(numeric court id),/members/{id}/metadata/{key}(the member's user id).
Remove a value
DELETE …/{id}/metadata/{key} removes your own value and answers { "deleted": true }, or
{ "deleted": false } if there was none. It also works for keys a newer version of your
manifest no longer declares, so you can clean up.
Read values
curl "https://app.checkcourt.de/api/v1/bookings/bk_123/metadata" \
-H "Authorization: Bearer cca_…"{
"object": { "type": "booking", "id": "bk_123" },
"metadata": {
"belegung-demo": {
"note": { "value": "Flutlicht", "updated_at": "2026-10-06T17:58:02.000Z" }
},
"wingfield": {
"video_url": { "value": "https://video.example/abc", "updated_at": "2026-10-06T18:02:11.000Z" }
}
}
}The response contains your own values and the values of other apps that you declare under
reads.metadata, that the producer declares under shares.metadata and that the club
approved, grouped by app slug.
When values are deleted
- When the app that wrote them is uninstalled.
- When the court is deleted, or the member leaves the club or their account is anonymized.
- Bookings are not deleted when cancelled; values on a cancelled booking stay until the club's data is deleted.
app.metadata_changed
Readers with an approved connection receive this webhook when a value they declare under
reads.metadata changes or is removed. The payload carries ids only; fetch the value with
GET …/metadata.
{
"id": "evt_…",
"type": "app.metadata_changed",
"created_at": "2026-10-06T18:02:11.000Z",
"tenant_id": "tn_…",
"installation_id": "inst_…",
"object": { "type": "booking", "id": "bk_123" },
"data": {
"object_type": "booking",
"object_id": "bk_123",
"app": "wingfield",
"key": "video_url",
"deleted": false
}
}You don't list app.metadata_changed under events; your reads entry is the subscription.
App events
Publish
curl -X POST "https://app.checkcourt.de/api/v1/app/events" \
-H "Authorization: Bearer cca_…" \
-H "Content-Type: application/json" \
-d '{ "name": "match_recorded", "data": { "court_id": 3 }, "subject": { "type": "court", "id": "3" } }'{ "id": "evt_…", "type": "app.wingfield.match_recorded" }namemust be declared underemits.datais a JSON object, at most 4096 bytes serialized, validated againstschemaif you declared one. It defaults to{}.subjectis optional: a booking, court or member the event is about. It needs the read scope of that object type and becomesobjectin the delivery. Without it,objectis your own installation (app_installation).- At most 60 events per minute per installation; beyond that the answer is
429withRetry-After.
With the SDK (from 0.5.0), putObjectMetadata,
getObjectMetadata, deleteObjectMetadata and publishAppEvent wrap these endpoints:
import { getObjectMetadata, publishAppEvent, putObjectMetadata } from "@checkcourt/sdk";
await putObjectMetadata(client, "booking", "bk_123", "video_url", "https://video.example/abc");
const { metadata } = await getObjectMetadata(client, "booking", "bk_123");
await publishAppEvent(client, {
name: "match_recorded",
data: { court_id: 3 },
subject: { type: "court", id: "3" },
});The answer 200 means the event is in the outbox. Whether anyone receives it depends on the
connections the club approved at delivery time.
Receive
Subscribers receive the event through the regular webhook delivery
of their app, signed with their own webhook secret, with retries. The type is
app.<producer slug>.<name>, also in the CheckCourt-Event-Type header:
{
"id": "evt_…",
"type": "app.wingfield.match_recorded",
"created_at": "2026-10-06T18:05:00.000Z",
"tenant_id": "tn_…",
"installation_id": "inst_…",
"object": { "type": "court", "id": "3" },
"data": { "court_id": 3 }
}An event reaches an installation only if, at fan-out and again before every attempt:
- the subscriber declares it under
subscribesand the producer underemits, - the club approved the connection (or it is auto-approved in your sandbox),
- the subscriber is a running club installation with a webhook URL,
- the event was published after the subscriber was installed.
With the SDK, isAppEvent(event, "wingfield", "match_recorded") narrows a verified event.
Data protection
The club is the controller; every app provider is a processor of the club. A connection lets one processor's data reach another processor, so CheckCourt makes it the club's decision:
- The categories you declare are shown when the club approves. Keep them accurate and covered
by the purpose in your
dataProcessingand your data processing agreement. - CheckCourt stores shared values only as long as described above and logs approvals and revocations.
- Only share what the other app needs. Prefer ids and links over copies of personal data, and never put data in a value or an event that the reading app could not see itself.
- After a revocation or an uninstall, delete what you received through the connection according to the club's instructions, as for your own data. See Data protection.