> ## Documentation Index
> Fetch the complete documentation index at: https://docs.launchmystore.io/llms.txt
> Use this file to discover all available pages before exploring further.

# App Installations

> List, configure, and uninstall app installations on a merchant store

# App Installations

An installation represents one app installed on one merchant
store. It carries the OAuth token pair, granted scopes, billing state,
and per-merchant configuration. This page documents the merchant-facing
endpoints — for OAuth grant flow, see
[Authorize](/api-reference/oauth/authorize); for per-install rollback,
see [Rollback](/api-reference/apps/rollback).

All endpoints in this group require merchant (or staff-admin) JWT auth
and scope by the caller's `storeId`.

## Endpoints

| Method  | Path                                           | Purpose                                             |
| ------- | ---------------------------------------------- | --------------------------------------------------- |
| `GET`   | `/apps/store/installed`                        | List all installations for the caller's store.      |
| `POST`  | `/apps/store/install/:appId`                   | Install an app (merchant-initiated, no OAuth flow). |
| `POST`  | `/apps/store/uninstall/:appId`                 | Uninstall an app (cascade cleanup + webhook).       |
| `PATCH` | `/apps/store/:installationId/config`           | Update the install's `config` blob.                 |
| `GET`   | `/apps/installations/:installationId/settings` | Read merchant-configured app settings.              |
| `PUT`   | `/apps/installations/:installationId/settings` | Replace merchant-configured app settings.           |

## Installation object

```ts theme={null}
interface Installation {
  installationId: string;             // UUID
  appId: string;                      // the app
  storeId: string;                    // the merchant store

  // Status
  status: 'active' | 'disabled' | 'pending-uninstall';

  // OAuth tokens (cleared on revoke/uninstall)
  accessToken: string | null;
  refreshToken: string | null;
  tokenExpiresAt: Date | null;        // 24h after issue
  refreshTokenExpiresAt: Date | null; // 30d after issue
  grantedScopes: string[];            // e.g. ["read_products", "write_metafields"]

  // Per-install configuration
  config: Record<string, any>;        // dev-defined, set on install
  enabledEmbeds: Record<string, any>; // merchant toggles for app embeds
  enabledBlocks: Record<string, any>; // merchant toggles for theme blocks
  scriptData: Record<string, any>;
  settings: Record<string, any>;      // merchant-edited admin settings

  // Version pinning (see Rollback)
  installedVersion: string | null;    // semver
  autoUpdate: boolean;                // default true
  pinnedVersion: string | null;       // non-null = autoUpdate suspended

  // Billing (Stripe)
  billingStatus: 'free' | 'trial' | 'active' | 'cancelled' | 'past_due';
  billingStartDate: Date | null;
  trialEndsAt: Date | null;
  stripeSubscriptionId: string | null;
  stripeCustomerId: string | null;

  createdAt: Date;
  updatedAt: Date;
}
```

A single app can have at most one installation per merchant.

## List Installed Apps

### `GET /apps/store/installed`

Returns every installation for the caller's store, excluding rows in
`pending-uninstall`. Each row is joined with the parent `App` record so
the merchant admin can render name/icon/description without a second
fetch.

```bash theme={null}
curl -X GET "https://api.launchmystore.io/apps/store/installed" \
  -H "Authorization: Bearer MERCHANT_JWT"
```

Response:

```json theme={null}
{
  "status": 200,
  "state": "success",
  "data": [
    {
      "installationId": "inst_abc123",
      "appId": "lms_app_foundry_reviews",
      "status": "active",
      "installedVersion": "1.2.0",
      "pinnedVersion": null,
      "autoUpdate": true,
      "grantedScopes": ["read_products", "write_metafields", "read_orders"],
      "billingStatus": "active",
      "trialEndsAt": null,
      "createdAt": "2026-03-12T09:00:00.000Z",
      "updatedAt": "2026-05-16T12:34:00.000Z",
      "app": {
        "appId": "lms_app_foundry_reviews",
        "name": "Foundry Reviews",
        "iconUrl": "https://cdn.launchmystore.io/apps/foundry-reviews/icon.png",
        "developer": "Foundry Apps"
      }
    }
  ]
}
```

## Install App

### `POST /apps/store/install/:appId`

Direct install path used by the merchant admin "Install" button. **Does
not** go through the OAuth `code → token` exchange — the merchant is
already authenticated as themselves, and the platform issues an
installation token bound to their `storeId` directly.

```bash theme={null}
curl -X POST "https://api.launchmystore.io/apps/store/install/lms_app_foundry_reviews" \
  -H "Authorization: Bearer MERCHANT_JWT" \
  -H "Content-Type: application/json" \
  -d '{ "config": { "...": "..." } }'
```

<Note>
  Path parameter `appId` is the app's **UUID**, not the
  app handle. Looking up an app by handle via the marketplace endpoints
  returns both `appId` and `handle` — pass the `appId` here. The
  marketplace `?search=` filter matches on `name` (case-insensitive), not
  `handle`, so searching for `foundry-reviews` will not find an app named
  "Foundry Reviews". Search by the first word of the name instead.
</Note>

