docsSDKsTypeScript

TypeScript SDK

Complete TypeScript examples for integrating with Lorn AI.


Installation

npm install node-fetch  # or use built-in fetch in Node 18+

Setup

// lorn-client.ts
 
const LORN_BASE_URL = process.env.LORN_BASE_URL || "https://{{YOUR_STORE_URL}}";
const LORN_API_KEY = process.env.LORN_API_KEY || "{{YOUR_API_KEY}}";
 
// Types
interface Product {
  id: string;
  title: string;
  description: string;
  vendor?: string;
  category?: string;
  price: number;
  currency: string;
  availability?: {
    status: string;
    quantity: number;
  };
  tags: string[];
  images: Array<{ url: string; alt_text?: string }>;
  variants: Variant[];
  similarity_score?: number;
}
 
interface Variant {
  sku: string;
  title?: string;
  price: number;
  currency: string;
  attributes: Record<string, string>;
  inventory?: {
    status: string;
    quantity: number;
  };
}
 
interface SearchResponse {
  items: Product[];
  page: number;
  page_size: number;
  total: number;
}
 
interface Address {
  name?: string;
  line1?: string;
  line2?: string;
  city?: string;
  state?: string;
  postal_code?: string;
  country?: string;
}
 
interface Customer {
  email?: string;
  phone?: string;
  name?: string;
}
 
interface LineItem {
  product_id: string;
  variant_sku?: string;
  quantity: number;
  title?: string;
  unit_price: number;
  currency: string;
  subtotal: number;
}
 
interface CheckoutSession {
  id: string;
  status: string;
  currency: string;
  line_items: LineItem[];
  shipping_address?: Address;
  customer?: Customer;
  shipping: { method: string; amount: number; currency: string };
  tax: { rate: number; amount: number; currency: string };
  amounts: {
    subtotal: number;
    tax: number;
    shipping: number;
    total: number;
    currency: string;
  };
  client_secret: string;
  created_at: string;
}
 
interface CheckoutResponse {
  checkout_session: CheckoutSession;
}
 
// Client class
class LornClient {
  private baseUrl: string;
  private apiKey: string;
 
  constructor(baseUrl?: string, apiKey?: string) {
    this.baseUrl = baseUrl || LORN_BASE_URL;
    this.apiKey = apiKey || LORN_API_KEY;
  }
 
  private async request<T>(
    method: string,
    endpoint: string,
    options: { params?: Record<string, any>; body?: any } = {}
  ): Promise<T> {
    let url = `${this.baseUrl}${endpoint}`;
    
    if (options.params) {
      const searchParams = new URLSearchParams();
      Object.entries(options.params).forEach(([key, value]) => {
        if (value !== undefined) {
          searchParams.append(key, String(value));
        }
      });
      url += `?${searchParams}`;
    }
 
    const response = await fetch(url, {
      method,
      headers: {
        "X-ACP-API-Key": this.apiKey,
        "Content-Type": "application/json",
        Accept: "application/json",
      },
      body: options.body ? JSON.stringify(options.body) : undefined,
    });
 
    if (!response.ok) {
      throw new Error(`API error: ${response.status} ${response.statusText}`);
    }
 
    return response.json();
  }
 
  // Health
  async health(): Promise<{ status: string; supabase: string }> {
    return this.request("GET", "/health");
  }
 
  // Products
  async searchProducts(options: {
    q?: string;
    category?: string;
    tag?: string;
    min_price?: number;
    max_price?: number;
    page?: number;
    page_size?: number;
  } = {}): Promise<SearchResponse> {
    return this.request("GET", "/acp/products", { params: options });
  }
 
  async getProduct(productId: string): Promise<Product> {
    return this.request("GET", `/acp/products/${productId}`);
  }
 
  // Checkout
  async createCheckout(options: {
    items: Array<{ product_id: string; variant_sku?: string; quantity?: number }>;
    shipping_address?: Address;
    customer?: Customer;
  }): Promise<CheckoutResponse> {
    return this.request("POST", "/checkout_sessions", { body: options });
  }
 
  async getCheckout(sessionId: string): Promise<CheckoutResponse> {
    return this.request("GET", `/checkout_sessions/${sessionId}`);
  }
 
