Session tokens are short-lived HS256 JWTs that prove a request originated
from a real merchant session inside the LaunchMyStore admin. Your app’s
backend verifies them with the same clientSecret LaunchMyStore stored
when the merchant installed the app — no shared session store, no
round-trips back to LaunchMyStore for the common case.
The crucial property: the JWT signature is your proof. Anyone in
possession of clientSecret can mint a token; the secret lives only on
the LaunchMyStore host and on your backend. Your frontend never sees it.
import { createApp } from '@launchmystore/app-bridge';const app = createApp({ apiKey: 'your-app-client-id', host: new URLSearchParams(location.search).get('host'),});const token = await app.getSessionToken();
App.getSessionToken() is a thin wrapper over
dispatchAndWait('SESSION_TOKEN_REQUEST') — it round-trips every call
and does not cache. For caching + auto-refresh-when-near-expiry, use
the SessionToken helper class or the useSessionToken React hook.
import { SessionToken } from '@launchmystore/app-bridge/actions';const session = SessionToken.create(app);const token = await session.getToken(); // cached if >30s remaining; else round-tripsconst fresh = await session.refresh(); // force a new token
Prefer getToken() over the token field. token is null until the
first fetch resolves; calling getToken() always returns a valid (cached
or freshly minted) token and avoids race conditions on mount.
// Header{ "alg": "HS256", "typ": "JWT" }// Payload (issued by /api/apps/session-token){ "iss": "https://launchmystore.io", "dest": "https://{shop-host}", "aud": "<your app's client ID>", "sub": "<store id — the immutable Accounts UUID; falls back to domainSlug if the store can't be resolved>", "exp": 1700003600, "iat": 1700000000, "nbf": 1700000000, "sid": "<random UUID — unique per token>", "storeId": "<same immutable store UUID as sub, or null if unresolved>", "domainSlug": "<store slug>", "shop": "<shop host, no scheme>", "permissions": ["read_products", "write_orders", "..."], "scopes": ["read_products", "write_orders", "..."]}
Claim
Meaning
iss
Always https://launchmystore.io (use this when verifying).
aud
Your app’s client ID. Verify this matches your installed app.
sub
The immutable store identifier (internal Accounts UUID). Use this (or the identical storeId) as the primary key for every per-merchant record. Falls back to domainSlug only if the store can’t be resolved — treat a non-UUID sub as an error and fail closed.
storeId
Same immutable UUID as sub (or null if unresolved). Either sub or storeId is the canonical tenant key.
dest / shop / domainSlug
The merchant’s storefront host. Mutable (custom domains / renames) — never use as a database key.
exp / iat / nbf
Standard JWT timestamps.
sid
Random UUID per token issuance — useful for audit/dedup logs.
permissions / scopes
The granted-scope list at the time the token was minted, included as an informational hint only (scopes is a legacy alias — both arrays are identical). Do not authorize from these claims. See the authorization note below.
Authorize against the OAuth installation’s granted scopes, not the JWT
permissions claim. The permissions / scopes arrays in the session
token are informational. The platform itself enforces scopes server-side on
every /api/v1/* call against the installation’s stored grantedScopes
(presented via your OAuth access token, not the session JWT). For your
own backend endpoints, authorize against the granted-scope list you persisted
at install (see Authentication → OAuth),
and fail closed if the installation is missing. Use the session token to
authenticate identity (verify signature + aud + exp/nbf + iss, then
key off sub/storeId), not to make authorization decisions from its scope
claim.
Lifetime: tokens expire one hour after issuance (exp = iat + 3600).
Caching: the SDK’s SessionToken.create(app) and the
useSessionToken hook keep the token in memory until 30 seconds
before expiry — call getToken() before every authenticated request
and the helper does the right thing.
Forced refresh: call session.refresh() (vanilla) or refresh()
(hook) to drop the cache and request a new token immediately. Useful
after permission changes or before a long-running operation.
app.getSessionToken() / session.getToken() reject with the SDK’s
generic Error shape — there is no error.code. Branch on
error.message if you need to distinguish causes:
try { const token = await app.getSessionToken();} catch (err) { if (err.message.includes('timeout')) { // Host took longer than 10s — typically not in admin, or host JS errored showFallbackUI(); } else { showError('Auth error. Please reload.'); }}
Backend verification errors map cleanly:
jsonwebtoken error
HTTP status
Client action
TokenExpiredError
401
Call refresh() and retry the request once.
JsonWebTokenError (bad signature)
401
Stop retrying — likely wrong clientSecret or token was tampered.
The secret lives on the LaunchMyStore host (to mint tokens) and your
backend (to verify them). If you put it in JavaScript that ships to
the browser, anyone can forge a token.
Always scope by sub
Every backend query that touches merchant data MUST be scoped by
claims.sub. Treat session tokens like row-level security tokens.
Verify aud and iss explicitly
The signature alone isn’t enough — a token issued for another app
(different aud) or by a different host (different iss) is a valid
JWT but a wrong one.
Authorize from granted scopes, not the JWT claim
The session token’s permissions / scopes arrays are an
informational hint, not the authorization source. Authorize against
the installation’s granted scopes — the platform enforces them
server-side on every /api/v1/* call (via your OAuth access token),
and for your own endpoints you should check the granted-scope list you
persisted at install. Fail closed if the installation is missing.
HTTPS only
Tokens grant access to merchant data — transmit only over TLS.
Don't log tokens
The signature is private; never write tokens to logs, exception
reports, or telemetry.