Overview
Access product data through two methods:getStore()- Returns all products in the store (included in store response)getProduct()- Fetch a single product by ID or sluggetProductReviews()- Get paginated reviews for a product
Get All Products
Products are included in the store response:Copy
const response = await komerza.getStore();
if (response.success) {
const products = response.data.products;
products.forEach((product) => {
console.log(product.name, product.variants.length, "variants");
});
}
Get Single Product
Fetch a product by ID or slug:Copy
const response = await komerza.getProduct("product-id-or-slug");
Parameters
The product’s unique ID (UUID) or URL slug
Example
Copy
// By ID
const byId = await komerza.getProduct("550e8400-e29b-41d4-a716-446655440000");
// By slug
const bySlug = await komerza.getProduct("premium-license");
if (bySlug.success) {
const product = bySlug.data;
console.log(product.name);
console.log(product.description);
console.log(product.variants);
}
Product Object
Copy
interface Product {
id: string; // Unique identifier
name: string; // Product name
description: string; // Description (supports markdown)
slug?: string; // URL-friendly slug
imageNames: string[]; // Product image filenames (see Images page)
rating: number; // Average review rating
order: number; // Display order
visibility: number; // 0=Public, 1=Unlisted, 2=Private
storeId: string; // Parent store ID
variants: Variant[]; // Product variants
dateCreated: string; // ISO 8601 timestamp
}
Variants
Every product has one or more variants. Each variant has its own price, stock, and settings:Copy
interface Variant {
id: string; // Variant ID
name: string; // Variant name
productId: string; // Parent product ID
storeId: string; // Store ID
cost: number; // Price in store currency
stock: number; // Available stock
stockMode: number; // Stock calculation mode (see below)
minimumQuantity: number; // Min purchase quantity (default: 1)
maximumQuantity: number; // Max purchase quantity (-1 = unlimited)
order: number; // Display order
imageNames: string[]; // Variant-specific image filenames
type: number; // 0=One-time, 1=Subscription
subscriptionPeriod?: string; // Subscription duration if applicable
requireDiscordAuthorization: boolean;
dateCreated: string;
}
Stock Calculation Mode
ThestockMode field determines how stock availability is calculated:
| Value | Mode | Description |
|---|---|---|
| 0 | Calculated | Stock is automatically calculated from available items |
| 1 | Ignored | Stock is not tracked—always available |
| 2 | Fixed | Stock is manually set and decremented on purchase |
Displaying Images
Product and variantimageNames contain filenames, not full URLs. See the Images page for how to construct full image URLs.
Display Variants
Copy
async function displayProduct(productId) {
const [productResponse, formatter] = await Promise.all([
komerza.getProduct(productId),
komerza.createFormatter(),
]);
if (!productResponse.success) return;
const product = productResponse.data;
// Sort variants by order
const variants = product.variants.sort((a, b) => a.order - b.order);
variants.forEach((variant) => {
const price = formatter.format(variant.cost);
const inStock = variant.stockMode === 1 || variant.stock > 0;
console.log(`${variant.name}: ${price}`);
console.log(` Stock: ${inStock ? "Available" : "Out of stock"}`);
console.log(` Min qty: ${variant.minimumQuantity}`);
console.log(
` Max qty: ${
variant.maximumQuantity === -1 ? "Unlimited" : variant.maximumQuantity
}`
);
});
}
Variant Selection
Copy
<select id="variant-select" onchange="updatePrice()">
<!-- Populated dynamically -->
</select>
<span id="price"></span>
<button onclick="addToCart()">Add to Cart</button>
<script>
let currentProduct;
let formatter;
async function loadProduct(productId) {
[{ data: currentProduct }, formatter] = await Promise.all([
komerza.getProduct(productId),
komerza.createFormatter(),
]);
const select = document.getElementById("variant-select");
select.innerHTML = currentProduct.variants
.map(
(v) =>
`<option value="${v.id}">${v.name} - ${formatter.format(
v.cost
)}</option>`
)
.join("");
updatePrice();
}
function updatePrice() {
const variantId = document.getElementById("variant-select").value;
const variant = currentProduct.variants.find((v) => v.id === variantId);
document.getElementById("price").textContent = formatter.format(
variant.cost
);
}
function addToCart() {
const variantId = document.getElementById("variant-select").value;
komerza.addToBasket(currentProduct.id, variantId);
}
</script>
Product Reviews
Get paginated reviews for a product:Copy
const response = await komerza.getProductReviews(productId, page);
Parameters
The product’s unique ID
Page number (1-indexed)
Returns
ReturnsPaginatedApiResponse<Review>:
Copy
interface PaginatedApiResponse<Review> {
success: boolean;
pages: number; // Total number of pages
data?: Review[]; // Array of reviews
message?: string;
}
interface Review {
id: string;
rating: number; // 1-5 stars
reason: string; // Review text
productId: string;
reply?: string; // Merchant reply if any
dateCreated: string;
}
Display Reviews
Copy
async function displayReviews(productId, page = 1) {
const response = await komerza.getProductReviews(productId, page);
if (!response.success) return;
const container = document.getElementById("reviews");
container.innerHTML = response.data
.map(
(review) => `
<div class="review">
<div class="rating">${"★".repeat(review.rating)}${"☆".repeat(
5 - review.rating
)}</div>
<p>${review.reason}</p>
${
review.reply
? `<p class="reply"><strong>Response:</strong> ${review.reply}</p>`
: ""
}
<time>${new Date(review.dateCreated).toLocaleDateString()}</time>
</div>
`
)
.join("");
// Pagination
if (response.pages > 1) {
container.innerHTML += `
<div class="pagination">
${
page > 1
? `<button onclick="displayReviews('${productId}', ${
page - 1
})">Previous</button>`
: ""
}
<span>Page ${page} of ${response.pages}</span>
${
page < response.pages
? `<button onclick="displayReviews('${productId}', ${
page + 1
})">Next</button>`
: ""
}
</div>
`;
}
}
Product Images
Product images are referenced by filename. Construct the full URL:Copy
function getProductImageUrl(storeId, productId, imageName) {
return `https://cdn.komerza.com/stores/${storeId}/products/${productId}/${imageName}`;
}
// Usage
const product = response.data;
const imageUrl = getProductImageUrl(
product.storeId,
product.id,
product.imageNames[0]
);
Image Gallery
Copy
function displayProductImages(product) {
const gallery = document.getElementById("gallery");
// Use variant images if available, otherwise product images
const images =
product.variants[0].imageNames.length > 0
? product.variants[0].imageNames
: product.imageNames;
gallery.innerHTML = images
.map(
(img) => `
<img
src="https://cdn.komerza.com/stores/${product.storeId}/products/${product.id}/${img}"
alt="${product.name}"
loading="lazy"
/>
`
)
.join("");
}
Stock Handling
Check stock availability before adding to cart:Copy
function canAddToCart(variant, quantity) {
// Stock mode 1 = Ignored (unlimited)
if (variant.stockMode === 1) return true;
// Check available stock
if (variant.stock < quantity) return false;
// Check quantity limits
if (quantity < variant.minimumQuantity) return false;
if (variant.maximumQuantity !== -1 && quantity > variant.maximumQuantity)
return false;
return true;
}
function getStockStatus(variant) {
if (variant.stockMode === 1) return "In Stock";
if (variant.stock === 0) return "Out of Stock";
if (variant.stock < 10) return `Only ${variant.stock} left`;
return "In Stock";
}
Complete Example
Copy
<!DOCTYPE html>
<html>
<head>
<title>Product Page</title>
<script src="https://cdn.komerza.com/komerza.min.js"></script>
<style>
.product {
max-width: 600px;
margin: 0 auto;
}
.gallery img {
max-width: 100%;
}
.variants {
margin: 20px 0;
}
.reviews {
margin-top: 40px;
}
.review {
border-bottom: 1px solid #eee;
padding: 15px 0;
}
.rating {
color: gold;
}
</style>
</head>
<body>
<div class="product">
<div id="gallery"></div>
<h1 id="name"></h1>
<p id="description"></p>
<p id="rating"></p>
<div class="variants">
<select id="variant-select"></select>
<span id="price"></span>
<span id="stock"></span>
</div>
<div>
<label
>Quantity: <input type="number" id="quantity" value="1" min="1"
/></label>
<button id="add-btn" onclick="addToCart()">Add to Cart</button>
</div>
<div class="reviews">
<h2>Reviews</h2>
<div id="reviews"></div>
</div>
</div>
<script>
komerza.init("your-store-id");
let product;
let formatter;
async function loadProduct(idOrSlug) {
const [productResponse, fmt] = await Promise.all([
komerza.getProduct(idOrSlug),
komerza.createFormatter(),
]);
if (!productResponse.success) {
alert("Product not found");
return;
}
product = productResponse.data;
formatter = fmt;
// Display product info
document.getElementById("name").textContent = product.name;
document.getElementById("description").textContent =
product.description;
document.getElementById(
"rating"
).textContent = `${product.rating}★ rating`;
// Display images
const images = product.imageNames;
document.getElementById("gallery").innerHTML = images
.map(
(img) =>
`<img src="https://cdn.komerza.com/stores/${product.storeId}/products/${product.id}/${img}" />`
)
.join("");
// Populate variants
const select = document.getElementById("variant-select");
select.innerHTML = product.variants
.map((v) => `<option value="${v.id}">${v.name}</option>`)
.join("");
select.onchange = updateVariantDisplay;
updateVariantDisplay();
loadReviews(1);
}
function updateVariantDisplay() {
const variantId = document.getElementById("variant-select").value;
const variant = product.variants.find((v) => v.id === variantId);
document.getElementById("price").textContent = formatter.format(
variant.cost
);
// Stock status
let stockText = "In Stock";
if (variant.stockMode !== 1) {
if (variant.stock === 0) stockText = "Out of Stock";
else if (variant.stock < 10) stockText = `Only ${variant.stock} left`;
}
document.getElementById("stock").textContent = stockText;
// Update quantity limits
const qtyInput = document.getElementById("quantity");
qtyInput.min = variant.minimumQuantity;
qtyInput.max =
variant.maximumQuantity === -1 ? 999 : variant.maximumQuantity;
qtyInput.value = variant.minimumQuantity;
// Disable add button if out of stock
document.getElementById("add-btn").disabled =
variant.stockMode !== 1 && variant.stock === 0;
}
function addToCart() {
const variantId = document.getElementById("variant-select").value;
const quantity = parseInt(document.getElementById("quantity").value);
komerza.addToBasket(product.id, variantId, quantity);
alert("Added to cart!");
}
async function loadReviews(page) {
const response = await komerza.getProductReviews(product.id, page);
if (!response.success || response.data.length === 0) {
document.getElementById("reviews").innerHTML =
"<p>No reviews yet</p>";
return;
}
document.getElementById("reviews").innerHTML = response.data
.map(
(review) => `
<div class="review">
<div class="rating">${"★".repeat(review.rating)}${"☆".repeat(
5 - review.rating
)}</div>
<p>${review.reason}</p>
${review.reply ? `<p><em>Reply: ${review.reply}</em></p>` : ""}
</div>
`
)
.join("");
}
// Load product from URL or default
const urlParams = new URLSearchParams(window.location.search);
const productId = urlParams.get("product") || "your-default-product-slug";
loadProduct(productId);
</script>
</body>
</html>