Skip to main content
POST
/
apps
/
oauth
/
token
Token Exchange
curl --request POST \
  --url https://api.launchmystore.io/apps/oauth/token \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '
{
  "grant_type": "<string>",
  "client_id": "<string>",
  "client_secret": "<string>",
  "code": "<string>",
  "state": "<string>",
  "code_verifier": "<string>",
  "refresh_token": "<string>"
}
'
{
  "status": 123,
  "state": "<string>",
  "data": {
    "access_token": "<string>",
    "refresh_token": "<string>",
    "token_type": "<string>",
    "expires_in": 123,
    "scope": "<string>"
  }
}

Documentation Index

Fetch the complete documentation index at: https://docs.launchmystore.io/llms.txt

Use this file to discover all available pages before exploring further.

Token Exchange

Exchanges either a one-time authorization code (from GET /apps/oauth/authorize) or a long-lived refresh_token for a fresh access/refresh token pair. Two grant types are supported:
  • authorization_code — first-time install flow.
  • refresh_token — silent token rotation after the 24h access token expires.
This endpoint is rate-limited to 10 requests per minute per IP.

Request

curl -X POST "https://api.launchmystore.io/apps/oauth/token" \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "client_id": "lms_app_xxx",
    "client_secret": "lms_secret_yyy",
    "code": "9fbb1c3e8d4a7b2e...",
    "state": "5d6f7c8b9e0d1c2a...",
    "code_verifier": "OPTIONAL_PKCE_VERIFIER"
  }'

Body Parameters

Grant type: authorization_code

grant_type
string
required
Must be authorization_code.
client_id
string
required
Your app’s public client identifier.
client_secret
string
required
Your app’s hashed client secret. Verified against the stored bcrypt hash on the App row.
code
string
required
The one-time authorization code from GET /apps/oauth/authorize. Single-use, expires in 10 minutes.
state
string
required
The server-generated state token from the authorize call. Must match what was bound to the code in Redis or the request fails with Invalid state parameter.
code_verifier
string
Required when the authorize call sent a code_challenge. Length 43-128 chars. For S256 flows, the server SHA-256 hashes this and compares (constant-time) against the stored challenge.

Grant type: refresh_token

grant_type
string
required
Must be refresh_token.
refresh_token
string
required
The current refresh token. Must not be expired, blacklisted, or already rotated.

Response

status
integer
200 on success.
state
string
success or error.
data
object

Example Response

{
  "status": 200,
  "state": "success",
  "data": {
    "access_token": "lms_token_9fbb1c3e8d4a7b2e0a5d6f7c8b9e0d1c2a3b4c5d6e7f8091a2b3c4d5e6f70819",
    "refresh_token": "lms_refresh_5d6f7c8b9e0d1c2a3b4c5d6e7f80910a2b3c4d5e6f70819b8c4d5e6f7081923a",
    "token_type": "bearer",
    "expires_in": 86400,
    "scope": "read_products write_metafields read_orders"
  }
}

Token Lifetimes

TokenTTLNotes
Access token24 hours (expires_in: 86400)Used as Authorization: Bearer on /api/v1/*.
Refresh token30 daysResets on every successful rotation.
Authorization code10 minutesSingle-use; deleted after exchange.
When you refresh, the old refresh token is blacklisted (SHA-256 hashed and stored in Redis for 31 days) and a brand new pair is issued. Re-using a rotated refresh token returns 401 Token has been revoked.

Install/Re-install Limits

When grant_type=authorization_code would create a brand new active installation (or re-activate a previously disabled one), the server checks per-shop function caps before issuing tokens. If the app ships functions and the merchant already has too many active installs of apps with the same function types, the endpoint returns:
{
  "status": 409,
  "state": "error",
  "message": "Per-shop function limit exceeded for <type>: max <N>"
}
This check is skipped when re-issuing tokens for an already-active installation (no status flip = no new active install counted).

Error Codes

HTTPError messageWhen
400Unsupported grant_typegrant_type is not authorization_code or refresh_token.
400Invalid or expired authorization codecode not in Redis (consumed or > 10 minutes old).
400Invalid state parameterstate doesn’t match the value bound to the code.
400State validation failedstate not in Redis or client_id doesn’t match.
400code_verifier is required for this authorization codeAuthorize call sent PKCE challenge but token call omitted verifier.
400code_verifier must be 43-128 charactersPKCE verifier length out of range.
400code_verifier does not match the code_challengePKCE verification failed (constant-time compare).
401Invalid client credentialsclient_id/client_secret doesn’t match.
401Invalid refresh tokenNo installation has this refresh token.
401Refresh token has expired. Please re-authenticate.> 30 days since last rotation.
401Token has been revokedRefresh token was rotated or revoked.
409Per-shop function limit exceeded for <type>: max <N>Function caps prevent a new active install.
429(throttler)More than 10 requests/minute from this IP.

Refresh Loop Pattern

// Server-side helper — refresh before every API call when expiry is close.
async function withFreshToken(installation) {
  const msLeft = new Date(installation.tokenExpiresAt) - Date.now();
  if (msLeft > 60_000) return installation.accessToken;

  const res = await fetch('https://api.launchmystore.io/apps/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'refresh_token',
      refresh_token: installation.refreshToken,
    }),
  });
  const { data } = await res.json();

  // Persist the new pair — both old tokens are now blacklisted.
  await db.installation.update({
    accessToken: data.access_token,
    refreshToken: data.refresh_token,
    tokenExpiresAt: new Date(Date.now() + data.expires_in * 1000),
  });
  return data.access_token;
}

Security Notes

  • Always send this request server-side. Never expose client_secret in browser code.
  • For browser-based public clients, use PKCE (no client_secret).
  • Successful exchange consumes both the code and its state — both are deleted from Redis atomically.
  • The HMAC on client_secret uses bcrypt; brute-force is throttled by the IP rate limit (10/min) and by bcrypt’s per-hash cost.