Docs
    GuidesAPI ReferenceBlog
    Sign inCreate account
    Overview

    Getting started

    Sign upAPI keysQuickstartLoad your catalog

    Integration

    Tracking EventsIdentity StitchingPersonalisation

    Production

    Errors & status codesRetries & rate limitsTypeScript SDKTroubleshooting

    Reference

    API ReferenceVersioningChangelog
    HomeDocumentationTypeScript SDK
    Previous
    Retries & rate limits
    Next
    Troubleshooting

    Skip the ML, Ship the Revenue

    Product

    • How It Works
    • Features
    • For Startups
    • For Developers

    Developers

    • Documentation

    Company

    • Blog
    • Contact

    © 2026 Lehnz, Inc. All rights reserved.

    Production

    TypeScript SDK

    A typed, retry-aware reference client for the lehnz API. Drop it into your project as a single file — under 200 lines, zero dependencies beyond fetch.

    Raw HTTP API is canonical

    The TypeScript SDK is a convenience wrapper. The canonical integration path is the raw HTTP API — see Quickstart and Tracking events. Use the SDK only if it fits your workflow.

    Roadmap: official npm package

    An official @lehnz/client npm package is on the roadmap. Until then, the file below is the canonical reference implementation — paste it in, modify what you need, and ship.

    The client

    Save as lehnz.ts in your project. No npm install required — it uses the platform's fetch.

    lehnz.ts
    // lehnz.ts — drop this into your project
    type Json = Record<string, unknown>;
    export type EventName =
    | 'view'
    | 'click'
    | 'search'
    | 'add_to_cart'
    | 'remove_from_cart'
    | 'checkout_start'
    | 'purchase'
    | 'identify'
    | (string & {});
    export interface LehnzEvent {
    user_id: string;
    event_name: EventName;
    item_id?: string;
    session_id?: string;
    recommendation_id?: string;
    previous_user_id?: string;
    context?: Json;
    }
    export interface LehnzItem {
    item_id: string;
    item_type?: string;
    status?: 'active' | 'inactive' | 'deleted';
    attributes?: Json;
    }
    export interface LehnzUser {
    user_id: string;
    attributes?: Json;
    }
    export interface RecommendationResponse {
    recommendation_id: string;
    all_item_ids: string[];
    meta: { total_items: number; total_pages: number };
    }
    interface Envelope<T> {
    success: boolean;
    data?: T;
    message?: string;
    error?: string | unknown;
    }
    export class LehnzError extends Error {
    constructor(
    message: string,
    readonly status: number,
    readonly retryable: boolean,
    readonly raw: unknown,
    ) {
    super(message);
    }
    }
    export interface LehnzOptions {
    apiKey: string;
    ingestionBase?: string;
    recommendationsBase?: string;
    retries?: number;
    fetch?: typeof fetch;
    }
    export class Lehnz {
    private readonly apiKey: string;
    private readonly ingestion: string;
    private readonly recommendations: string;
    private readonly retries: number;
    private readonly fetchImpl: typeof fetch;
    constructor(opts: LehnzOptions) {
    this.apiKey = opts.apiKey;
    this.ingestion = opts.ingestionBase ?? 'https://ingestion.lehnz.com/api/v1';
    this.recommendations = opts.recommendationsBase ?? 'https://recommendations.lehnz.com';
    this.retries = opts.retries ?? 3;
    this.fetchImpl = opts.fetch ?? fetch;
    }
    events = {
    ingest: (events: LehnzEvent | LehnzEvent[]) =>
    this.post<{ accepted: number }>(`${this.ingestion}/events/ingest`, Array.isArray(events) ? events : [events]),
    };
    items = {
    upsert: (items: LehnzItem[]) => this.post<unknown>(`${this.ingestion}/items/upsert`, items),
    };
    users = {
    upsert: (users: LehnzUser[]) => this.post<unknown>(`${this.ingestion}/users/upsert`, users),
    };
    recommend = {
    forUser: (req: { user_id: string; limit?: number; page?: number; placement?: string }) =>
    this.post<RecommendationResponse>(`${this.recommendations}/recommend`, req),
    similarToItem: (item_id: string, params: { limit?: number; page?: number; mode?: string } = {}) => {
    const qs = new URLSearchParams(Object.entries(params).filter(([, v]) => v != null) as [string, string][]).toString();
    return this.get<RecommendationResponse>(`${this.recommendations}/item/${item_id}${qs ? '?' + qs : ''}`);
    },
    forSession: (req: { clicked_item_id: string; limit?: number; page?: number }) =>
    this.post<RecommendationResponse>(`${this.recommendations}/recommend/session`, req),
    forBasket: (req: { item_ids: string[]; limit?: number; page?: number }) =>
    this.post<RecommendationResponse>(`${this.recommendations}/recommend/basket`, req),
    priceOptimized: (req: { user_id: string; limit?: number; page?: number; placement?: string }) =>
    this.post<RecommendationResponse>(`${this.recommendations}/recommend/price_optimized`, req),
    };
    private headers() {
    return {
    'X-API-KEY': this.apiKey,
    'Content-Type': 'application/json',
    };
    }
    private async get<T>(url: string): Promise<T> {
    return this.withRetry(() => this.fetchImpl(url, { headers: this.headers() }));
    }
    private async post<T>(url: string, body: unknown): Promise<T> {
    return this.withRetry(() =>
    this.fetchImpl(url, {
    method: 'POST',
    headers: this.headers(),
    body: JSON.stringify(body),
    }),
    );
    }
    private async withRetry<T>(send: () => Promise<Response>): Promise<T> {
    let lastErr: unknown;
    for (let attempt = 0; attempt <= this.retries; attempt++) {
    const res = await send();
    const json = (await res.json()) as Envelope<T>;
    if (json.success && json.data !== undefined) return json.data;
    const retryable = res.status === 429 || res.status >= 500;
    const err = new LehnzError(json.message ?? `HTTP ${res.status}`, res.status, retryable, json);
    lastErr = err;
    if (!retryable || attempt === this.retries) throw err;
    const reset = Number(res.headers.get('RateLimit-Reset')) || 0;
    const backoff = Math.max(reset * 1000, Math.min(250 * 2 ** attempt, 8000));
    const jitter = Math.random() * 250;
    await new Promise(r => setTimeout(r, backoff + jitter));
    }
    throw lastErr;
    }
    }

    What it does for you

    • Types every request and response — autocomplete on event names, recommendation params, and the item-IDs response shape.
    • Unwraps the envelope — methods return data, not { success, data, message }.
    • Retries on 429 and 5xx — exponential backoff with jitter, honoring RateLimit-Reset headers.
    • Throws LehnzError with the HTTP status and whether it's retryable, so your error boundary can branch correctly.
    • Pluggable fetch — pass a custom fetch for tests, mocking, or framework integration (e.g. Next.js's instrumented fetch).

    Usage

    example.ts
    import { Lehnz } from './lehnz';
    const lehnz = new Lehnz({ apiKey: process.env.LEHNZ_PUBLISHABLE_KEY! });
    await lehnz.events.ingest({
    user_id: 'usr_123',
    event_name: 'view',
    item_id: 'prod_456',
    });
    const recs = await lehnz.recommend.forUser({ user_id: 'usr_123', limit: 12 });
    console.log(recs.all_item_ids);
    const similar = await lehnz.recommend.similarToItem('prod_456', { limit: 4 });
    const cross = await lehnz.recommend.forBasket({
    item_ids: cart.map(c => c.id),
    limit: 8,
    });

    Error handling

    The client retries transient failures internally. By the time you see a LehnzError, retries have been exhausted — branch on retryable to decide between a fallback experience and a hard failure:

    errorHandling.ts
    import { Lehnz, LehnzError } from './lehnz';
    try {
    await lehnz.recommend.forUser({ user_id });
    } catch (err) {
    if (err instanceof LehnzError) {
    console.error(`lehnz ${err.status}: ${err.message}`);
    if (err.retryable) {
    showFallbackProducts();
    } else {
    reportToSentry(err);
    }
    }
    }

    Customizing

    • Server-side use: swap apiKey from lehnz_pk_ to lehnz_sk_ and unlock bulk uploads.
    • Custom event family: the EventName type uses an open string union — any custom name typechecks. Standard names are autocompleted.
    • Logging hook: wrap withRetry to emit metrics on every attempt, retry, and failure.
    • Shorter retries for the browser: pass { retries: 1 } to keep page-load latency predictable.

    What's next

    Errors & status codes

    Every error response, mapped to a fix.

    API Reference

    Every endpoint, header, and schema in one place.