App Installations
curl --request GET \
--url https://api.launchmystore.io/apps/store/installed \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.launchmystore.io/apps/store/installed"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.launchmystore.io/apps/store/installed', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.launchmystore.io/apps/store/installed",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.launchmystore.io/apps/store/installed"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.launchmystore.io/apps/store/installed")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.launchmystore.io/apps/store/installed")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_bodyApps
App Installations
List, configure, and uninstall app installations on a merchant store
GET
/
apps
/
store
/
installed
App Installations
curl --request GET \
--url https://api.launchmystore.io/apps/store/installed \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.launchmystore.io/apps/store/installed"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.launchmystore.io/apps/store/installed', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.launchmystore.io/apps/store/installed",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.launchmystore.io/apps/store/installed"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.launchmystore.io/apps/store/installed")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.launchmystore.io/apps/store/installed")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_bodyApp 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; for per-install rollback, see Rollback. All endpoints in this group require merchant (or staff-admin) JWT auth and scope by the caller’sstoreId.
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
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;
}
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.
curl -X GET "https://api.launchmystore.io/apps/store/installed" \
-H "Authorization: Bearer MERCHANT_JWT"
{
"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. The
installation itself is created immediately (no consent screen — the
merchant is already authenticated as themselves), but apps with an
external appUrl do still receive the OAuth code → token
handoff: the response includes an appLaunchUrl of the form
https://{your appUrl}/auth?shop=...&storeId=...&code=...&state=...&host=...×tamp=...&hmac=...
/auth endpoint should verify the hmac (HMAC-SHA256 of
the query string minus the hmac field, keyed with your client secret),
then exchange the code at POST /apps/oauth/token with
grant_type=authorization_code to obtain your access + refresh tokens.
The code is single-use and expires after 10 minutes. Re-installing an
app re-issues a fresh appLaunchUrl, so an app that lost its tokens can
recover by uninstall → reinstall. Apps with a platform-local appUrl
(first-party embeds) get appLaunchUrl: null and skip this step.
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": { "...": "..." } }'
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.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 — also returned for a private app when the caller is not the developer’s own store. |
400 | App is not available for installation | Public app not yet published (installing your own draft as its developer is allowed). |
403 | Premium apps are available only on Gold and Platinum plans. ... | isPremium app on a Trial/Starter merchant plan. |
400 | App is already installed | An active installation already exists for (appId, storeId). |
409 | Cannot install: this store already has <n> active <type> function(s), and the per-shop limit is <N>. ... | Function caps prevent a new active install. |
Paid apps without a free trial don’t create the installation row
immediately: the install call returns
{ requiresPayment: true, checkoutUrl, sessionId } and the row is created when Stripe confirms
payment. The /auth handoff for these installs fires after checkout
completes — the admin redeems it via GET /apps/store/launch-url/:appId
when the merchant lands back on ?billing=success.Uninstall App
POST /apps/store/uninstall/:appId
Atomically cleans up everything related to this installation:
- Cancels the Stripe subscription (
stripeSubscriptionId), if any. - Deletes the app’s webhook subscriptions, admin-extension registry rows, fulfillment services, sales channels, app-owned metafield definitions, and automation flows for this store.
- Deletes the installation row (this also clears the OAuth token pair).
- After commit: removes the merchant-scoped extension files staged for this app on the storefront, and wipes compiled function artefacts once the app’s last installation is gone.
- Dispatches the
app/uninstalledwebhook.
curl -X POST "https://api.launchmystore.io/apps/store/uninstall/lms_app_foundry_reviews" \
-H "Authorization: Bearer MERCHANT_JWT"
{
"status": 200,
"state": "success",
"message": "App uninstalled successfully"
}
data field on the uninstall response.
| 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.
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.
{
"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.
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 — and theapp/uninstalled webhook payload
carries only { appId } (no reason field). If you want exit feedback,
collect it in your own off-boarding flow when the webhook arrives.
Per-installation analytics (events, function execution counts, billing
transactions) are queryable through the developer dashboard endpoints
under
/apps/developer/:appId/.... See
Versions for the per-version install
breakdown and
Webhook Delivery Logs for the
per-installation webhook history.