> ## 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 Lifecycle

> Install, OAuth, and uninstall — every event your app receives during its life on a merchant store

# App Lifecycle

Every app on LaunchMyStore moves through the same lifecycle: a merchant **discovers** it in the marketplace, **installs** it (granting OAuth scopes), **uses** it (your code runs in their store), and eventually **uninstalls** it. This page is a single reference for every event your app will receive at each stage and what your handlers should do.

## Lifecycle States

```
   ┌──────────────┐                  ┌──────────────┐
   │ Marketplace  │  install         │              │
   │   Listing    │ ───────────────▶ │  installed   │
   └──────────────┘                  │              │
                                     └──────┬───────┘
                                            │
                       ┌──────── publish ───┤
                       │     new version     │
                       ▼                     │
                  ┌─────────┐                │
                  │ updated │                │
                  └────┬────┘                │
                       │                     │
                       └─────────────────────┘
                                            │
                                            │ uninstall
                                            ▼
                                     ┌──────────────┐
                                     │ uninstalled  │
                                     └──────────────┘
```

| State            | App can call APIs? | Webhooks delivered?          | Extensions render? |
| ---------------- | ------------------ | ---------------------------- | ------------------ |
| **Listing only** | No                 | No                           | No                 |
| **Installed**    | Yes                | Yes                          | Yes                |
| **Uninstalled**  | No (revoked)       | Final `app/uninstalled` only | No                 |

## 1. Discovery & Install

Merchants discover apps in the marketplace and click **Install**. This kicks off the OAuth flow.

### OAuth flow (high level)

```
Merchant clicks "Install"
        ↓
Browser → /apps/oauth/authorize?client_id=...&redirect_uri=...&scopes=...&state=...
        ↓
Merchant approves scopes
        ↓
Browser → <redirect_uri>?code=...&state=...
        ↓
Your app: POST /apps/oauth/token { grant_type: 'authorization_code', code, ... }
        ↓
Response: { access_token, refresh_token, expires_in, scopes }
```

See [Authentication](/getting-started/authentication) for the full code walkthrough.

### Install rejection — per-shop function caps

If your app declares one or more functions in `app.json` and the merchant's store already has the maximum number of active apps shipping that function type, both install paths reject the install with **HTTP 409 Conflict**:

* Direct merchant install: `POST /apps/:appId/install`
* OAuth code exchange: `POST /apps/oauth/token` with `grant_type=authorization_code`

Response shape:

```json theme={null}
{
  "status": 409,
  "state": "error",
  "message": "Cannot install: this store already has 1 active cart_transform function, and the per-shop limit is 1. Uninstall another cart_transform app before installing this one."
}
```

