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.Install rejection — per-shop function caps
If your app declares one or more functions inapp.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/tokenwithgrant_type=authorization_code
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
X-LMS-Topic header, the delivery id in
X-LMS-Webhook-Id, and the shop host in X-LMS-Shop-Domain:
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:3. Uninstall
Merchants can uninstall an app at any time from their admin. When they do:- OAuth tokens are revoked immediately — your app cannot make any further API calls
- Extensions are unmounted — blocks/checkout/admin extensions stop rendering
app/uninstalledwebhook fires (this is the only webhook your app receives after uninstall)- Webhook subscriptions are cleared
app/uninstalled webhook
Body is the payload itself; the topic is in the X-LMS-Topic header:
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
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.
customers/redact
Fires when a customer-redaction request is submitted (merchant/staff
create it via the GDPR endpoints — POST /apps/gdpr/customer-redact).
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.
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-SHA256header. The signing secret depends on how the webhook was registered: API-registered subscriptions (POST /api/v1/webhooks.json) are signed with the per-subscriptionsecretreturned once in the create response; manifest-declared (extensions.webhooks[]) and GDPR webhooks are signed with yourclientSecret. 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
createdAtto 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.
Testing Webhooks Locally
Waiting for a real merchant install, order, or GDPR request just to verify a handler works is painful — and some events (likecustomers/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, yourclientSecret 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.
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.
Related
- Authentication — OAuth flow code walkthrough
- App Versioning — draft/publish/deprecate flow + per-install rollback
- GDPR Reference — full payloads + retention windows
- Webhooks Overview — delivery guarantees + topic list