> ## 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.

# API Authentication

> Authenticate your API requests to LaunchMyStore

# API Authentication

The LaunchMyStore API uses OAuth 2.0 for authentication. This page covers the authentication methods available and how to use them in your API requests.

## Authentication Methods

<CardGroup cols={2}>
  <Card title="Access Tokens" icon="key">
    OAuth 2.0 access tokens for server-to-server API calls
  </Card>

  <Card title="Session Tokens" icon="id-badge">
    JWTs for embedded app API calls
  </Card>
</CardGroup>

## Access Tokens

Access tokens are obtained through the OAuth 2.0 flow and used for server-to-server API calls.

### Obtaining Access Tokens

Your app receives its authorization `code` through the managed
[install handoff](/getting-started/install-handoff): when a merchant
installs (or reinstalls) your app, LaunchMyStore redirects their browser
to your `/auth` endpoint with `code`, `state`, and an HMAC signature. Do
**not** redirect merchants to `GET /apps/oauth/authorize` yourself — that
endpoint is a merchant-authenticated JSON API used by the LaunchMyStore
admin, and a top-level browser redirect returns `401`.

```javascript theme={null}
// Inside your /auth handler (after HMAC verification):
// exchange the handoff code for tokens
const tokenResponse = await fetch('https://api.launchmystore.io/apps/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    client_id: CLIENT_ID,
    client_secret: CLIENT_SECRET,
    code: req.query.code,       // from the /auth handoff
    grant_type: 'authorization_code',
    state: req.query.state      // echo the handoff state verbatim
  })
});

// The token payload is wrapped in the platform envelope — read `data`
const { data } = await tokenResponse.json();
const { access_token, refresh_token, expires_in } = data;
```

### Using Access Tokens

Include the access token in the `Authorization` header:

```bash theme={null}
curl -X GET "https://api.launchmystore.io/api/v1/products.json" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json"
```

```javascript theme={null}
const response = await fetch('https://api.launchmystore.io/api/v1/products.json', {
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json'
  }
});
```

### Token Expiration

Access tokens expire after **24 hours**. Use the refresh token to get a new access token:

```javascript theme={null}
async function refreshAccessToken(refreshToken) {
  const response = await fetch('https://api.launchmystore.io/apps/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      refresh_token: refreshToken,
      grant_type: 'refresh_token'
    })
  });
  
  const { data } = await response.json();   // envelope: { status, state, data }
  
  // Store new tokens
  await storeTokens(data.access_token, data.refresh_token);
  
  return data.access_token;
}
```

<Warning>
  Refresh tokens expire after **30 days**. If a refresh token expires, the merchant must re-authorize your app.
</Warning>

### Token Management

Implement a token manager for automatic refresh:

```javascript theme={null}
class TokenManager {
  constructor(shopId) {
    this.shopId = shopId;
  }
  
  async getAccessToken() {
    const tokens = await this.loadTokens();
    
    // Check if access token is expired (with 5 min buffer)
    if (tokens.expiresAt < Date.now() + 300000) {
      return this.refreshToken(tokens.refreshToken);
    }
    
    return tokens.accessToken;
  }
  
  async refreshToken(refreshToken) {
    try {
      const response = await fetch('https://api.launchmystore.io/apps/oauth/token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          client_id: process.env.APP_CLIENT_ID,
          client_secret: process.env.APP_CLIENT_SECRET,
          refresh_token: refreshToken,
          grant_type: 'refresh_token'
        })
      });
      
      if (!response.ok) {
        throw new Error('Token refresh failed');
      }
      
      const { data } = await response.json();   // unwrap the envelope
      
      await this.storeTokens({
        accessToken: data.access_token,
        refreshToken: data.refresh_token,
        expiresAt: Date.now() + (data.expires_in * 1000)
      });
      
      return data.access_token;
    } catch (error) {
      // Refresh token may be expired - need re-authorization
      await this.markNeedsReauth();
      throw error;
    }
  }
  
  async loadTokens() {
    // Load from database
    return db.tokens.findOne({ shopId: this.shopId });
  }
  
  async storeTokens(tokens) {
    await db.tokens.updateOne(
      { shopId: this.shopId },
      { $set: tokens },
      { upsert: true }
    );
  }
  
  async markNeedsReauth() {
    await db.shops.updateOne(
      { id: this.shopId },
      { $set: { needsReauth: true } }
    );
  }
}
```

## Session Tokens

Session tokens are JWTs used for embedded app API calls. They're obtained through App Bridge.

