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" }, indexing: { surfaces: { embed_doc: { kind: "dense", embedding: "doc", build: ({ data }) => `${data.title} ${data.description} ${data.category}`.trim(), }, fts_doc: { kind: "fts", build: ({ data }) => `${data.title} ${data.description} ${data.category}`.trim(), }, }, gate: ({ data }) => (data.title ? { index: true } : { index: false, reason: "missing-title" }), }, embeddings: { doc: { 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.
import type { SamesakeBundle } from "@samesake/postgres";
async function bulkIndex(base: string, auth: string, app: SamesakeBundle) { 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 app.enrich.upsert(products.map(toDoc)); page++; } while (page <= totalPages); await app.enrich.enrich();}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 app.enrich.remove([String(payload.id)]);} else { await app.enrich.upsert([toDoc(payload)]); await app.enrich.enrich();}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 → upsert/enrich or soft-delete. - Backstop: nightly reconciliation with
?modified_after=<ISO8601>to repair missed events.
Run it end to end
Section titled “Run it end to end”The runnable, tested version of this guide lives in examples/connectors — deterministic stubs, no API key. Run bun test (Postgres-backed examples need SAMESAKE_DATABASE_URL).