Appearance
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,
});Menu Browsing
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
);
}Flatten Menu for Search
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 secondsError 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
| Date | Change |
|---|---|
| 2026-01-15 | Initial publication. |