On this page

6 endpoints.

MethodPathSummary
GET/api/communities/{communityId}/app-store/installationsThe app catalog, decorated with this community's entitlement and prices
POST/api/communities/{communityId}/app-store/installationsInstall 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-previewWhat uninstalling this app would do, without doing it
GET/api/communities/{communityId}/app-store/pricesCatalog price book

entitled 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

NameInRequiredTypeDescription
communityIdpathyesstring (uuid)
currencyquerynostringWhich price list to render. Does not affect entitlement.

Responses

StatusDescriptionBody
200Catalog + installationsobject
403Caller lacks settings:manage

Records 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

NameInRequiredTypeDescription
communityIdpathyesstring (uuid)

Request body (required) — application/json

FieldTypeRequiredDescription
appKeystringyes
billingCycleenum: MONTHLY, YEARLY, FREEyesMust be offered by the app's published price.
currencystringno

Responses

StatusDescriptionBody
200Idempotent no-op — already installed on the same billing cycle, or already granted permanently and free (LEGACY_GRANDFATHERED).
201Installed (or reinstalled)object
400APP_STORE_INVALID_INPUT, or APP_STORE_BILLING_CYCLE_UNAVAILABLE
403Caller lacks settings:manage or app_store:manage
404APP_STORE_UNKNOWN_APP — appKey is not in the catalog
409APP_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).

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

NameInRequiredTypeDescription
communityIdpathyesstring (uuid)
appKeypathyesstring
acknowledgeDataRetainedquerynoenum: trueConfirms 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

StatusDescriptionBody
200Uninstalledobject
403Caller lacks settings:manage or app_store:manage
404APP_STORE_UNKNOWN_APP, or APP_STORE_NOT_INSTALLED
409APP_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).

"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

NameInRequiredTypeDescription
communityIdpathyesstring (uuid)
appKeypathyesstring

Responses

StatusDescriptionBody
200The app
404APP_STORE_UNKNOWN_APP

Answers 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

NameInRequiredTypeDescription
communityIdpathyesstring (uuid)
appKeypathyesstring

Responses

StatusDescriptionBody
200The projected outcomeobject
404APP_STORE_UNKNOWN_APP, or APP_STORE_NOT_INSTALLED

What 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

NameInRequiredTypeDescription
communityIdpathyesstring (uuid)
currencyquerynostring

Responses

StatusDescriptionBody
200One entry per catalog appobject