iSeller — sync_products¶
Part of the iSeller adapter guide — see it for authentication, endpoints, pagination, rate limits and shared quirks.
Contract: Cata flat products — see guidelines. Cata owns menu composition; sync is flat (no menu tree/categories).
- Auth: mint a bearer token per run from
input.apiKey/input.apiSecret(see Script input contract). Do not readinput.config.accessToken— there is no stored token here. - API call:
POST {baseUrl}/api/v2/GetProductswith JSON body{ "page": 1, "page_size": 200, "track_inventory": false, "includes": "OutletPrice,Tags,ModifierGroups,Modifiers,Bundles" }(looppage;modified_afterfor incremental).Bundlesmust be inincludesorbundlescomes backnullfor combosets. - Item code:
sku(fall back toproduct_idGUID whenskuis empty). - Price:
priceis a decimal IDR string/number (e.g.18000.0000,12727.0000) — pass through as-is (no /100). Use the matchingoutlet_prices[].pricewhen syncing per outlet.
Verified (Flash Coffee, curl — two captures): - Top-level key is
products(lowercase). Envelope also carrieshas_next_item,status,error_message,time,error_detail(all snake_case). Page withhas_next_item, not a total count — loop whilehas_next_item:true; treat HTTP 200 +status:falseas an error. -track_inventory:truereturns an emptyproducts:[]for this tenant — Flash Coffee does not track inventory. Must sendtrack_inventory:false. -keywordis a loose/ignored filter: queryingkeyword:"Americano"andkeyword:"Latte"both returned the same full page (Latte first). Do not rely onkeywordto fetch one product — paginate the whole catalog. - Full product field set seen:product_id(GUID),product_header_id,name,description,type,product_type,vendor,attribute,sku,barcode,price,outlet_prices[],taxable,track_inventory,allow_negative_stock,sold_count,unit_of_measurement,buying_price,buying_prices,inventories[],ingredients,variant_options[],bundles,tags[],images[],modifier_groups,is_active,modified_date. (inventoriesis[]whentrack_inventory:false.) -type("standard") is the kind discriminator;product_type("Coffee", ornull) is a free-text category label, not a discriminator — do not branch on it. -modifier_groupsmay benull(e.g. "TopUp Card", "Butter Croissant") as well as an array — guard before iterating. -pricemay be0.0orNNNN.0000(decimal). A0.0base price is legit (e.g. "TopUp Card") — don't skip it. - Modifier groups carry no min/max/required/single-vs-multiple fields, and modifiers carry no sku. Emit groups with sensible defaults (optional, multi-select) until the vendor confirms selection rules. - Modifierpriceappears net of tax while basepricelooks gross: e.g.12727 ×1.10 ≈ 14000,10909 ×1.10 = 12000,9091 ×1.10 = 10000. For sync, pass values through as-is — tax handling is downstream — but flag to the integrator so add-on prices aren't double-taxed at dispatch.
Product → Cata (verified fields):
| iSeller field | Cata field | Notes |
|---|---|---|
sku (or product_id) |
itemCode |
product_id is a GUID; fallback when sku empty |
name / description |
name / description |
|
price (or outlet_prices[].price) |
basePrice |
decimal IDR, pass through |
is_active |
visible |
|
type |
kind discriminator | One of standard (simple item), variant (one concrete variant row), composite (made from ingredients — a recipe/BOM), comboset (a bundle of standard products). Set in the Add Product → Inventory section (Standard / Composite / Comboset buttons; variant comes from adding variant options). |
outlet_prices[] = { outlet_id, outlet_name, price }. For standard products
variant_options / bundles / ingredients were [] / null.
Variants — verified (Flash Coffee, curl): iSeller does not nest variants under one parent. Each variant is a separate product row with
type:"variant", its ownproduct_id/sku/price, sharing a commonproduct_header_idwith its siblings, and avariant_options[]of{ option, value, bundles }describing the axis (e.g. three "Kids Barber" rows:{option:"server", value:"sule"|"ucup"|"jonathan"}, allproduct_header_id: 917f9a24…).Because Cata sync is flat, the clean mapping is: emit each
variantrow as its own product (itemCode = sku), disambiguating the name with the variant value (e.g."Kids Barber — sule"). Group byproduct_header_idonly if Cata later needs a variant parent. Do not droptype:"variant"rows.Comboset bundles — verified (Flash Coffee, curl — "Simple Bundle"): A
combosetis its own sellable product (sku:"BDL001",price:49000.0000,taxable:false,product_type:"Bundles",tags:["bundle"],modifier_groups:null). Its contents live inbundles[]— a flat list (not nested by category;categoryis a field on each line):"bundles": [ { "product_id": "ea701479-…", "sku": "111112", "category": "Snack", "product_name": "Choco Croissant", "variant_name": "", "quantity": 1.000000 }, { "product_id": "fb32afdf-…", "sku": "1234", "category": "Drinks", "product_name": "Latte", "variant_name": "", "quantity": 1.000000 } ]
bundles[]requiresincludesto containBundles— it isnullotherwise. Each line:product_id(GUID of the included standard product),sku,category(the UI grouping),product_name,variant_name(empty unless the line is a specific variant),quantity(decimal, e.g.1.000000). Semantics — RESOLVED: a comboset is a FIXED bundle, not a choice. Every line inbundles[]is always included (no "choose 1 of N"); thecategoryis just a display grouping andquantityis the count of that item in the package. The bundle is sold as a whole at the parentprice.Mapping: - Emit the comboset as its own product:
itemCode = sku,basePrice = price,isBundle = true,modifierGroups = []. - BuildbundleSections[]by groupingbundles[]oncategory— one section per distinct category, in first-seen order: -itemCode: synthesize${parentSku}::${category}(no native section ID). -name: thecategorystring. -minSelection = maxSelection = count of lines in that category— this forces every item (= "always included"), the choice-model encoding of a fixed bundle. -items[]: one per line —itemCode = line.sku,price = null(the bundle is priced as a whole; no per-item upcharge),sortNumby order. -quantity: all observed values are1. Cata'sbundleSections[].itemshas no per-item quantity field, soquantity > 1is not directly representable — if encountered, emit the item once and flag for review (don't silently drop the multiplier). Revisit if a realquantity > 1comboset appears.
compositeproducts (recipe/BOM from ingredients) are an inventory concern — for a flat menu sync, treat them likestandard(their components don't ship to Cata). Not captured live (none in this tenant).
Modifiers (modifier_groups[] → modifierGroups; modifiers[] → options) —
verified field names:
| iSeller field | Cata field | Notes |
|---|---|---|
modifier_group_id (GUID) |
modifierGroups[].itemCode |
|
group title ("Size", "Alternative Milk") |
modifierGroups[].name |
|
group is_active |
filter | skip inactive groups |
modifier_id (GUID) |
options[].itemCode |
no sku exists |
modifier title ("L", "Oat Milk") |
options[].name |
|
modifier price |
options[].additionalPrice |
decimal IDR; 0.0000 = free |
Modifier objects also carry modifier_group_id (back-ref) and quantity
(observed null). No selection-rule fields — apply group defaults.
Scope: sync emits every product the API returns — standard items and
each variant row alike (one Cata product per row). The only rows to skip are
is_active:false (→ visible:false rather than dropped) and rows with no usable
item_code (no sku and no product_id). bundles/comboset rows, when a
tenant has them, are handled separately once their nested shape is captured.
Admin UI reference — Add Product¶
How a Flash Coffee operator creates a product (Catalog → Products → Add Product).
This is the source of the fields GetProducts returns. The screenshots below live
in images/iseller/ (index) and
are human reference only — the generator reads the text/tables here, not the
PNGs.
| UI control | Section | API field |
|---|---|---|
| Product Title (required) | General Information | name |
| Description | General Information | description |
| Media (images) | General Information | images[] |
| Active toggle | Status | is_active |
| Online Store toggle | Visibility | controls online-store/web exposure — the channel Cata web orders use |
| Visible at specific time (start/expiry) | Visibility | (scheduling; not in basic GetProducts) |
| Point of Sale / All Outlets | Visibility | channel/outlet exposure |
| Product Type (e.g. "Coffee", "Bundles") | Organization | product_type — a free-text category label, not the type discriminator |
| Vendor | Organization | vendor |
| Collections | Organization | (collections) |
Tags (e.g. bundle) |
Organization | tags[] |
| Price | Pricing | price |
| Charge taxes on this product | Pricing | taxable |
| Outlet Prices (per outlet) | Pricing | outlet_prices[] {outlet_id, outlet_name, price} |
| Standard / Composite / Comboset | Inventory | type discriminator |
| Stock Keeping Unit (SKU) (required) | Inventory | sku |
| Barcode | Inventory | barcode |
| Unit of Measurement | Inventory | unit_of_measurement |
| Include Products (Comboset only) | Product Policy | populates bundles — standard products grouped by category, each with a Quantity |
| Require Shipping | Shipping | (fulfilment flag) |
SEO slug (e.g. simple-bundle) |
SEO | online-store URL …/product/<slug> |
Notes that matter for sync:
- The UI's "Product Type" (Organization) is the category label
(product_type) — confusingly named; it is not type. The real kind lives
in Inventory (Standard / Composite / Comboset).
- SKU is required in the UI, so sku should normally be present; keep the
product_id fallback for legacy/edge rows anyway.
- A Comboset's Include Products are grouped by category (Drinks, Snack…) with
per-line quantities — this is the bundles payload to capture.
Screenshots (Flash Coffee Sample — "Simple Bundle" comboset)¶
General Information, Status, Visibility, Organization → Product Type:

Pricing → Charge taxes, per-outlet Outlet Prices, Tags, Loyalty:

Inventory → the Standard / Composite / Comboset type selector (sets type), SKU, UoM:

Product Policy → Include Products, one category ("Drinks") with per-line quantities → bundles:

Include Products across two categories ("Drinks" + "Snack") — combosets group by category:

Shipping (Require Shipping) and SEO slug (…/product/simple-bundle):

// @provider iseller @topic sync_products — TO BE GENERATED