6 endpoints.
| Method | Path | Summary |
|---|---|---|
GET | /api/communities/{communityId}/app-store/installations | The app catalog, decorated with this community's entitlement and prices |
POST | /api/communities/{communityId}/app-store/installations | Install an app for this community |
DELETE | /api/communities/{communityId}/app-store/installations/{appKey} | Uninstall an app — removes ACCESS, never DATA |
GET | /api/communities/{communityId}/app-store/installations/{appKey} | One app's catalog entry, entitlement, installation and price |
GET | /api/communities/{communityId}/app-store/installations/{appKey}/uninstall-preview | What uninstalling this app would do, without doing it |
GET | /api/communities/{communityId}/app-store/prices | Catalog price book |
GET /api/communities/{communityId}/app-store/installationsentitled comes from the same resolver the route guards use, so the store can never disagree with what the runtime enforces. origin explains that value (override / installation / plan / none).
Auth: Bearer JWT required.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
communityId | path | yes | string (uuid) | — |
currency | query | no | string | Which price list to render. Does not affect entitlement. |
Responses
| Status | Description | Body |
|---|---|---|
200 | Catalog + installations | object |
403 | Caller lacks settings:manage | — |
POST /api/communities/{communityId}/app-store/installationsRecords an INTENT and a PRICE. No payment provider is called in this phase — nothing is charged. The catalog price is snapshotted onto the installation row at this instant and is never re-read, so a later catalog edit cannot re-price this community.
Reinstalling an app that was previously uninstalled UPDATES the existing row (the (communityId, appKey) pair is unique) and is lossless: no tenant data was ever removed, so everything the community created comes back.
Requires BOTH settings:manage and app_store:manage. The second exists because settings:manage is a moderator default and installing an app is a spending decision.
Auth: Bearer JWT required.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
communityId | path | yes | string (uuid) | — |
Request body (required) — application/json
| Field | Type | Required | Description |
|---|---|---|---|
appKey | string | yes | — |
billingCycle | enum: MONTHLY, YEARLY, FREE | yes | Must be offered by the app's published price. |
currency | string | no | — |
Responses
| Status | Description | Body |
|---|---|---|
200 | Idempotent no-op — already installed on the same billing cycle, or already granted permanently and free (LEGACY_GRANDFATHERED). | — |
201 | Installed (or reinstalled) | object |
400 | APP_STORE_INVALID_INPUT, or APP_STORE_BILLING_CYCLE_UNAVAILABLE | — |
403 | Caller lacks settings:manage or app_store:manage | — |
404 | APP_STORE_UNKNOWN_APP — appKey is not in the catalog | — |
409 | APP_STORE_ALREADY_INSTALLED (installed on a different cycle; uninstall first), or APP_STORE_PRICE_UNAVAILABLE (no published price — the app is not purchasable, and installing at an unknown price would bill the customer an amount nobody quoted them). | — |
DELETE /api/communities/{communityId}/app-store/installations/{appKey}No tenant table is dropped, truncated or deleted from. The installation row is kept and marked UNINSTALLED, so a later install restores access to everything the community created.
Uninstalling does not always end access. An installation row can only ADD entitlement, so removing it drops access only where the installation was the sole source. Where the community's PLAN also includes the app, access continues and the response says so in accessEnded: false / stillEntitledVia: "plan". Show that to the user — otherwise they uninstall, see the app still working, and open a bug.
When the app owns measurable tenant content, the first call is refused with 409 APP_STORE_UNINSTALL_NEEDS_ACK and a preview payload stating how many rows are retained. Repeat with acknowledgeDataRetained=true to proceed. This is a disclosure gate, not a safety gate — nothing is ever deleted.
Auth: Bearer JWT required.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
communityId | path | yes | string (uuid) | — |
appKey | path | yes | string | — |
acknowledgeDataRetained | query | no | enum: true | Confirms the caller has been told content is retained. Accepted in the query string as well as the body, because DELETE-with-body is unreliable in some HTTP clients. |
Responses
| Status | Description | Body |
|---|---|---|
200 | Uninstalled | object |
403 | Caller lacks settings:manage or app_store:manage | — |
404 | APP_STORE_UNKNOWN_APP, or APP_STORE_NOT_INSTALLED | — |
409 | APP_STORE_UNINSTALL_NEEDS_ACK (retry with acknowledgeDataRetained=true; the body carries a preview), or APP_STORE_GRANDFATHERED_IMMUTABLE (the app was granted permanently and free of charge when this community was migrated to the App Store — it costs nothing and cannot be uninstalled). | — |
GET /api/communities/{communityId}/app-store/installations/{appKey}"Not installed" is a state, not an error: it returns 200 with installation: null. Only an appKey absent from the CATALOG is a 404.
Auth: Bearer JWT required.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
communityId | path | yes | string (uuid) | — |
appKey | path | yes | string | — |
Responses
| Status | Description | Body |
|---|---|---|
200 | The app | — |
404 | APP_STORE_UNKNOWN_APP | — |
GET /api/communities/{communityId}/app-store/installations/{appKey}/uninstall-previewAnswers the two questions a confirmation dialog should be asking on the user's behalf: does access actually end, and how much content is retained. blocked: true means the app cannot be uninstalled at all (a permanent, free grandfathered grant).
Auth: Bearer JWT required.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
communityId | path | yes | string (uuid) | — |
appKey | path | yes | string | — |
Responses
| Status | Description | Body |
|---|---|---|
200 | The projected outcome | object |
404 | APP_STORE_UNKNOWN_APP, or APP_STORE_NOT_INSTALLED | — |
GET /api/communities/{communityId}/app-store/pricesWhat each app costs today, before anyone installs it. Every catalog app appears — an app with no published price returns availableCycles: [], which means "not for sale", NOT "free". Omitting unpriced apps would make "we have not priced this yet" indistinguishable from "this app does not exist".
Prices here are the CATALOG. An existing installation keeps the price snapshotted at install time, so editing the catalog never re-prices anyone who already bought.
Read-only and non-sensitive, so it takes the viewing permission (SETTINGS_MANAGE), not the spending one (APP_STORE_MANAGE) — an admin deciding whether to delegate installs still needs to see the prices.
Auth: Bearer JWT required.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
communityId | path | yes | string (uuid) | — |
currency | query | no | string | — |
Responses
| Status | Description | Body |
|---|---|---|
200 | One entry per catalog app | object |