Skip to main content

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 Rate Limits

The LaunchMyStore scoped REST API (/api/v1/) is rate-limited per app + store using a Redis-backed sliding window. Limits are per-second and tier-based — the app’s tier on the developer dashboard controls the cap.
Rate limits apply only to the OAuth-scoped REST API under /api/v1/. Internal admin endpoints, public storefront routes, and the CustomerLMS proxy routes (/api/apps/*) are not subject to this guard.

Limits by tier

TierRequests / secondUse case
free20Hobby/test apps, internal tools.
basic40Default tier for new published apps.
pro100Production apps with batch sync or background workers.
enterprise500High-volume integrations, ERP/data-warehouse syncs.
Tier is set per-app on the developer dashboard. See App Types & Tiers.

Algorithm

A sliding 1-second window per (appId, storeId) pair, implemented as a Redis sorted set:
  1. Every request’s timestamp is added to the set under the key rate_limit:{appId}:{storeId}.
  2. On each request, entries older than 1 second are evicted, the remaining count is checked against the tier limit.
  3. If count >= limit, the request is rejected with HTTP 429. Otherwise the timestamp is added and the request proceeds.
Both the window size and the default tier can be overridden via environment variables (RATE_LIMIT_WINDOW_SECONDS, RATE_LIMIT_DEFAULT), and rate limiting can be disabled entirely with RATE_LIMIT_ENABLED=false.

Response headers

Every response from /api/v1/ includes these headers, whether the request succeeded or was rate-limited:
HeaderDescription
X-RateLimit-LimitMaximum requests allowed per window for this app’s tier.
X-RateLimit-RemainingApproximate remaining requests in the current window. Floored at 0.
X-RateLimit-ResetUnix timestamp (seconds) when the current window resets.
Retry-AfterOnly on 429. Seconds to wait before retrying — equals the window size.
Example:
HTTP/1.1 200 OK
X-RateLimit-Limit: 40
X-RateLimit-Remaining: 37
X-RateLimit-Reset: 1747402805
Content-Type: application/json

{ "data": { ... } }

429 response

When the cap is exceeded:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 40
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1747402805
Retry-After: 1
Content-Type: application/json

{
  "error": "Too Many Requests",
  "message": "Rate limit exceeded. Retry after 1 second(s)."
}
A 429 means this app’s requests on this store exceeded the cap. Other stores using the same app are unaffected. Other apps on the same store are unaffected.

Best practices

Exponential backoff on 429

Always honor Retry-After and add a small random jitter to spread retries across multiple instances of your worker.
async function callWithRetry(fn, maxRetries = 5) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const res = await fn();
    if (res.status !== 429) return res;

    const retryAfter = parseInt(res.headers.get('retry-after') || '1', 10);
    const baseDelay = retryAfter * 1000;
    // Exponential backoff with jitter
    const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 250;
    await new Promise((r) => setTimeout(r, delay));
  }
  throw new Error('Rate limit exhausted after retries');
}

Read the headers proactively

Don’t wait for a 429 — check X-RateLimit-Remaining after every response and slow down when it drops below a safety threshold.
function shouldThrottle(res) {
  const remaining = parseInt(res.headers.get('x-ratelimit-remaining') || '40', 10);
  const limit = parseInt(res.headers.get('x-ratelimit-limit') || '40', 10);
  // Slow down when under 20% remaining
  return remaining < limit * 0.2;
}

if (shouldThrottle(res)) {
  await new Promise((r) => setTimeout(r, 250));
}

Batch requests where the API supports it

Several REST resources expose bulk endpoints to amortize the cap:
  • Metafields: POST /metafields/bulk upserts up to 250 metafields in one call. See Metafields.
  • Products: pagination via ?page=…&limit=… returns up to 250 products per call.
  • Orders: bulk fetch with ?ids=id1,id2,id3,….
One batched call costs one slot vs. N individual calls — usually the single biggest win for a sync-style app.

Use webhooks instead of polling

Many sync workflows poll the API every few seconds to detect changes. Subscribe to a webhook topic instead — LaunchMyStore pushes the event to your endpoint as it happens, costing zero rate-limit slots on your side.

Use background workers, not on-request fan-out

If a single end-user action triggers ten outbound API calls, those ten calls all count toward the per-second cap in the same window. Enqueueing them into a background worker that paces itself smooths the load.

Avoid the leader-election trap

Two instances of your app both polling on a one-second schedule will double the request rate. Use a Redis lock or a single scheduler to ensure only one instance fires the polled call per interval.

Multi-store apps

The rate-limit key includes both appId and storeId. A single app installed across 1000 stores has its own per-store window for each — the limits don’t pool. So an app at the basic tier with 1000 installs can sustain 40 × 1000 = 40,000 req/sec system-wide, as long as the requests are spread across stores.

Configuring tier on the developer dashboard

App tier is set on the developer dashboard’s app settings page. Tier upgrades are handled by:
  1. The merchant accepting a higher subscription tier for the app, or
  2. The developer requesting a tier upgrade via support (for first-party apps that don’t go through the marketplace flow).
Tier lookups are cached in-process for 60 seconds — a tier change can take up to 60s to propagate.

Failures and fallbacks

The guard logs a warning and allows the request through with headers set to “1 remaining” — operating without rate limiting is preferred over rejecting traffic during a Redis outage. Limits resume automatically once Redis reconnects.
Not for individual apps via API. Tier-level limits apply uniformly. If your integration has a one-off catch-up backfill requirement, contact support — for some integrations a temporary enterprise tier can be granted.
No. Every authenticated request to /api/v1/ regardless of method counts as one slot.
Yes — every request that reaches the guard counts toward the window, even if it ultimately 4xx’s on validation. Avoid “ping-and-error” patterns.

See also

Authentication

OAuth flow, scopes, and Bearer-token format.

App Types & Tiers

What each tier unlocks beyond rate limits.

Webhooks Overview

Push notifications — zero rate-limit cost on your side.

Function Active Limits

A parallel per-shop cap for declarative functions.