Headless WooCommerce
This connects headless WooCommerce to samesake using the wc/v3 REST API: paginate the catalog once, then keep it current with product webhooks. Base URL: https://<store>/wp-json/wc/v3/.
1. Map the Woo product to a collection
Section titled “1. Map the Woo product to a collection”GET /products returns name, slug, description, short_description, sku, price, stock_status, categories, tags, images, attributes, type. Monetary values are strings; descriptions are HTML — strip before indexing.
import { collection, f, Channels } from "@samesake/core";
export const products = collection("products", { fields: { title: f.text({ searchable: true }), // <- Woo "name" description: f.text({ searchable: true }), slug: f.text({ filterable: true }), category: f.text({ filterable: true }), price: f.number({ filterable: true, budget: true }), available: f.boolean({ filterable: true }), // stock_status === "instock" }, embeddings: { doc: { source: "$title $description $category", model: "your-model", dim: 1536 } }, search: { channels: [ Channels.fts({ fields: ["title", "description", "category"], weight: 1 }), Channels.cosine({ embedding: "doc", weight: 1 }), ], combiner: "rrf", },});2. Auth
Section titled “2. Auth”WooCommerce admin → Settings → Advanced → REST API → Add key, Read permission. You get ck_... / cs_.... Over HTTPS, use HTTP Basic (key = user, secret = password):
curl https://store.example/wp-json/wc/v3/products -u ck_xxx:cs_xxx3. Initial index — paginate
Section titled “3. Initial index — paginate”per_page max is 100; total pages come from the X-WP-TotalPages response header. Order by a stable key to avoid skips on a mutating catalog.
async function bulkIndex(base: string, auth: string, matcher: any) { let page = 1, totalPages = 1; do { const res = await fetch(`${base}/products?status=publish&per_page=100&orderby=id&order=asc&page=${page}`, { headers: { Authorization: `Basic ${auth}` }, }); totalPages = Number(res.headers.get("X-WP-TotalPages") ?? 1); const products = await res.json(); await matcher.pushDocuments("shop", "products", products.map(toDoc)); page++; } while (page <= totalPages); await matcher.index("shop", "products");}For variable products (type: "variable"), the parent price is only a range — fetch GET /products/<id>/variations and flatten size/price/stock into the doc (or one doc per variation).
4. Keep in sync — product webhooks
Section titled “4. Keep in sync — product webhooks”Create webhooks (UI: Settings → Advanced → Webhooks, or POST /webhooks) for topics product.created, product.updated, product.deleted pointing at one HTTPS endpoint with a strong shared secret.
The X-WC-Webhook-Signature header is Base64(HMAC-SHA256(raw body, secret)). Hash the raw bytes as received, constant-time compare, ACK 2xx fast (Woo retries and auto-disables after repeated failures), then process. Source: WooCommerce webhooks.
import crypto from "node:crypto";
export function verify(rawBody: Buffer, sigHeader: string, secret: string) { const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("base64"); const a = Buffer.from(sigHeader), b = Buffer.from(expected); return a.length === b.length && crypto.timingSafeEqual(a, b);}
// after verify + res.sendStatus(200):const topic = req.get("X-WC-Webhook-Topic"); // product.updatedconst payload = JSON.parse(rawBody.toString("utf8"));if (topic === "product.deleted") await matcher.removeDocuments("shop", "products", [String(payload.id)]);else { await matcher.pushDocuments("shop", "products", [toDoc(payload)]); await matcher.index("shop", "products"); }A *.deleted payload carries only { id }. On product.updated for a variable product, re-fetch its variations.
5. Recommended shape
Section titled “5. Recommended shape”- Read-only REST key; store on HTTPS so Basic auth works.
- Backfill: paginate
/products?status=publish&per_page=100toX-WP-TotalPages; flatten variations; strip HTML. - Webhooks:
product.created|updated|deleted→ verify raw-body HMAC → push/index or remove. - Backstop: nightly reconciliation with
?modified_after=<ISO8601>to repair missed events.