Skip to main content

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

1. Discovery & Install

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

Install flow (high level)

Apps receive their first token through the managed install handoff — there is no app-initiated browser redirect to /apps/oauth/authorize (that endpoint is a merchant-authenticated JSON API used by the admin):
data.scope is a single space-delimited string of granted scopes. See Authentication and Install handoff for the full code walkthrough.

How the admin embeds your app

Once installed, opening the app in the admin loads your app’s root in an iframe. Three details trip up most first integrations:
1

Serve your entry point at /

The admin embeds the app root — it has no way to know which of your pages is the entry point. If / is unrouted, merchants see your framework’s bare 404 (Cannot GET /) inside the iframe. Redirect / to your home page and preserve the query string — it carries everything below.
2

Sub-pages load as {appUrl}/<path>, without any prefix

Navigating to an admin sub-path requests {appUrl}/settings, not {appUrl}/admin/settings. If your pages live under a prefix, serve or redirect the bare paths too.
3

One merchant, three identifiers — register all of them

shop is the full storefront host (acme.launchmystore.io, or the merchant’s custom domain). storeId is the immutable UUID — prefer it for keying per-merchant state, since shop changes when a merchant moves to a custom domain. domainSlug is the canonical slug the App Proxy uses.These are not interchangeable, and different surfaces send different ones: this handoff carries all three, while a storefront App Proxy request carries only shop=<domainSlug> and never storeId. An app that keys its storage on whichever arrived first splits one merchant into two tenants — buyer-facing writes land in one bucket while the merchant’s admin reads another, with no error to show for it.Store one record at install and alias every identifier to it.
Do not derive the slug with shop.split('.')[0]. On a custom domain that collapses every www.<brand>.com store to the single slug www, so unrelated merchants share one entry — one merchant’s credentials serving another merchant’s store. Use the domainSlug param above.
These query params are a hint, not authentication. The only trustworthy tenant identity is the verified App Bridge session token (Session tokens); treat shop / storeId in the URL as user-supplied.

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/store/install/:appId
  • OAuth code exchange: POST /apps/oauth/token with grant_type=authorization_code
Response shape:
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 for the per-type limits.
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.

app/installed webhook

Fired immediately after the install completes. Use this to:
  • Provision merchant-side resources (DB row, default settings)
  • Send a welcome email
  • Sync initial data via the granted scopes
The request body is the payload itself — there is no envelope. The topic travels in the X-LMS-Topic header, the delivery id in X-LMS-Webhook-Id, and the shop host in X-LMS-Shop-Domain:
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.

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:
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

Body is the payload itself; the topic is in the X-LMS-Topic header:
Cross-reference appId against your own install records (keyed by storeId from the install handoff) — the payload does not carry an uninstall reason or timestamp. 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
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 on a daily cron rather than relying on the webhook alone.

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.
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-redaction request is submitted (merchant/staff create it via the GDPR endpoints — POST /apps/gdpr/customer-redact).
You must permanently delete or anonymize all data tied to this customer within 30 days of receiving this webhook. See GDPR Reference for the audit trail format.

shop/redact

Fires when a shop-redaction request is submitted (POST /apps/gdpr/shop-redact) — typically after an uninstall when the merchant wants their data purged. It is not scheduled automatically after app/uninstalled.
You must permanently delete all merchant-tied data within 30 days. Keep data intact until you actually receive shop/redact — an uninstall alone is not a deletion request, and merchants often reinstall.

Webhook Delivery Guarantees

  • Signature: HMAC-SHA256 (base64) of the raw request body, sent in the X-LMS-Hmac-SHA256 header. The signing secret depends on how the webhook was registered: API-registered subscriptions (POST /api/v1/webhooks.json) are signed with the per-subscription secret returned once in the create response; manifest-declared (extensions.webhooks[]) and GDPR webhooks are signed with your clientSecret. Legacy rows with no secret are delivered unsigned. See Webhook Verification. 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 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. Once configured, your clientSecret is read from LMS_CLIENT_SECRET or .lmsrc.json, so most invocations need only the topic.

Common scenarios

Headers it sends

Each request mirrors what production webhook delivery sends, so the same verification code path runs in dev: Your handler should verify X-LMS-Hmac-SHA256 against the raw request body before doing anything else. See Webhook Verification for the exact comparison code.
Pair lms webhook trigger with a local tunnel (e.g. ngrok) to test handlers that need to call back into the LaunchMyStore API. Configure the CLI with the same signing secret your handler verifies against so the production verification code path runs unchanged in dev.

Lifecycle Checklist

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

Handle OAuth callback (`/oauth/callback`)

Exchange code for tokens, create your installation row, redirect merchant to your dashboard.
2

Handle `app/installed` webhook

Idempotent provisioning — running it twice should not duplicate data.
3

Refresh tokens before they expire

Background job or just-in-time refresh inside your API client.
4

Handle `app/uninstalled`

Mark installation as inactive, schedule data retention timer.
5

Handle `customers/data_request`, `customers/redact`, `shop/redact`

Required for marketplace approval — compile/delete data within 30 days.