On success, the installation is created with `status = 'active'`
and the app's storefront extensions (blocks/snippets/assets) are
deployed under `extensions/{domainSlug}/{appHandle}/` so the
storefront theme can render them.

Error codes:

| HTTP  | Message                                                | When                                                          |
| ----- | ------------------------------------------------------ | ------------------------------------------------------------- |
| `404` | `App not found`                                        | `appId` doesn't exist.                                        |
| `400` | `App is not published`                                 | App in `draft` / `pending-review` / `rejected`.               |
| `409` | `App already installed`                                | An active installation already exists for `(appId, storeId)`. |
| `409` | `Per-shop function limit exceeded for <type>: max <N>` | Function caps prevent a new active install.                   |

## Uninstall App

### `POST /apps/store/uninstall/:appId`

Atomically cleans up everything related to this installation:

1. Cancels the Stripe subscription (`stripeSubscriptionId`), if any.
2. Deletes the installation.
3. Deletes related webhook registrations, billing transaction
   records, GDPR acknowledgments, function logs, etc.
4. Removes the merchant-scoped extension files staged for this app on
   the storefront.
5. Dispatches the `app/uninstalled` webhook to the app's
   `webhookCallbackUrl`.

All server-side cleanup runs in a single atomic transaction — a partial
failure rolls back to the pre-uninstall state. Stripe cancellation runs
**first** (remote idempotent call) so a successful Stripe cancel
followed by a DB rollback can be safely re-driven on retry.

```bash theme={null}
curl -X POST "https://api.launchmystore.io/apps/store/uninstall/lms_app_foundry_reviews" \
  -H "Authorization: Bearer MERCHANT_JWT"
```

Response:

```json theme={null}
{
  "status": 200,
  "state": "success",
  "message": "App uninstalled successfully",
  "data": {
    "appId": "lms_app_foundry_reviews",
    "uninstalledAt": "2026-05-16T12:34:56.000Z"
  }
}
```

| HTTP  | Message                  | When                                       |
| ----- | ------------------------ | ------------------------------------------ |
| `404` | `Installation not found` | No active install for `(appId, storeId)`.  |
| `500` | (generic)                | DB transaction rolled back; safe to retry. |

## Update Installation Config

### `PATCH /apps/store/:installationId/config`

Used by the merchant admin for per-install settings that the app developer
defined as merchant-editable (e.g. block enable/disable toggles, embed
defaults). Distinct from the `settings` blob — `config` is more raw and
typically driven by the app's installation manifest.

```bash theme={null}
curl -X PATCH "https://api.launchmystore.io/apps/store/inst_abc123/config" \
  -H "Authorization: Bearer MERCHANT_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "config": { "review_layout": "grid", "show_photos": true }
  }'
```

## Settings

### `GET /apps/installations/:installationId/settings`

Returns the merchant-editable `settings` blob.

```json theme={null}
{
  "status": 200,
  "state": "success",
  "data": {
    "settings": {
      "review_layout": "grid",
      "auto_publish": false,
      "moderation_email": "reviews@acme.example"
    }
  }
}
```

### `PUT /apps/installations/:installationId/settings`

Replaces the `settings` blob entirely. There is no PATCH-merge variant —
the caller is expected to send the full settings object every time.

```bash theme={null}
curl -X PUT "https://api.launchmystore.io/apps/installations/inst_abc123/settings" \
  -H "Authorization: Bearer MERCHANT_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "settings": {
      "review_layout": "list",
      "auto_publish": true,
      "moderation_email": "reviews@acme.example"
    }
  }'
```

| HTTP  | Message                  | When                                     |
| ----- | ------------------------ | ---------------------------------------- |
| `404` | `Installation not found` | `(installationId, storeId)` not matched. |
| `400` | (validation)             | `settings` not an object.                |

## Status Reference

| `status`            | Visible to merchant admin      | Tokens valid                      | Extensions on storefront |
| ------------------- | ------------------------------ | --------------------------------- | ------------------------ |
| `active`            | Yes                            | Yes (until expiry)                | Yes                      |
| `disabled`          | Yes (greyed out)               | No (validateToken returns `null`) | No                       |
| `pending-uninstall` | Hidden from `getInstalledApps` | No                                | Removed                  |

`disabled` is set by staff admin when an app has been flagged but not
fully uninstalled (e.g. payment past-due grace period, abuse review).
The OAuth `validateToken` path short-circuits on any non-`active`
status, so a disabled install's API calls 401 immediately even if the
access token hasn't expired.

## Uninstall reasons

The platform records an uninstall reason **only when** the merchant
provides one via the admin UI confirmation dialog. There is currently
no REST surface to read uninstall reasons after the fact — the row is
deleted, not soft-deleted. Developers should subscribe to the
`app/uninstalled` webhook to capture the reason in their own systems at
the moment of uninstall.

<Note>
  Per-installation analytics (events, function execution counts, billing
  transactions) are queryable through the developer dashboard endpoints
  under `/apps/developer/:appId/...`. See
  [Versions](/api-reference/apps/versions) for the per-version install
  breakdown and
  [Webhook Delivery Logs](/api-reference/webhooks/delivery-logs) for the
  per-installation webhook history.
</Note>