  async updateCheckout(
    sessionId: string,
    updates: {
      items?: Array<{ product_id: string; variant_sku?: string; quantity?: number }>;
      shipping_address?: Address;
      customer?: Customer;
    }
  ): Promise<CheckoutResponse> {
    return this.request("PATCH", `/checkout_sessions/${sessionId}`, { body: updates });
  }
 
  async completeCheckout(sessionId: string): Promise<CheckoutResponse> {
    return this.request("POST", `/checkout_sessions/${sessionId}/complete`);
  }
 
  async cancelCheckout(sessionId: string): Promise<CheckoutResponse> {
    return this.request("POST", `/checkout_sessions/${sessionId}/cancel`);
  }
 
  // Webhooks
  async emitWebhook(options: {
    event_type: string;
    checkout_session_id?: string;
    target_url?: string;
    payload?: Record<string, any>;
  }): Promise<{ delivered: number | null; signature: string; payload: any }> {
    return this.request("POST", "/webhooks/emit", { body: options });
  }
}
 
export const client = new LornClient();
export { LornClient, Product, Variant, CheckoutSession, CheckoutResponse };

Usage Examples

Search Products

import { client } from "./lorn-client";
 
// Basic search
const results = await client.searchProducts({ q: "comfortable running shoes" });
 
for (const product of results.items) {
  console.log(`${product.title} - $${product.price}`);
  if (product.similarity_score) {
    console.log(`  Relevance: ${product.similarity_score.toFixed(2)}`);
  }
}
 
// With filters
const filtered = await client.searchProducts({
  q: "wireless headphones",
  min_price: 50,
  max_price: 200,
  page_size: 5,
});
 
// Category browse
const electronics = await client.searchProducts({
  category: "Electronics",
  page_size: 20,
});

Get Product Details

const product = await client.getProduct("prod_abc123");
 
console.log(`Title: ${product.title}`);
console.log(`Price: $${product.price}`);
console.log(`Description: ${product.description}`);
 
// Show variants
for (const variant of product.variants) {
  const attrs = Object.entries(variant.attributes)
    .map(([k, v]) => `${k}: ${v}`)
    .join(", ");
  console.log(`  - ${variant.sku}: ${attrs} - $${variant.price}`);
}

Create Checkout

// Simple checkout
const session = await client.createCheckout({
  items: [{ product_id: "prod_abc123", quantity: 1 }],
});
 
console.log(`Session ID: ${session.checkout_session.id}`);
console.log(`Total: $${session.checkout_session.amounts.total}`);
 
// With full details
const fullSession = await client.createCheckout({
  items: [
    {
      product_id: "prod_abc123",
      variant_sku: "sku_black_m",
      quantity: 2,
    },
  ],
  shipping_address: {
    name: "Jane Doe",
    line1: "123 Main Street",
    city: "San Francisco",
    state: "CA",
    postal_code: "94102",
    country: "US",
  },
  customer: {
    email: "jane@example.com",
    name: "Jane Doe",
  },
});

Update Checkout

// Update shipping address
const updated = await client.updateCheckout("cs_demo_abc123", {
  shipping_address: {
    name: "Jane Doe",
    line1: "456 Oak Avenue",
    city: "Los Angeles",
    state: "CA",
    postal_code: "90001",
    country: "US",
  },
});
 
// Update cart items
const cartUpdated = await client.updateCheckout("cs_demo_abc123", {
  items: [{ product_id: "prod_abc123", quantity: 3 }],
});

Complete Checkout

const order = await client.completeCheckout("cs_demo_abc123");
 
console.log(`Status: ${order.checkout_session.status}`);
console.log(`Total: $${order.checkout_session.amounts.total}`);

Complete Purchase Flow