Your installer / OAuth callback should surface the `message` field directly — it names the offending function type and the current cap so the merchant knows what to uninstall. See [Functions › Active-Install Caps Per Shop](/functions/overview#active-install-caps-per-shop) for the per-type limits.

<Note>
  Re-issuing tokens for an already-active installation never triggers the cap check — only **new** activations are counted. A previously-uninstalled app that the merchant is re-installing also passes the check as long as the cap still has headroom.
</Note>

### `app/installed` webhook

Fired immediately after the merchant completes OAuth. Use this to:

* Provision merchant-side resources (DB row, default settings)
* Send a welcome email
* Sync initial data via the granted scopes

```json theme={null}
{
  "topic": "app/installed",
  "createdAt": "2026-05-06T12:00:00Z",
  "domainSlug": "merchant-store",
  "merchantId": "mer_01H...",
  "appId": "app_01H...",
  "data": {
    "installationId": "inst_01H...",
    "version": "1.5.0",
    "scopes": ["read_products", "write_orders"],
    "installedAt": "2026-05-06T12:00:00Z"
  }
}
```

<Warning>
  The `app/installed` webhook fires **after** the OAuth redirect — your redirect handler should not assume any merchant-side state has been created yet. Either bootstrap your DB row inside the OAuth handler (synchronous, blocks the redirect) or render a "Setting up..." page that polls until the webhook completes.
</Warning>

## 2. Active Use

Once installed, your app can:

* Call any `/api/v1/*` endpoint with the access token (subject to granted scopes)
* Receive any webhook the merchant has subscribed (or that you registered at install time)
* Have its extensions rendered into storefronts, checkouts, and admin pages
* Have its functions executed during cart verification and order placement flows

### Token refresh

Access tokens expire after **24 hours**. Refresh tokens last **30 days**. Always implement refresh:

```javascript theme={null}
async function getValidToken(installation) {
  if (Date.now() < installation.expiresAt - 60_000) {
    return installation.accessToken;
  }
  const r = await fetch('https://api.launchmystore.io/apps/oauth/token', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'refresh_token',
      client_id: process.env.LMS_CLIENT_ID,
      client_secret: process.env.LMS_CLIENT_SECRET,
      refresh_token: installation.refreshToken
    })
  });
  const tokens = await r.json();
  await storeTokens(installation.id, tokens);
  return tokens.access_token;
}
```

If the refresh token has also expired (30+ days of inactivity), the merchant must re-authorize.

## 3. Uninstall

Merchants can uninstall an app at any time from their admin. When they do:

1. **OAuth tokens are revoked** immediately — your app cannot make any further API calls
2. **Extensions are unmounted** — blocks/checkout/admin extensions stop rendering
3. **`app/uninstalled` webhook fires** (this is the only webhook your app receives after uninstall)
4. **Webhook subscriptions are cleared**

### `app/uninstalled` webhook

```json theme={null}
{
  "topic": "app/uninstalled",
  "createdAt": "2026-05-06T12:00:00Z",
  "data": {
    "installationId": "inst_01H...",
    "merchantId": "mer_01H...",
    "uninstalledAt": "2026-05-06T12:00:00Z",
    "uninstallReason": "merchant_initiated"
  }
}
```

Use this to:

* **Clean up merchant data** (or schedule it — see GDPR below)
* Cancel any active subscriptions / usage charges
* Send an "we'd love your feedback" email
* Update internal analytics

<Warning>
  `app/uninstalled` is delivered with a 3-retry exponential backoff (1m / 5m / 15m). If your endpoint is down for the full retry window, the webhook is dropped — but the uninstall still happens. Always reconcile with the [installations list endpoint](/api-reference/overview) on a daily cron rather than relying on the webhook alone.
</Warning>

## 4. GDPR Lifecycle Webhooks

Three additional webhooks satisfy GDPR / CCPA obligations. Subscribing to these is **mandatory** for any app published to the public marketplace.

### `customers/data_request`

Fires when a merchant or customer requests a data export.

```json theme={null}
{
  "topic": "customers/data_request",
  "data": {
    "shopDomain": "merchant.example.com",
    "customerId": 1234567,
    "customerEmail": "shopper@example.com",
    "ordersRequested": [9876, 9877]
  }
}
```

Within **30 days**, you must compile every piece of customer data your app stores and email it to the merchant (the merchant then forwards to the customer).

### `customers/redact`

Fires when a customer exercises their right to be forgotten, **48 hours after** the merchant approves the request.

```json theme={null}
{
  "topic": "customers/redact",
  "data": {
    "shopDomain": "merchant.example.com",
    "customerId": 1234567,
    "customerEmail": "shopper@example.com",
    "ordersToRedact": [9876, 9877]
  }
}
```

You must permanently delete or anonymize all data tied to this customer within **30 days** of receiving this webhook. See [GDPR Reference](/api-reference/gdpr) for the audit trail format.

### `shop/redact`

Fires **48 hours after** an app is uninstalled (i.e. 48h after `app/uninstalled`).

```json theme={null}
{
  "topic": "shop/redact",
  "data": {
    "shopDomain": "merchant.example.com",
    "shopId": 12345,
    "uninstalledAt": "2026-05-04T12:00:00Z"
  }
}
```

You must permanently delete all merchant-tied data within 30 days. The 48-hour delay gives merchants a window to **reinstall** if they uninstalled by accident — during that window, your data should remain intact.

## Webhook Delivery Guarantees

* **Signature**: HMAC-SHA256 of the raw request body, signed with your `clientSecret`, sent in the `X-LMS-Hmac-SHA256` header. **Always verify before processing.**
* **Retries**: 3 attempts with exponential backoff (1m, 5m, 15m). After the 3rd failure, the event is dropped.
* **Ordering**: Best-effort, not guaranteed. Use `createdAt` to sequence; use idempotency keys (e.g. `installationId + topic + createdAt`) to dedupe.
* **At-least-once**: A successful response (2xx) acks the event. Missing the ack causes a retry — your handler must be idempotent.

See [Webhook Verification](/api-reference/webhooks/verification) for the HMAC signature code.

## Testing Webhooks Locally

Waiting for a real merchant install, order, or GDPR request just to verify a handler works is painful — and some events (like `customers/redact`) are nearly impossible to reproduce on demand. The `lms webhook trigger` CLI command generates a sample payload for any topic, signs it with your app's `clientSecret`, and POSTs it to a local URL so you can exercise your handler end-to-end.

### Install the CLI

The command ships with the LaunchMyStore CLI. If you haven't set it up yet, see [CLI Setup](/getting-started/cli-setup). Once configured, your `clientSecret` is read from `LMS_CLIENT_SECRET` or `.lmsrc.json`, so most invocations need only the topic.

### Common scenarios

<CodeGroup>
  ```bash orders/create theme={null}
  # Fire a sample order at your local handler
  lms webhook trigger orders/create

  # Override the URL if your handler isn't on the default port
  lms webhook trigger orders/create --url http://localhost:4000/api/webhooks
  ```

  ```bash customers/redact (GDPR) theme={null}
  # Verify your data-deletion handler runs without waiting 48h
  # for a real merchant-approved redact request
  lms webhook trigger customers/redact
  ```

  ```bash custom payload theme={null}
  # Reproduce a specific event from a captured production payload
  lms webhook trigger orders/paid --payload-file ./fixtures/order-12345.json

  # Or pass JSON inline
  lms webhook trigger products/create --payload '{"id":"prd_test","title":"Demo"}'
  ```
</CodeGroup>

### Headers it sends

Each request mirrors what production webhook delivery sends, so the same verification code path runs in dev:

| Header                   | Value                                                               |
| ------------------------ | ------------------------------------------------------------------- |
| `Content-Type`           | `application/json`                                                  |
| `X-LMS-Topic`            | The topic you passed (e.g. `orders/create`)                         |
| `X-LMS-Webhook-Id`       | Random UUID — useful for dedupe testing                             |
| `X-LMS-Delivery-Attempt` | `1` (bump manually if you want to test retry behavior)              |
| `X-LMS-Hmac-SHA256`      | Base64 HMAC-SHA256 of the raw body, signed with your `clientSecret` |

Your handler should verify `X-LMS-Hmac-SHA256` against the raw request body before doing anything else. See [Webhook Verification](/api-reference/webhooks/verification) for the exact comparison code.

<Tip>
  Pair `lms webhook trigger` with a local tunnel (e.g. ngrok) to test handlers that need to call back into the LaunchMyStore API. The signature is computed against your real `clientSecret`, so production-grade verification works either way.
</Tip>

## Lifecycle Checklist

Before you publish your app to the marketplace, your handler must:

<Steps>
  <Step title="Handle OAuth callback (`/oauth/callback`)">
    Exchange code for tokens, create your installation row, redirect merchant to your dashboard.
  </Step>

  <Step title="Handle `app/installed` webhook">
    Idempotent provisioning — running it twice should not duplicate data.
  </Step>

  <Step title="Refresh tokens before they expire">
    Background job or just-in-time refresh inside your API client.
  </Step>

  <Step title="Handle `app/uninstalled`">
    Mark installation as inactive, schedule data retention timer.
  </Step>

  <Step title="Handle `customers/data_request`, `customers/redact`, `shop/redact`">
    Required for marketplace approval — compile/delete data within 30 days.
  </Step>
</Steps>

## Related

* [Authentication](/getting-started/authentication) — OAuth flow code walkthrough
* [App Versioning](/getting-started/app-versioning) — draft/publish/deprecate flow + per-install rollback
* [GDPR Reference](/api-reference/gdpr) — full payloads + retention windows
* [Webhooks Overview](/api-reference/webhooks/overview) — delivery guarantees + topic list
