Developer Docs

← Back to Console

Catalog Feed

The Catalog Feed API (GET /e9v3/catalog) serves the published B2B catalog — a curated, quantity-free, orderable product feed — as a paginated snapshot. It is designed for commerce consumers: static site generators (SSG), headless storefronts, and any integration that needs a stable product list plus prices without reading live inventory.

What it’s for

Unlike GET /e9v3/inventory, the feed is a snapshot: it reflects the last catalog publish, not real-time stock. Republish the catalog (or subscribe to the rebuild webhooks below) to refresh it.

Authentication

Paging

The feed is chunked into batches of up to 500 items each. The version object on every response tells you how many batches exist:

GET /e9v3/catalog?batch=0
{
  "success": true,
  "version": { "publishedAt": "2026-07-06T02:14:00Z", "itemCount": 1200, "batchCount": 3 },
  "batch": 0,
  "items": [ /* up to 500 items */ ],
  "pricing": { "tierId": "wholesale", "version": 4, "currency": "AUD" }
}

Iterate batch = 0 .. version.batchCount - 1 to pull the whole feed. A non-integer or negative batch returns 400; a batch at or beyond version.batchCount returns 404 { "success": false, "error": "Batch out of range" }. A business that has never published a catalog returns 404 { "success": false, "error": "Catalog not published" }.

The effective-SKU join

Each item carries a parent sku and, for variant products, a variantSku:

{
  "productId": "p_123",
  "sku": "SHIRT",
  "variantSku": "SHIRT-BLUE-L",
  "name": "Cotton Shirt",
  "variantName": "Blue / L",
  "barcode": "9312345678900",
  "category": "Apparel",
  "uom": "ea",
  "stockStatus": "in_stock",
  "price": 24.5,
  "gst": true
}

The effective SKU — the key you order and price against — is variantSku when present and non-empty, otherwise sku. Simple (non-variant) products omit variantSku/variantName entirely. This is the same convention the order APIs use, so an effective SKU from the feed round-trips cleanly into POST /e9v3/orders/send-matched.

Prices and the feed tier

Prices are opt-in per business. In the Console (Pricing → Public feed tier) a business selects which price tier is exposed to the feed, stored as feedTierId. When that tier is set and published:

When no feed tier is configured, or the selected tier has not been published, price/gst and the pricing block are omitted — the feed is still fully usable as a product catalog. Items whose effective SKU has no price in the tier also omit price/gst.

Caching with ETags

Every response carries a strong ETag derived from the catalog publish time, the merged price-tier version, and the batch index, plus Cache-Control: no-cache (always revalidate). Send the ETag back on the next request:

GET /e9v3/catalog?batch=0
If-None-Match: "a1b2c3…"

If the batch is unchanged you get 304 Not Modified with an empty body — cheap polling and cheap SSG rebuilds. The ETag changes whenever the catalog or the merged price tier is republished, so it is a reliable “did anything change” signal.

Rebuild webhooks

Rather than poll, subscribe to the publish events and re-pull the feed on change:

Event Fires when
catalog.published The catalog snapshot is republished (products, names, categories, stock status). Payload: { businessId, itemCount?, batchCount? }.
prices.published A feed price tier is republished. Payload: { businessId }.

A common pattern is to wire catalog.published to a CI build hook so a static storefront rebuilds automatically after every catalog publish. See the Webhooks guide for subscription and signature verification details.

Syncing to WooCommerce

Equos can push the published catalog outbound to a WooCommerce store, keeping its stock status and prices in step with what you publish — no polling, no code. This uses the same published snapshot as the feed above.

Direction & policy. This is a one-way, Equos-wins push. On every sync Equos overwrites the matched WooCommerce products’ stock_status (and regular_price when a feed price tier is set); manage_stock is set to false. It never reads back from, nor creates products in, the store.

What syncs (v1)

Matching & the variable-product limitation

Products are matched by SKU (effective SKU = variant SKU when present, else parent SKU). SKUs with no matching WooCommerce product are reported, not created — the result summary lists them (capped) under unmatchedEquosSkus.

Variable products (important for AU stores). v1 matches top-level product SKUs only. WooCommerce variation SKUs are separate objects and are not matched in this round — variations will show up as unmatched. The result includes a hint field flagging this. Variation-level matching is a planned follow-up.

Price mode & GST

Catalog prices are stored ex-GST. The connection’s priceMode controls the regular_price written to WooCommerce:

priceMode regular_price written
inclusive (default) ex-GST price + 10% GST on GST-flagged items (price × 1.10), rounded to 2 dp. Non-GST items are sent as-is.
exclusive the raw ex-GST price, rounded to 2 dp.

Most AU stores display GST-inclusive prices, so inclusive is the default. The GST rate defaults to 0.10 and can be overridden per connection (gstRate). Make sure this matches your store’s own tax settings so prices aren’t double-taxed.

When it runs

Setup (manual, this release)

There is no Console screen for this yet — configure it once by writing a Firestore document at businessdata/{businessId}/integrations/woocommerce:

  1. In WooCommerce, go to WooCommerce → Settings → Advanced → REST API and create a key with Read/Write permissions. Copy the consumer key and consumer secret.

  2. Encrypt the consumer secret (recommended). The integrations document is readable by your own staff, so store the secret encrypted. Generate a key once and run the helper:

    export WOO_TOKEN_ENCRYPTION_KEY="$(openssl rand -base64 32)"   # 32-byte AES-256 key
    printf 'cs_your_consumer_secret' | go run ./integrations/cmd/woo-encrypt
    # → prints the base64 blob for consumerSecretEncrypted

    Set the same WOO_TOKEN_ENCRYPTION_KEY in the worker’s environment so it can decrypt at sync time.

  3. Write the connection document:

    {
      "providerId": "woocommerce",
      "providerName": "WooCommerce",
      "enabled": true,
      "storeUrl": "https://store.example.com",
      "consumerKey": "ck_xxx",
      "consumerSecretEncrypted": "<blob from step 2>",
      "priceMode": "inclusive",   // or "exclusive"
      "gstRate": 0.10,             // optional; default 0.10
      "syncStock": true,           // optional; default true
      "syncPrice": true            // optional; default true
    }

    Plaintext fallback. If you cannot run the helper, you may instead set "consumerSecret": "cs_xxx" in plaintext. The worker will use it but logs a warning on every sync — prefer the encrypted field.

Set "enabled": false (or delete the document) to stop syncing.

Result summary

After each run the worker writes health and a summary back onto the same document:

partial means some batch updates failed (details in result.errors); the run is still recorded. Re-publishing the catalog or price tier re-syncs.