Skip to content

iSeller — Adapter Guide

Status: SKELETON. This pack was scaffolded with the new-POS onboarding framework. The iSeller Public API v2 docs are received and authentication is documented below; per-topic endpoints + sample payloads are still being gathered. Sections marked [NEEDS API DOCS] / [TBC] are intake questions to answer; do not generate a topic's script until that topic has an endpoint and a real sample payload (see the onboarding playbook). So far test_connection and list_remote_outlets are generated (scripts/adapters/iseller/); the remaining topics are still skeletons.

iSeller is an Indonesian cloud POS / commerce platform. Fill this in as the integrator supplies answers (paste a docs URL and samples into the onboarding conversation).

Provider Info

Field Value
Slug iseller
Auth Type oauth (OAuth 2.0 — Authorization Code or Client Credentials)
Base URL Per-store resource_urlhttps://{storename}.isellershop.com; OAuth/token endpoints are global on https://isellershop.com
API Docs iSeller Public API v2 + ERP API v3/v4 + Webhook Notification (partner PDFs)
Sample fixtures samples/iseller/ — derived from the vendor docs
Sandbox access [TBC]
Topics in scope test_connection, list_remote_outlets, sync_products, order_dispatch, order_status_update (optional: sync_outlet, snooze_items)

Authentication

iSeller uses OAuth 2.0 (auth_type = oauth). Two grant types are available; the token endpoint is global, the API base URL is per-store.

Authorization Code (per-merchant, self-service)

  1. Send the merchant to https://isellershop.com/oauth/authorize?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&state={STATE} → they log in to their store and grant access.
  2. iSeller redirects to {REDIRECT_URI}?code=…&state=….
  3. Exchange the code: POST https://isellershop.com/oauth/token with grant_type=authorization_code, code, redirect_uri, client_id, client_secret
{
  "access_token": "…",
  "token_type": "bearer",
  "expires_in": 1209599,
  "refresh_token": "…",
  "resource_url": "https://{storename}.isellershop.com"
}
  1. Call the API at {resource_url}/api/v2/… with header Authorization: Bearer {access_token}.
  2. Refresh: POST https://isellershop.com/oauth/token with grant_type=refresh_token, refresh_token, client_id, client_secret.

expires_in ≈ 1,209,599s (~14 days). iSeller also ships a JS SDK (iSellerOAuth.Open(...)) for embedding the grant dialog — relevant to the self-service Connect UI (INTR-766).

Client Credentials (app-level — used for Phase 1)

POST https://isellershop.com/oauth/token with grant_type=client_credentials, client_id, client_secret → token response. Verified (Flash Coffee sandbox): the response includes access_token, token_type:"bearer", expires_in (≈14 days), refresh_token, and resource_url (the store base URL, e.g. https://flashcoffee.isellershop.com) — so baseUrl comes straight from the token.

How it maps to a provider connection

Connection field iSeller value
apiKey client_id
apiSecret client_secret
baseUrl store URL https://{storename}.isellershop.com (the resource_url)
config (optional) { "tokenUrl": "https://isellershop.com/oauth/token" }

⚠️ Token host ≠ API host. Mint the token at the global https://isellershop.com/oauth/token; call data at the per-store https://{storename}.isellershop.com. Do not use baseUrl for the token request.

What the adapter scripts do (Client Credentials path)

Scripts mint a fresh bearer token at the start of each run — no stored token, no expiry handling needed:

// every iSeller script begins with a token mint.
// IMPORTANT: the token endpoint needs application/x-www-form-urlencoded — pass a
// STRING body (the runtime sends strings verbatim; an object would be JSON-encoded
// and the endpoint returns HTTP 400).
var form = "grant_type=client_credentials"
  + "&client_id=" + encodeURIComponent(input.apiKey)
  + "&client_secret=" + encodeURIComponent(input.apiSecret);
var resp = context.http.post(
  "https://isellershop.com/oauth/token",
  form,
  { "Content-Type": "application/x-www-form-urlencoded" }
);
var token = resp.body.access_token;
// then call the per-store API with the bearer token (Get* calls are POST + JSON body):
var data = context.http.post(input.baseUrl + "/api/v2/GetStoreInfo", {}, { "Authorization": "Bearer " + token });

Secrets stay in the Go-resolved connection fields; the script receives only input.apiKey / input.apiSecret / input.baseUrl.