async function completePurchaseFlow(): Promise<void> {
  // 1. Search for products
  console.log("Searching for running shoes...");
  const results = await client.searchProducts({
    q: "running shoes",
    max_price: 150,
  });
 
  if (results.items.length === 0) {
    console.log("No products found");
    return;
  }
 
  const product = results.items[0];
  console.log(`Found: ${product.title} - $${product.price}`);
 
  // 2. Get product details
  const productDetails = await client.getProduct(product.id);
  const variant = productDetails.variants[0];
 
  // 3. Create checkout
  console.log("\nCreating checkout...");
  const session = await client.createCheckout({
    items: [
      {
        product_id: product.id,
        variant_sku: variant?.sku,
        quantity: 1,
      },
    ],
    shipping_address: {
      name: "Jane Doe",
      line1: "123 Main St",
      city: "San Francisco",
      state: "CA",
      postal_code: "94102",
      country: "US",
    },
    customer: { email: "jane@example.com" },
  });
 
  const { amounts } = session.checkout_session;
  console.log(`Session: ${session.checkout_session.id}`);
  console.log(`Subtotal: $${amounts.subtotal}`);
  console.log(`Tax: $${amounts.tax}`);
  console.log(`Shipping: $${amounts.shipping}`);
  console.log(`Total: $${amounts.total}`);
 
  // 4. Complete checkout
  console.log("\nCompleting purchase...");
  const order = await client.completeCheckout(session.checkout_session.id);
 
  console.log("Order completed!");
  console.log(`Status: ${order.checkout_session.status}`);
}
 
completePurchaseFlow().catch(console.error);

Error Handling

async function safeApiCall(): Promise<void> {
  try {
    const results = await client.searchProducts({ q: "shoes" });
    console.log(`Found ${results.total} products`);
  } catch (error) {
    if (error instanceof Error) {
      if (error.message.includes("401")) {
        console.error("Invalid API key");
      } else if (error.message.includes("404")) {
        console.error("Not found");
      } else if (error.message.includes("429")) {
        console.error("Rate limited - retry later");
      } else {
        console.error(`Error: ${error.message}`);
      }
    }
  }
}

Webhook Signature Verification

import crypto from "crypto";
 
function verifyWebhookSignature(
  body: string,
  signatureHeader: string,
  secret: string,
  toleranceSeconds: number = 300
): boolean {
  // Parse signature header: "t=<timestamp>,v1=<signature>"
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((p) => p.split("="))
  );
  const timestamp = parseInt(parts.t || "0");
  const receivedSig = parts.v1 || "";
 
  // Check timestamp freshness
  if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) {
    return false;
  }
 
  // Compute expected signature
  const expectedSig = crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
 
  // Timing-safe comparison
  try {
    return crypto.timingSafeEqual(
      Buffer.from(expectedSig),
      Buffer.from(receivedSig)
    );
  } catch {
    return false;
  }
}
 
// Express webhook handler example
import express from "express";
 
const app = express();
const WEBHOOK_SECRET = process.env.LORN_WEBHOOK_SECRET!;
 
app.post(
  "/webhooks/lorn",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.headers["x-webhook-signature"] as string;
 
    if (!verifyWebhookSignature(req.body.toString(), signature, WEBHOOK_SECRET)) {
      return res.status(401).json({ error: "Invalid signature" });
    }
 
    const event = JSON.parse(req.body.toString());
 
    switch (event.type) {
      case "order.created":
        console.log(`New order: ${event.data.object.id}`);
        break;
      case "order.fulfilled":
        console.log(`Order fulfilled: ${event.data.object.id}`);
        break;
    }
 
    res.json({ received: true });
  }
);
 
app.listen(3000, () => console.log("Webhook server running on port 3000"));

React Hook Example

// useLornProducts.ts
import { useState, useEffect } from "react";
import { client, Product } from "./lorn-client";
 
interface UseProductsResult {
  products: Product[];
  loading: boolean;
  error: Error | null;
  search: (query: string) => Promise<void>;
}
 
export function useLornProducts(): UseProductsResult {
  const [products, setProducts] = useState<Product[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);
 
  const search = async (query: string) => {
    setLoading(true);
    setError(null);
    
    try {
      const results = await client.searchProducts({ q: query });
      setProducts(results.items);
    } catch (e) {
      setError(e instanceof Error ? e : new Error("Unknown error"));
    } finally {
      setLoading(false);
    }
  };
 
  return { products, loading, error, search };
}
 
// Usage in component
function ProductSearch() {
  const { products, loading, error, search } = useLornProducts();
  const [query, setQuery] = useState("");
 
  const handleSearch = () => search(query);
 
  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <button onClick={handleSearch} disabled={loading}>
        Search
      </button>
      
      {error && <p>Error: {error.message}</p>}
      
      {products.map((product) => (
        <div key={product.id}>
          <h3>{product.title}</h3>
          <p>${product.price}</p>
        </div>
      ))}
    </div>
  );
}