### Getting Session Tokens

```javascript theme={null}
import { createApp } from '@launchmystore/app-bridge';

const app = createApp({
  apiKey: 'your-client-id',
  host: new URLSearchParams(location.search).get('host')
});

const token = await app.getSessionToken();
```

### Using Session Tokens

Include in the `Authorization` header:

```javascript theme={null}
const token = await app.getSessionToken();

const response = await fetch('https://your-app.com/api/data', {
  headers: {
    'Authorization': `Bearer ${token}`
  }
});
```

### Verifying Session Tokens

Verify session tokens on your backend:

```javascript theme={null}
import jwt from 'jsonwebtoken';

function verifySessionToken(token) {
  try {
    const decoded = jwt.verify(token, process.env.APP_CLIENT_SECRET, {
      algorithms: ['HS256'],
      audience: process.env.APP_CLIENT_ID,
      issuer: 'https://launchmystore.io'
    });
    
    return {
      valid: true,
      shopId: decoded.sub,
      shopDomain: decoded.dest
    };
  } catch (error) {
    return { valid: false, error: error.message };
  }
}
```

See [Session Tokens](/app-bridge/session-tokens) for detailed documentation.

## Scopes

Request only the scopes your app needs:

### Store Data Scopes

| Scope               | Access                                                                |
| ------------------- | --------------------------------------------------------------------- |
| `read_shop`         | View store information                                                |
| `write_shop`        | Modify store settings                                                 |
| `read_products`     | View products and variants                                            |
| `write_products`    | Create, update, delete products                                       |
| `read_collections`  | *Reserved — unused.* Collection reads are gated by `read_products`.   |
| `write_collections` | *Reserved — unused.* Collection writes are gated by `write_products`. |
| `read_inventory`    | View inventory levels                                                 |
| `write_inventory`   | Adjust inventory                                                      |

### Order Scopes

| Scope                | Access                                                               |
| -------------------- | -------------------------------------------------------------------- |
| `read_orders`        | View orders and transactions, **and fulfillments**                   |
| `write_orders`       | Create, update, fulfill orders **and manage fulfillments**           |
| `read_fulfillments`  | *Reserved — unused.* Fulfillment reads are gated by `read_orders`.   |
| `write_fulfillments` | *Reserved — unused.* Fulfillment writes are gated by `write_orders`. |

### Customer Scopes

| Scope             | Access                   |
| ----------------- | ------------------------ |
| `read_customers`  | View customer data       |
| `write_customers` | Create, update customers |

### Content Scopes

| Scope              | Access                                       |
| ------------------ | -------------------------------------------- |
| `read_content`     | View pages, blogs, articles                  |
| `write_content`    | Manage pages, blogs, articles                |
| `read_themes`      | View theme files                             |
| `write_themes`     | Modify theme files                           |
| `read_metafields`  | View metafield data                          |
| `write_metafields` | Create, update metafields                    |
| `read_files`       | View files in the store asset library        |
| `write_files`      | Upload and delete files in the asset library |

### Admin Scopes

| Scope              | Access                       |
| ------------------ | ---------------------------- |
| `read_discounts`   | View discount codes          |
| `write_discounts`  | Manage discounts             |
| `read_gift_cards`  | View gift cards              |
| `write_gift_cards` | Manage gift cards            |
| `read_shipping`    | View shipping zones (full)   |
| `write_shipping`   | Manage shipping zones (full) |

### Billing & Analytics Scopes

| Scope            | Access                                                 |
| ---------------- | ------------------------------------------------------ |
| `read_billing`   | View merchant plan, subscriptions, invoices, credits   |
| `write_billing`  | Reserved — future support for managing payment methods |
| `read_analytics` | View store analytics (summary, sales, top products)    |

### Settings & Marketing Scopes

| Scope                   | Access                                                      |
| ----------------------- | ----------------------------------------------------------- |
| `read_settings`         | View general store settings, shipping, payment, tax rules   |
| `write_settings`        | Update a whitelisted subset of general store settings       |
| `read_marketing`        | Reserved — future read access to campaigns and automations  |
| `write_marketing`       | Reserved — future write access to campaigns and automations |
| `read_email_templates`  | View transactional email templates                          |
| `write_email_templates` | Create and modify transactional email templates             |

## Rate Limits

API requests are rate limited based on your app's billing tier:

| Tier       | Requests/Second |
| ---------- | --------------- |
| Free       | 20              |
| Basic      | 40              |
| Pro        | 100             |
| Enterprise | 500             |

