Skip to content

JavaScript Integration

There is no official Storelayer JavaScript SDK; use fetch from your server runtime. The client below is all most integrations need: it adds the key, unwraps { success, data }, and turns both error shapes into one typed error. The only published npm package is @storelayer/mcp-server, which is for agents, not application code.

// storelayer.ts — server only; never import from browser code
const BASE_URL = process.env.STORE_LAYER_API_URL ?? 'https://api.storelayer.io'
const API_KEY = process.env.STORE_LAYER_API_KEY!
const PROJECT_ID = process.env.STORE_LAYER_PROJECT_ID! // must be the key's project
export class StorelayerError extends Error {
constructor(
readonly status: number,
readonly code: number | string, // 'VALIDATION' for schema issues; promotion evaluate may send 'VALIDATION_ERROR'
message: string,
readonly issues?: unknown[]
) {
super(message)
}
}
export async function sl<T>(method: string, path: string, body?: unknown): Promise<T> {
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
body: body === undefined ? undefined : JSON.stringify(body),
signal: AbortSignal.timeout(10_000)
})
const json = (await res.json().catch(() => null)) as any
if (res.ok && json?.success) return json.data as T
// Schema validation failure: { success: false, error: Issue[], data }
if (Array.isArray(json?.error)) {
throw new StorelayerError(res.status, 'VALIDATION', 'Request validation failed', json.error)
}
// Standard error: { success: false, error: { code, message } }
throw new StorelayerError(res.status, json?.error?.code ?? 0, json?.error?.message ?? res.statusText)
}
const p = (path: string) => `/projects/${PROJECT_ID}${path}`
type AssetBalance = { assetType: string; balance: number; lifetimeEarned: number; lifetimeSpent: number; lifetimeExpired: number; nextExpiration: string | null }
export const storelayer = {
registerCustomer: (c: { userId: string; firstName: string; lastName: string; email?: string }) =>
sl<{ id: string }>('POST', p('/users'), c),
getWallet: (userId: string) =>
sl<{ assets: Record<string, AssetBalance> }>('GET', p(`/users/${encodeURIComponent(userId)}/wallet`)),
earn: (userId: string, body: { assetType: string; amount: number; referenceId?: string; referenceType?: string; description?: string }) =>
sl('POST', p(`/users/${encodeURIComponent(userId)}/wallet/earn`), body),
sendEvent: (event: { type: string; userId?: string; payload: Record<string, unknown> }) =>
sl<{ id: string; status: 'accepted' }>('POST', '/ingest/events', event)
}

Usage:

try {
await storelayer.registerCustomer({ userId: 'cus_1001', firstName: 'Ada', lastName: 'Lovelace' })
} catch (e) {
if (!(e instanceof StorelayerError && e.code === 2500)) throw e // 2500 = already registered
}
await storelayer.sendEvent({ type: 'purchase', userId: 'cus_1001', payload: { orderId: 'ord_5001', amount: 5000 } })
const wallet = await storelayer.getWallet('cus_1001')
const points = wallet.assets.points?.balance ?? 0 // asset absent until first earn
SituationSignalWhat to do
Bad request body/paramscode === 'VALIDATION', issues[]Fix the payload; never retry as-is
Bad or revoked key401, code 1002Alert; don’t retry
Key/project mismatch403, code 1003Fix configuration
Business outcome (e.g. insufficient balance 2601, user exists 2500)4xx with domain codeHandle in your flow
Timeout / network error / 5xx on a readthrownSafe to retry with backoff
Timeout / 5xx on a writethrownCheck state before retrying — wallet earn/spend are not idempotent

A 200 is not always a “yes”: promotion evaluation can succeed with no qualifying promotions, and an accepted event may match no rule. Check data.

For full typing across all endpoints, generate types from the live spec with a third-party tool such as openapi-typescript:

Terminal window
npx openapi-typescript https://api.storelayer.io/openapi.json -o src/storelayer-api.d.ts

Many response schemas in the spec describe only the envelope (data is untyped), so keep hand-written response types like the ones above for the fields you read. Regenerate when you upgrade and review the diff.