# OAuth 2.0 for apps: authorization, scopes, and refresh tokens

OAUTH 2.0 FOR APPS: AUTHORIZATION, SCOPES, AND REFRESH TOKENS

LaunchMyStore apps use the OAuth 2.0 authorization code flow to gain scoped
access to a merchant's store. This guide walks you through the full handshake:
building the authorize URL, handling the redirect, exchanging the code for
tokens, and refreshing access before it expires.


THE OVERALL FLOW

 1. Merchant clicks Install on your app's listing.
 2. Their admin redirects to your authorize URL with the app's client_id,
    requested scope, your redirect_uri, and a CSRF state.
 3. Merchant approves the scopes; LaunchMyStore redirects back to your
    redirect_uri with a one-time code and the same state.
 4. Your server exchanges code for an access token (24h lifetime) and a refresh
    token (30 day lifetime).
 5. Your server uses the access token to call /api/v1/* endpoints. When it nears
    expiry, exchange the refresh token for a fresh pair.

Authorization codes themselves are short-lived (10 minutes in Redis) and
one-time use — consume them immediately.


STEP 1: BUILD THE AUTHORIZE URL

https://api.launchmystore.io/apps/oauth/authorize?
  client_id=YOUR_API_KEY&
  scope=read_products,write_products,read_orders&
  redirect_uri=https://app.example.com/oauth/callback&
  state=RANDOM_CSRF_TOKEN&
  shop=MERCHANT_STORE_HANDLE

Always generate a fresh, unguessable state per install (e.g.
crypto.randomBytes(32).toString('hex')) and store it server-side keyed by
session. Verify it matches when the merchant returns — this prevents CSRF.


STEP 2: HANDLE THE REDIRECT

The merchant lands on your redirect_uri with:

https://app.example.com/oauth/callback?
  code=ONE_TIME_CODE&
  state=RANDOM_CSRF_TOKEN&
  shop=merchant-store

Verify state matches what you stored. If not, abort — you're being attacked.


STEP 3: EXCHANGE CODE FOR TOKENS

POST https://api.launchmystore.io/apps/oauth/token
Content-Type: application/json

{
  "grant_type": "authorization_code",
  "client_id": "YOUR_API_KEY",
  "client_secret": "YOUR_CLIENT_SECRET",
  "code": "ONE_TIME_CODE",
  "redirect_uri": "https://app.example.com/oauth/callback"
}

Successful response:

{
  "access_token": "lms_at_...",
  "token_type": "Bearer",
  "expires_in": 86400,
  "refresh_token": "lms_rt_...",
  "scope": "read_products,write_products,read_orders"
}

Store both tokens, keyed by the merchant’s store handle (or merchant id). Store
securely — they grant the same access as a logged-in merchant within the
requested scopes.


STEP 4: CALL THE API

fetch('https://api.launchmystore.io/api/v1/products', {
  headers: { Authorization: 'Bearer lms_at_...' },
});

All app-scoped endpoints live under /api/v1/. They enforce the scope you were
granted and apply tier-based rate limiting (FREE 20 req/s, BASIC 40, PRO 100,
ENTERPRISE 500) via a sliding window per app + store.


STEP 5: REFRESH BEFORE EXPIRY

Access tokens last 24h. Before they expire (or on the first 401), exchange the
refresh token:

POST https://api.launchmystore.io/apps/oauth/token
Content-Type: application/json

{
  "grant_type": "refresh_token",
  "client_id": "YOUR_API_KEY",
  "client_secret": "YOUR_CLIENT_SECRET",
  "refresh_token": "lms_rt_..."
}

You'll receive a fresh access token and a fresh refresh token. Discard the old
pair. The refresh window resets to 30 days from the new exchange, so an active
app effectively stays installed forever.


SCOPES CATALOG

Request only what you need — reviewers reject apps that ask for surplus scope,
and merchants see the full list at install time. Common scopes:

 * read_products, write_products
 * read_orders, write_orders
 * read_customers, write_customers
 * read_metafields, write_metafields — required for both definitions and values.
 * read_inventory, write_inventory
 * read_themes, write_themes
 * read_discounts, write_discounts
 * read_checkouts
 * read_analytics

The full list lives in the developer documentation
[https://docs.launchmystore.io]. Write scopes implicitly grant read, so you
don't need to request both.


WEBHOOKS AND UNINSTALL

The host fires an app/uninstalled webhook (one of 31 supported topics) when a
merchant removes your app. Treat it as authoritative — the access token is
revoked immediately. Webhooks have 3 retries with exponential backoff (1m / 5m /
15m), so reply 200 as soon as you've queued the work.


FAQ


WHAT SCOPES DOES MY APP NEED TO READ METAFIELDS?

read_metafields for read access to both definitions and values; add
write_metafields if you need to create or update either. Scopes are shared
between definitions and value endpoints — one pair covers both.


HOW LONG DOES THE INSTALL CODE STAY VALID?

10 minutes, and it's one-time-use. The state is stored in Redis with that TTL.
Always exchange immediately on receipt — don't queue it.


CAN I HAVE MORE THAN ONE REDIRECT URI?

Yes. List every variant in your app configuration (production, staging,
localhost). The token endpoint requires the redirect_uri to exactly match one of
the registered ones — trailing slashes count.


WHAT HAPPENS IF MY REFRESH TOKEN EXPIRES?

After 30 days of inactivity, the refresh token expires and you'll get an
invalid_grant error. You'll need the merchant to reinstall (re-run the authorize
flow). Plan to refresh well before the 30-day window if your app is dormant for
long stretches.


HOW DO I REVOKE A TOKEN?

Call the uninstall endpoint or simply uninstall the app. Tokens are also revoked
automatically when the merchant uninstalls. There's no "revoke single token"
endpoint — rotating client_secret invalidates every JWT signed with it, which is
the closest equivalent.


WHY AM I GETTING 429 ERRORS?

You're exceeding your tier's rate limit on /api/v1/. Each app+store pair has a
per-second sliding window: FREE 20, BASIC 40, PRO 100, ENTERPRISE 500. Back off
and retry; or upgrade your tier in the developer portal.