🛑 Script input contract — applies to EVERY iSeller adapter script (test_connection, list_remote_outlets, sync_products, order_dispatch, order_status_update). The transform receives input.apiKey, input.apiSecret, input.baseUrl — nothing else for auth. Each script mints its own bearer token per run from apiKey/apiSecret (the snippet above) and prefers tokenResp.body.resource_url over input.baseUrl for the API host.

Do NOT read input.config.accessToken / input.config.refreshToken. There is no stored token on the Phase-1 (Client Credentials) path — requiring input.config.* makes the script fail at test time with input.config.accessToken is required. The stored-token model belongs to the future Authorization Code flow (INTR-766) and is out of scope for these scripts. When in doubt, mirror the already-activated list_remote_outlets / test_connection scripts in scripts/adapters/iseller/.

For the self-service Authorization Code flow (browser grant + tokens stored in config.accessToken/refreshToken with server-side auto-refresh), see INTR-766 (OAuth Connect UI + token refresh). Not required for Phase 1.

Logging convention (all topics)

Every script should call context.log(...) at each meaningful step so the job-log dialog shows live progress (especially for long syncs). Prefix every message with iseller.<topic>: to match the activated scripts. Log at least: token mint, each paged fetch + running count, any skipped rows (with the reason), and a final summary count. Keep messages free of secrets (never log the token, apiSecret, or full auth headers).

context.log("iseller.sync_products: minting bearer token");
context.log("iseller.sync_products: fetching page " + page);
context.log("iseller.sync_products: page " + page + " returned " + batch.length + " (total " + products.length + ")");
context.log("iseller.sync_products: skipping product with no item_code (product_id=" + p.product_id + ")");
context.log("iseller.sync_products: mapped " + out.length + " products (" + nStandard + " standard, " + nVariant + " variant, " + nBundle + " comboset)");

Connection & outlet settings

What to store where when connecting iSeller.

provider_connections (tenant-level) — one row for the iseller provider:

Field Value
apiKey iSeller client_id
apiSecret iSeller client_secret
baseUrl store URL https://{storename}.isellershop.com (the resource_url)
config Phase 1: leave empty. (Future: server-managed OAuth token state — see ADR 0006.)

Phase 1 (Client Credentials) — the ONLY model these adapter scripts use. A connection has just apiKey / apiSecret / baseUrl. Scripts mint a token per run from those; there is no stored token, so scripts never read config / config.accessToken / config.refreshToken. Adding a precondition like if (!input.config.accessToken) throw … is a bug — it fails at test time because config is empty.

Future only (INTR-766) — Authorization Code stored-token config shape. Adapter scripts MUST NOT read this. When the self-service Authorization Code flow lands, a token manager writes/ refreshes this `config` server-side. It is **not** consumed by the adapter scripts (they still receive a usable token path via the Phase-1 contract):
{
  "accessToken": "… DO NOT read from a script …",
  "refreshToken": "…",
  "expiresAt": "2026-06-23T00:00:00Z",
  "resourceUrl": "https://{storename}.isellershop.com",
  "tokenUrl": "https://isellershop.com/oauth/token"
}

outlet_providers.settings (per-outlet) — one row per mapped Cata outlet:

{ "outletCode": "MO", "outletId": "134b57bb-2d34-fe5c-39dd-58b6fb035341" }
Field Value Used by
outletCode iSeller outlet_code (e.g. "MO") order_dispatchCreateOrder.outlet_code
outletId iSeller outlet_id GUID resolving GetProducts.outlet_prices[].outlet_id / GetStoreInfo.active_outlets[] to this outlet (per-outlet pricing)

iSeller exposes two outlet identifiers and different endpoints key on different ones: outlet_code (CreateOrder.outlet_code) vs outlet_id GUID (GetProducts.outlet_prices, GetStoreInfo). The outlet external_id stays the outlet_code (the dispatch key — see list_remote_outlets); the GUID lives in settings.outletId. Do not pack both into external_idorder_dispatch reads external_id directly and GUIDs contain hyphens, so a composite would be ambiguous and break dispatch.

Resolving the GUID (lazy, no FE/list_remote_outlets change needed): GetOutlets returns both outlet_code and the outlet_id GUID, so a script that needs the map calls GetOutlets once per run, builds outlet_id → outlet_code, and re-keys outlet_prices by outlet_code (= external_id). Per-run resolution is always fresh (the JS VM is recreated each run, so cross-run caching would have to be Go-side or persisted — persist settings.outletId at mapping time only if per-run lookups ever become a cost). census/iseller.js does this for sync-doctor pricing.