The limit is a flat per-second sliding window per (app, store); there is
no separate burst allowance.

### Rate Limit Headers

Every response includes rate limit information:

```http theme={null}
X-RateLimit-Limit: 40
X-RateLimit-Remaining: 38
X-RateLimit-Reset: 1705312800
```

### Handling Rate Limits

```javascript theme={null}
async function apiCall(url, options, retries = 3) {
  const response = await fetch(url, options);
  
  if (response.status === 429) {
    if (retries > 0) {
      const resetTime = response.headers.get('X-RateLimit-Reset');
      const waitMs = (resetTime * 1000) - Date.now() + 100;
      
      await sleep(Math.min(waitMs, 60000));
      return apiCall(url, options, retries - 1);
    }
    
    throw new Error('Rate limit exceeded');
  }
  
  return response;
}
```

## Error Responses

### Authentication Errors

| Status | Description                                                                                                        |
| ------ | ------------------------------------------------------------------------------------------------------------------ |
| 401    | Missing, invalid, or expired access token (also returned when the app has been uninstalled and its token cleared). |
| 403    | The installation is missing a scope the endpoint requires.                                                         |

There is **no machine-readable `code` string** — branch on the HTTP
status. The message is carried in `errors.base[0]`.

### Error Response Format

Errors use the standard `{ errors }` body:

```json theme={null}
{
  "errors": {
    "base": ["Invalid or expired access token"]
  }
}
```

### Handling Errors

```javascript theme={null}
async function makeApiRequest(endpoint, options = {}) {
  const response = await fetch(
    `https://api.launchmystore.io/api/v1${endpoint}`,
    {
      ...options,
      headers: {
        ...options.headers,
        'Authorization': `Bearer ${await tokenManager.getAccessToken()}`,
        'Content-Type': 'application/json'
      }
    }
  );
  
  if (!response.ok) {
    const body = await response.json().catch(() => ({}));
    const message = body?.errors?.base?.[0] || 'Request failed';

    switch (response.status) {
      case 401:
        // Token invalid/expired (or app uninstalled) — refresh and retry once
        await tokenManager.refreshToken();
        return makeApiRequest(endpoint, options);

      case 403:
        throw new Error(`Insufficient permissions: ${message}`);

      default:
        throw new Error(message);
    }
  }

  return response.json();
}
```

## Security Best Practices

<AccordionGroup>
  <Accordion title="Never expose secrets">
    Never include your client secret in client-side code. Keep it on your backend only.
  </Accordion>

  <Accordion title="Use HTTPS everywhere">
    Always use HTTPS for API calls and webhook endpoints.
  </Accordion>

  <Accordion title="Validate the state parameter">
    Always validate the `state` parameter in OAuth callbacks to prevent CSRF attacks.
  </Accordion>

  <Accordion title="Store tokens securely">
    Encrypt access and refresh tokens in your database. Never log them.
  </Accordion>

  <Accordion title="Request minimal scopes">
    Only request the scopes your app actually needs. Merchants trust apps with fewer permissions.
  </Accordion>

  <Accordion title="Handle token revocation">
    Tokens can be revoked if a merchant uninstalls your app. Handle this gracefully.
  </Accordion>
</AccordionGroup>

## Testing Authentication

### Test Mode

Use test credentials for development:

```javascript theme={null}
const config = {
  clientId: process.env.NODE_ENV === 'production'
    ? process.env.APP_CLIENT_ID
    : process.env.APP_TEST_CLIENT_ID,
  clientSecret: process.env.NODE_ENV === 'production'
    ? process.env.APP_CLIENT_SECRET
    : process.env.APP_TEST_CLIENT_SECRET
};
```

### Mock Tokens

For unit testing, create mock tokens:

```javascript theme={null}
import jwt from 'jsonwebtoken';

function createMockSessionToken(payload = {}) {
  return jwt.sign(
    {
      iss: 'https://launchmystore.io',
      aud: 'test-client-id',
      sub: 'test-shop-id',
      dest: 'https://test-store.launchmystore.io',
      exp: Math.floor(Date.now() / 1000) + 86400,
      iat: Math.floor(Date.now() / 1000),
      ...payload
    },
    'test-client-secret',
    { algorithm: 'HS256' }
  );
}
```

## See Also

* [OAuth Flow](/getting-started/authentication) - Complete OAuth setup guide
* [Session Tokens](/app-bridge/session-tokens) - JWT authentication for embedded apps
* [API Overview](/api-reference/overview) - API structure and conventions
