Skip to content

JavaScript Examples

Complete JavaScript examples for integrating with the KitchenClick Ecommerce API.

API Client Setup

Create a reusable API client with authentication:

javascript
class KitchenClickClient {
  constructor(config) {
    this.baseUrl = config.baseUrl || 'https://api.kitchenclick.retailsuccessplatform.com/api/v1/ecommerce';
    this.clientId = config.clientId;
    this.clientSecret = config.clientSecret;
    this.identityUrl = config.identityUrl || 'https://identity.retailsuccessplatform.com';
    this.token = null;
    this.tokenExpiry = 0;
  }

  async getToken() {
    if (this.token && Date.now() < this.tokenExpiry - 300000) {
      return this.token;
    }

    const response = await fetch(`${this.identityUrl}/oauth/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'client_credentials',
        client_id: this.clientId,
        client_secret: this.clientSecret,
        scope: 'kitchenclick:*',
      }),
    });

    if (!response.ok) {
      throw new Error('Failed to obtain access token');
    }

    const data = await response.json();
    this.token = data.access_token;
    this.tokenExpiry = Date.now() + (data.expires_in * 1000);

    return this.token;
  }

  async request(path, options = {}) {
    const url = `${this.baseUrl}${path}`;
    const headers = { ...options.headers };

    if (options.authenticated !== false) {
      const token = await this.getToken();
      headers['Authorization'] = `Bearer ${token}`;
    }

    if (options.body && typeof options.body === 'object') {
      headers['Content-Type'] = 'application/json';
      options.body = JSON.stringify(options.body);
    }

    const response = await fetch(url, { ...options, headers });
    const data = await response.json();

    if (data.status === 'error') {
      const error = new Error(data.message);
      error.errors = data.errors;
      error.code = data.error_code;
      throw error;
    }

    return data;
  }

  // Public endpoints (no auth)
  async getLocation(locationId) {
    return this.request(`/locations/${locationId}`, { authenticated: false });
  }

  async getLocationHours(locationId) {
    return this.request(`/locations/${locationId}/hours`, { authenticated: false });
  }

  async getConcepts(locationId) {
    return this.request(`/locations/${locationId}/concepts`, { authenticated: false });
  }

  async getMenus(locationId, conceptId) {
    return this.request(`/locations/${locationId}/concepts/${conceptId}/menus`, { authenticated: false });
  }

  async getMenuItems(locationId, conceptId, menuId) {
    return this.request(`/locations/${locationId}/concepts/${conceptId}/menus/${menuId}/items`, { authenticated: false });
  }

  async getItemDetails(locationId, conceptId, itemId) {
    return this.request(`/locations/${locationId}/concepts/${conceptId}/items/${itemId}`, { authenticated: false });
  }

  async trackOrder(orderId) {
    return this.request(`/orders/${orderId}/track`, { authenticated: false });
  }

  // Authenticated endpoints
  async checkAvailability(locationId, itemIds) {
    return this.request(`/locations/${locationId}/items/check-availability`, {
      method: 'POST',
      body: { items: itemIds.map(id => ({ item_id: id })) },
    });
  }

  async calculateOrder(orderData) {
    return this.request('/orders/calculate', {
      method: 'POST',
      body: orderData,
    });
  }

  async createOrder(orderData) {
    return this.request('/orders', {
      method: 'POST',
      body: orderData,
    });
  }

  async createPaymentIntent(data) {
    return this.request('/payment/stripe/create-intent', {
      method: 'POST',
      body: data,
    });
  }
}

// Usage
const client = new KitchenClickClient({
  clientId: process.env.KITCHENCLICK_CLIENT_ID,
  clientSecret: process.env.KITCHENCLICK_CLIENT_SECRET,
});

Load Full Menu Structure

javascript
async function loadMenuForDisplay(locationId, conceptId) {
  const client = new KitchenClickClient(config);

  // Get available menus
  const { data: menus } = await client.getMenus(locationId, conceptId);

  // Filter to currently available menus
  const availableMenus = menus.filter(menu => {
    if (menu.status !== 'live') return false;
    return isMenuAvailableNow(menu);
  });

  if (availableMenus.length === 0) {
    return { menus: [], items: [] };
  }

  // Load items for the first available menu
  const { data } = await client.getMenuItems(
    locationId,
    conceptId,
    availableMenus[0].hashkey
  );

  return {
    menus: availableMenus,
    currentMenu: data.menu,
    categories: data.hierarchy,
  };
}

function isMenuAvailableNow(menu) {
  if (!menu.availability_rules) return true;

  const now = new Date();
  const dayOfWeek = now.toLocaleDateString('en-US', { weekday: 'long' }).toLowerCase();
  const currentTime = now.toTimeString().slice(0, 5);

  const { days, time_ranges } = menu.availability_rules;

  if (!days.includes(dayOfWeek)) return false;

  return time_ranges.some(range =>
    currentTime >= range.start && currentTime <= range.end
  );
}
javascript
function flattenMenuItems(hierarchy) {
  const items = [];

  function traverse(nodes, parentCategory = null) {
    for (const node of nodes) {
      if (node.type === 'item') {
        items.push({
          ...node,
          category: parentCategory,
        });
      }

      if (node.children?.length > 0) {
        traverse(
          node.children,
          node.type === 'category' ? node.name : parentCategory
        );
      }
    }
  }

  traverse(hierarchy);
  return items;
}

// Usage
const { categories } = await loadMenuForDisplay(locationId, conceptId);
const allItems = flattenMenuItems(categories);

// Search items
const searchResults = allItems.filter(item =>
  item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
  item.description?.toLowerCase().includes(searchTerm.toLowerCase())
);

Shopping Cart

Cart Manager

javascript
class CartManager {
  constructor() {
    this.items = [];
  }

  addItem(item, quantity = 1, modifiers = [], notes = '') {
    const existingIndex = this.items.findIndex(
      i => i.itemId === item.hashkey &&
           JSON.stringify(i.modifiers) === JSON.stringify(modifiers) &&
           i.notes === notes
    );

    if (existingIndex >= 0) {
      this.items[existingIndex].quantity += quantity;
    } else {
      this.items.push({
        itemId: item.hashkey,
        name: item.name,
        basePrice: item.base_price,
        quantity,
        modifiers,
        notes,
      });
    }

    return this.items;
  }

  updateQuantity(index, quantity) {
    if (quantity <= 0) {
      this.items.splice(index, 1);
    } else {
      this.items[index].quantity = quantity;
    }
    return this.items;
  }

  removeItem(index) {
    this.items.splice(index, 1);
    return this.items;
  }

  clear() {
    this.items = [];
    return this.items;
  }

  getSubtotal() {
    return this.items.reduce((total, item) => {
      const modifierTotal = item.modifiers.reduce(
        (sum, m) => sum + (m.priceAdjustment || 0),
        0
      );
      return total + (item.basePrice + modifierTotal) * item.quantity;
    }, 0);
  }

  toOrderItems() {
    return this.items.map(item => ({
      item_id: item.itemId,
      quantity: item.quantity,
      special_instructions: item.notes || undefined,
      modifiers: item.modifiers.map(m => ({
        modifier_id: m.hashkey,
        quantity: 1,
      })),
    }));
  }
}

// Usage
const cart = new CartManager();

// Add item with modifiers
cart.addItem(
  { hashkey: 'itm_xxxxx', name: 'Burger', base_price: 12.99 },
  1,
  [{ hashkey: 'mod_cheddar', name: 'Cheddar', priceAdjustment: 0 }],
  'No onions'
);

Order Flow

Complete Order Submission

javascript
async function submitOrder(client, locationId, cart, customer, orderType) {
  // 1. Calculate totals
  const calcResult = await client.calculateOrder({
    location_id: locationId,
    order_type: orderType,
    items: cart.toOrderItems(),
  });

  console.log('Order total:', calcResult.data.totals.total);

  // 2. Create the order
  const orderResult = await client.createOrder({
    location_id: locationId,
    customer: {
      name: customer.name,
      phone: customer.phone,
      email: customer.email,
    },
    order_type: orderType,
    items: cart.toOrderItems(),
    payment_method: 'cash',
  });

  console.log('Order created:', orderResult.data.order_number);

  return orderResult.data;
}

// With error handling
async function submitOrderSafe(client, locationId, cart, customer, orderType) {
  try {
    return await submitOrder(client, locationId, cart, customer, orderType);
  } catch (error) {
    if (error.errors?.items) {
      // Handle item availability issues
      console.error('Some items are unavailable:', error.errors.items);
      throw new Error('Some items in your cart are no longer available');
    }

    if (error.code === 'location_closed') {
      throw new Error('Sorry, this location is currently closed');
    }

    throw error;
  }
}

Order Tracking

Polling with Status Updates

javascript
class OrderTracker {
  constructor(client, orderId, onUpdate) {
    this.client = client;
    this.orderId = orderId;
    this.onUpdate = onUpdate;
    this.lastStatus = null;
    this.polling = false;
  }

  start(intervalMs = 10000) {
    if (this.polling) return;

    this.polling = true;
    this.poll(intervalMs);
  }

  stop() {
    this.polling = false;
  }

  async poll(intervalMs) {
    if (!this.polling) return;

    try {
      const { data } = await this.client.trackOrder(this.orderId);

      if (data.status !== this.lastStatus) {
        this.lastStatus = data.status;
        this.onUpdate(data);
      }

      // Stop polling when order is complete
      if (['completed', 'cancelled'].includes(data.status)) {
        this.stop();
        return;
      }
    } catch (error) {
      console.error('Tracking error:', error);
    }

    if (this.polling) {
      setTimeout(() => this.poll(intervalMs), intervalMs);
    }
  }
}

// Usage
const tracker = new OrderTracker(client, 'ord_xxxxx', (data) => {
  console.log(`Order status: ${data.status}`);

  if (data.status === 'ready') {
    showNotification('Your order is ready for pickup!');
  }
});

tracker.start(10000); // Poll every 10 seconds

Error Handling

Comprehensive Error Handler

javascript
function handleApiError(error) {
  // Network error
  if (error.name === 'TypeError' && error.message === 'Failed to fetch') {
    return {
      type: 'network',
      message: 'Unable to connect. Please check your internet connection.',
      retry: true,
    };
  }

  // Rate limited
  if (error.code === 'rate_limit_exceeded') {
    return {
      type: 'rate_limit',
      message: 'Too many requests. Please wait a moment and try again.',
      retry: true,
      retryAfter: error.retry_after_seconds || 60,
    };
  }

  // Validation errors
  if (error.errors) {
    const messages = Object.entries(error.errors)
      .flatMap(([field, errors]) => errors)
      .join('. ');

    return {
      type: 'validation',
      message: messages || error.message,
      errors: error.errors,
      retry: false,
    };
  }

  // Business logic errors
  const businessErrors = {
    location_closed: 'This location is currently closed.',
    item_unavailable: 'Some items are no longer available.',
    minimum_not_met: 'Your order does not meet the minimum.',
    delivery_unavailable: 'Delivery is not available to your address.',
  };

  if (businessErrors[error.code]) {
    return {
      type: 'business',
      message: businessErrors[error.code],
      retry: false,
    };
  }

  // Generic error
  return {
    type: 'unknown',
    message: error.message || 'An unexpected error occurred.',
    retry: true,
  };
}

// Usage with retry
async function withRetry(fn, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      const handled = handleApiError(error);

      if (!handled.retry || attempt === maxRetries - 1) {
        throw handled;
      }

      const delay = handled.retryAfter
        ? handled.retryAfter * 1000
        : Math.pow(2, attempt) * 1000;

      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

Changelog
DateChange
2026-01-15Initial publication.

ShopHero CommerceCore Platform