API Endpoints

iSeller Public API v2 is a pull / reporting API. Calls are POST with a JSON request body (despite the Get* names — the docs define each call's filters as a "request body"), under {resource_url}/api/v2 (the per-store base URL). Send Authorization: Bearer {access_token} on every call.

Endpoint Use for Notes
POST /api/v2/GetStoreInfo test_connection · sync_outlet store info + active_outlets[] + payment types (no params)
POST /api/v3/GetOutlets list_remote_outlets ERP API — outlets incl. outlet_code; page/page_size (max 200) + has_next_item
POST /api/v2/GetProducts sync_products products incl. variants, combos, modifier groups, per-outlet prices, inventory
POST /api/v2/GetOrders order status (pull only) orders + fulfillment status; no push/webhook
POST /api/v2/GetOrderSummary · GetTransactions · GetTransactionSummary · GetRegisterShifts reporting (not used by the 5 core topics)
GET /api/v1/version test_connection (lightweight alt)
POST /api/v4/CreateOrder order_dispatch ERP API — create order (Online Ordering); returns order_id
POST /api/v3/CancelOrder · RefundOrder (cancel / refund) ERP API (future)

Two API families on the same per-store base URL: Public API v2 (/api/v2/, read-only Get* — outlets, products, orders) and the ERP API v3/v4 (/api/v3/, /api/v4/ — writes: CreateOrder, CancelOrder, RefundOrder, CreateProducts, …). order_dispatch uses ERP POST /api/v4/CreateOrder (Online Ordering); status returns via the inbound webhook (order_status_update).

Pagination

Page-based: page + page_size in the JSON request body (on GetProducts, GetOrders, etc.) — not cursor / meta.next. context.http.getAll won't auto-detect this; the script loops page until has_next_item is false (ERP list calls) or a short/empty page. Incremental sync via modified_after (ISO-8601, no timezone).

Rate Limits

Not documented in the PDF — [verify with vendor]. Respect 429 + backoff defensively.

Quirks & Constraints

Quirk Detail Confidence
Two API families Public API v2 (/api/v2/, read-only) for catalog/outlets; ERP API v3/v4 (/api/v3/, /api/v4/) for writes incl. CreateOrder. Plus inbound webhooks for status. Same per-store base URL. VERIFIED (docs)
Response key casing varies per endpoint Verify each endpoint by curl — casing is inconsistent: GetOutletsOutlets (PascalCase) with id/name; GetStoreInfostore_info (snake_case) with active_outlets[] of outlet_id/outlet_name. Read tolerantly (body.Outlets \|\| body.outlets). Metadata (status, error_message, has_next_item) is snake_case. VERIFIED (curl)
200 + status:false on error Calls can return HTTP 200 with status:false + error_message for body-level errors — check status, not just the HTTP code. VERIFIED (docs/curl)
Prices are whole-number IDR price is rupiah (e.g. 100000 = Rp100,000), not cents — do not divide by 100. IDR has no minor unit; pass through to basePrice. VERIFIED (docs)
Per-outlet pricing outlet_prices[] gives a price per outlet; top-level price is the default. Use the outlet's price when syncing for a specific outlet. VERIFIED
IDs are GUID strings product_id, outlet_id, modifier_group_id, … are GUIDs — already strings, no conversion. VERIFIED
🔴 list_remote_outlets id = outlet_code, NOT the GUID id GetOutlets returns both a GUID id and a short outlet_code (e.g. "MO"). The list's id MUST be outlet_code — it becomes the outlet external_id and order_dispatch sends it verbatim as CreateOrder.outlet_code. Mapping the GUID lists fine but breaks dispatch (surfaces only on the first order, not at mapping time). VERIFIED
Token host ≠ API host Mint token at https://isellershop.com/oauth/token; call data at https://{storename}.isellershop.com. VERIFIED
Timestamps modified_date / modified_after are ISO-8601 without timezone (store-local: WIB/WITA/WIT). VERIFIED
Product types typestandard / variant / composite / comboset / composite_variant; includes=OutletPrice,Tags,ModifierGroups,Modifiers expands extras. VERIFIED

Feature Parity

Topic iSeller Public API v2
test_connection GetStoreInfo
list_remote_outlets GetOutlets (/api/v3) → outlets[] (incl. outlet_code)
sync_products GetProducts
sync_outlet (optional) GetStoreInfo
order_dispatch ERP API POST /api/v4/CreateOrder (Online Ordering); returns order_id
order_status_update Inbound webhook — Order / Fulfillment events (SHA256-signed)
snooze_items ❌ not in API

Topics

Each topic has its own generation guide so the script generator ingests it whole. Shared Authentication, API Endpoints, Pagination, Rate Limits and Quirks live in the sections above and apply to every topic.

Topic Guide
test_connection iseller/test_connection.md
list_remote_outlets iseller/list_remote_outlets.md
sync_products iseller/sync_products.md
order_dispatch iseller/order_dispatch.md
order_status_update iseller/order_status_update.md
sync_outlet (optional) iseller/sync_outlet.md
snooze_items (optional) iseller/snooze_items.md

Changelog

Date Version Change
2026-06-15 1.0.0 order_dispatch.js generated — POST /api/v4/CreateOrder, per-run token mint, maps Cata order → CreateOrder body (output matches the sample request fixture). Required a pipeline change: buildTransformerInput now surfaces the tenant connection apiKey/apiSecret/baseUrl at the top level of the transformer input (Revel-style header-auth providers ignore them; OAuth providers like iSeller mint a token from them). Also extended the Go response handling: probeExternalOrderID recognizes snake_case order_id, and detectBodyError flags boolean status:false + error_message.
2026-06-10 Added Add Product admin-UI screenshots (Flash Coffee "Simple Bundle" comboset) under images/iseller/ and embedded them in the Admin UI reference section (general, pricing, inventory/comboset, include-products Drinks + Snack, shipping/SEO). Human reference only.
2026-06-09 list_remote_outlets.js generated (v1.0.1). 🔴 KB: the list id MUST be outlet_code, not the GUID id — it becomes the outlet external_id and order_dispatch reuses it as CreateOrder.outlet_code. GUID lists fine but breaks dispatch. Added a Quirks row + section callout.
2026-06-09 First script generated: scripts/adapters/iseller/test_connection.js (v1.0.1) — token mint + GetStoreInfo, guards status:false, reads the store_info wrapper. Doc references the source file (no inline code).
2026-06-09 curl-verified GetStoreInfo: wrapper is store_info (snake_case — unlike GetOutlets' PascalCase Outlets), with store_name + active_outlets[]. Confirms casing varies per endpoint; test_connection reads store_info.* and guards status:false.
2026-06-09 curl-verified GetOutlets (Flash Coffee): live key is Outlets (PascalCase; docs show lowercase), items carry outlet_code. Map outlet_codeid; read arrays case-tolerantly. Added quirks: response-key casing + 200-with-status:false. Verify GetProducts casing the same way.
2026-06-09 First-generation fixes: token mint must be form-urlencoded (string body; runtime updated to send string bodies verbatim). list_remote_outlets switched to GetOutlets (/api/v3) which returns outlet_code — resolves the outlet_code/outlet_id gap.
2026-06-09 Token verified (Flash Coffee sandbox): the response returns access_token + refresh_token + expires_in (≈14d) + resource_url — so baseUrl comes from the token.
2026-06-09 Correction: Public API v2 Get* calls are POST with a JSON request body (the docs define each call's filters as a "request body"), not GET.
2026-06-09 ERP API v3/v4 received — order_dispatch fully specced: POST /api/v4/CreateOrder (Online Ordering), returns order_id; mapped Cata order → CreateOrder body. iSeller now has endpoints for all in-scope topics.
2026-06-09 Correction: order_dispatch is in scope (Cata dispatches paid orders to iSeller).
2026-06-09 Mapped endpoints from iSeller Public API v2 + Webhook Notification docs: GetStoreInfo (outlets), GetProducts (catalog), inbound Order/Fulfillment webhooks (SHA256-signed) for status.
2026-06-09 Authentication documented (OAuth 2.0: Authorization Code + Client Credentials).
2026-06-09 Skeleton created via the onboarding framework.

Open uncertainties

  • outlet_code (resolved): GetOutlets (/api/v3) returns outlet_code per outlet, so list_remote_outlets maps it to the outlet external_id and order_dispatch reuses it directly — no separate lookup.
  • Reconciliation: the inbound webhook order_id maps back to the order_id returned by CreateOrder (stored at dispatch); confirm filtering of iSeller-native (pos/web) orders.
  • Status enums: full value sets for status / payment_status / fulfillment_status (docs show only examples).
  • Rate limits: not documented.