Skip to content

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 read input.config.accessToken — there is no stored token here.
  • API call: POST {baseUrl}/api/v2/GetProducts with JSON body { "page": 1, "page_size": 200, "track_inventory": false, "includes": "OutletPrice,Tags,ModifierGroups,Modifiers,Bundles" } (loop page; modified_after for incremental). Bundles must be in includes or bundles comes back null for combosets.
  • Item code: sku (fall back to product_id GUID when sku is empty).
  • Price: price is a decimal IDR string/number (e.g. 18000.0000, 12727.0000) — pass through as-is (no /100). Use the matching outlet_prices[].price when syncing per outlet.

Verified (Flash Coffee, curl — two captures): - Top-level key is products (lowercase). Envelope also carries has_next_item, status, error_message, time, error_detail (all snake_case). Page with has_next_item, not a total count — loop while has_next_item:true; treat HTTP 200 + status:false as an error. - track_inventory:true returns an empty products:[] for this tenant — Flash Coffee does not track inventory. Must send track_inventory:false. - keyword is a loose/ignored filter: querying keyword:"Americano" and keyword:"Latte" both returned the same full page (Latte first). Do not rely on keyword to 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. (inventories is [] when track_inventory:false.) - type ("standard") is the kind discriminator; product_type ("Coffee", or null) is a free-text category label, not a discriminator — do not branch on it. - modifier_groups may be null (e.g. "TopUp Card", "Butter Croissant") as well as an array — guard before iterating. - price may be 0.0 or NNNN.0000 (decimal). A 0.0 base 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. - Modifier price appears net of tax while base price looks 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 own product_id / sku / price, sharing a common product_header_id with its siblings, and a variant_options[] of { option, value, bundles } describing the axis (e.g. three "Kids Barber" rows: {option:"server", value:"sule"|"ucup"|"jonathan"}, all product_header_id: 917f9a24…).

Because Cata sync is flat, the clean mapping is: emit each variant row as its own product (itemCode = sku), disambiguating the name with the variant value (e.g. "Kids Barber — sule"). Group by product_header_id only if Cata later needs a variant parent. Do not drop type:"variant" rows.

Comboset bundles — verified (Flash Coffee, curl — "Simple Bundle"): A comboset is its own sellable product (sku:"BDL001", price:49000.0000, taxable:false, product_type:"Bundles", tags:["bundle"], modifier_groups:null). Its contents live in bundles[] — a flat list (not nested by category; category is 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[] requires includes to contain Bundles — it is null otherwise. 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 in bundles[] is always included (no "choose 1 of N"); the category is just a display grouping and quantity is the count of that item in the package. The bundle is sold as a whole at the parent price.

Mapping: - Emit the comboset as its own product: itemCode = sku, basePrice = price, isBundle = true, modifierGroups = []. - Build bundleSections[] by grouping bundles[] on category — one section per distinct category, in first-seen order: - itemCode: synthesize ${parentSku}::${category} (no native section ID). - name: the category string. - 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), sortNum by order. - quantity: all observed values are 1. Cata's bundleSections[].items has no per-item quantity field, so quantity > 1 is not directly representable — if encountered, emit the item once and flag for review (don't silently drop the multiplier). Revisit if a real quantity > 1 comboset appears.

composite products (recipe/BOM from ingredients) are an inventory concern — for a flat menu sync, treat them like standard (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:

Add Product — General Information, Status, Visibility, Organization

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

Add Product — Pricing, Outlet Prices, Tags

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

Add Product — Inventory type selector (Comboset), SKU

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

Comboset — Include Products, Drinks group

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

Comboset — Include Products, Drinks + Snack groups

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

Add Product — Shipping + SEO slug

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