CheckCourt
App platform

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:

  1. The producing app declares in its manifest what it offers (shares, emits), with a description and data categories.
  2. The consuming app declares what it wants (reads, subscribes), naming the producer by its app slug.
  3. 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

Producer: manifest.json (excerpt)
{
  "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
      }
    }
  ]
}
Consumer: manifest.json (excerpt)
{
  "installTargets": ["tenant"],
  "tenantScopes": ["bookings:read"],
  "reads": {
    "metadata": [{ "app": "wingfield", "key": "video_url", "object": "booking" }]
  },
  "subscribes": [{ "app": "wingfield", "event": "match_recorded" }]
}
FieldEntriesFields of an entry
shares.metadataup to 20key, object (booking, court, member), description (1 to 200 characters), data_categories (1 to 10)
reads.metadataup to 50app (producer slug), key, object
emitsup to 20name, description, data_categories, optional schema
subscribesup to 50app (producer slug), event (the producer's name)

Rules:

  • All four fields are allowed only with "tenant" in installTargets.
  • Keys and event names match ^[a-z][a-z0-9_]{0,39}$. Each object and key pair, each event name and each reads or subscribes entry may appear only once.
  • Sharing or reading metadata on an object type needs its read scope in tenantScopes: bookings:read for booking, courts:read for court, members:read for member.
  • schema is a flat JSON Schema subset like settingsSchema: type: "object", up to 30 properties of type string (enum or maxLength), number or integer (minimum, maximum) and boolean, plus required and additionalProperties. Each property may have a description.
  • 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 …/metadata no 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" }'
  • value is any JSON value except null, at most 4096 bytes serialized.
  • Only keys your manifest declares under shares.metadata for that object type (403 otherwise).
  • 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" }
  • name must be declared under emits.
  • data is a JSON object, at most 4096 bytes serialized, validated against schema if you declared one. It defaults to {}.
  • subject is optional: a booking, court or member the event is about. It needs the read scope of that object type and becomes object in the delivery. Without it, object is your own installation (app_installation).
  • At most 60 events per minute per installation; beyond that the answer is 429 with Retry-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 subscribes and the producer under emits,
  • 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 dataProcessing and 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.

On this page