Catalog: products & variants
Products are items; each item has zero or more variants. Reads are
available on both the public surface (storefront, ACTIVE only) and the
merchant surface (all statuses). Writes are merchant-only and role-gated.
A tiny client
Every example uses this helper. Merchant calls add a bearer token; public calls drop it.
const API = "https://api.litecommerce.io/api/v1";
async function api<T>(path: string, init: RequestInit & { token?: string } = {}) {
const { token, ...rest } = init;
const res = await fetch(`${API}${path}`, {
...rest,
headers: {
"x-organization-slug": "your-store",
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
...rest.headers,
},
});
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return res.json() as Promise<T>;
}Read products (public)
// List active items (optionally ?search= & ?type= & ?limit=)
const items = await api<Item[]>("/public/items");
// One product by slug — 404 if not ACTIVE
const item = await api<Item>("/public/items/trail-pack-38l");Public reads only ever return ACTIVE items. To see DRAFT or archived
records, use the merchant list below.
Public item reads (and collection reads) also carry a rating aggregate —
ratingAverage (null when there are no published reviews) and ratingCount —
so a listing or collection page can render stars without a per-item call. See
Product reviews.
Create a product (merchant)
POST /merchant/items — permission: catalog:write. Owner/Admin retain broad
access; Staff needs that group.
const created = await api<Item>("/merchant/items", {
method: "POST",
token,
body: JSON.stringify({
name: "Trail Pack 38L", // required
slug: "trail-pack-38l", // required, unique per tenant
type: "SALE", // required: SALE | RENTAL | SERVICE
status: "DRAFT", // required: ACTIVE | DRAFT
priceInCents: 18900, // required
// optional:
compareAtPriceInCents: 22900,
description: "Built for long weekend hikes and daily carry.",
shortDescription: "Lightweight 38L hauler.",
brand: "Summit",
vendor: "Summit Gear Co.",
tags: ["packs", "hiking"],
metafields: { warrantyMonths: 24 },
}),
});A duplicate slug returns 409. All prices are integer cents — there are
no floats in the API.
Product copy fields
Use the core copy fields before reaching for metafields:
shortDescriptionis the compact product blurb for cards, grids, and PDP headers.descriptionis the primary product detail page body copy. The admin Description textarea writes this field, so storefronts should render it as the default long description.metafieldsare for structured or tenant-specific details the core schema does not model — ingredients, scent notes, edition numbers, care instructions, dimensions, certifications, and similar extras.
If a tenant needs variant-specific descriptive copy before a first-class variant
description field exists, use metafields.description on the variant as the
conventional override key and fall back to item.description. That override
should be an exception; it should not replace item.description as the default
description source.
Metafields
metafields is an untyped JSON object — a free-form key/value bag, available
on both items and variants — for anything the core schema doesn't model
(warranty length, scent notes, edition numbers).
Because it's untyped, values come back exactly as they were stored; the API
doesn't coerce them. A value written as "01" reads back as the string
"01", not the number 1 — a numeric-looking field may be a string or a number
depending on how it was entered. Coerce on read in your storefront
(Number(m.editionNo), String(m.warrantyMonths)) rather than trusting the
shape of any given key.
Update, archive, restore
await api(`/merchant/items/${id}`, { method: "PATCH", token,
body: JSON.stringify({ status: "ACTIVE", priceInCents: 17900 }) });
// Soft-delete (idempotent). Sets status ARCHIVED; remembers prior status.
await api(`/merchant/items/${id}/archive`, { method: "POST", token });
// Restore returns the item to its pre-archive status.
await api(`/merchant/items/${id}/restore`, { method: "POST", token });Archiving is reversible by design — restore puts the item back at the status it held before, so an accidental archive never loses the publish state.
Variants
Variants live under their item. sku is unique within the item; a collision
returns 409.
// Create
const variant = await api<Variant>(`/merchant/items/${itemId}/variants`, {
method: "POST",
token,
body: JSON.stringify({
name: "Granite", // required
sku: "TP38-GR", // required, unique within the item
priceInCents: 18900, // required
options: { color: "Granite", size: "38L" }, // optional
}),
});
// Update / archive / restore mirror the item endpoints:
await api(`/merchant/items/${itemId}/variants/${variant.id}`, {
method: "PATCH", token, body: JSON.stringify({ priceInCents: 17900 }),
});Reordering
Both items and variants support explicit display order. Send the complete set of non-archived IDs in the desired order; an incomplete list returns 400.
await api("/merchant/items/reorder", { method: "PATCH", token,
body: JSON.stringify({ orderedIds: ["id-a", "id-b", "id-c"] }) });Related
- Collections & pages — group products
- Inventory — stock per variant
- Media uploads — attach product images