WordPress & WooCommerce
WooCommerce deep links
When WooCommerce is active on the WordPress site, the Pitchbar
plugin unlocks a deep integration that goes far beyond the
standard content sync: logged-in shopper context, the
lookup_order tool, coupon emission & apply, lead
push-back, and the abandoned_cart trigger.
None of these endpoints fire if WooCommerce isn't loaded โ the
plugin defers every WC-dependent hookup to the
woocommerce_loaded action so alphabetical plugin
load order doesn't matter.
WooCommerce load-order race
Plenty of pre-v2.0 WordPress integrations have a subtle bug:
they call class_exists('WooCommerce') at
plugins_loaded priority 10 and silently disable WC
features when the check returns false. On sites where the
integration plugin loads alphabetically before
woocommerce/ (e.g. pitchbar <
woocommerce), the WooCommerce main class hasn't been
registered yet at priority 10 โ false โ broken integration with
no error message.
Pitchbar v2.0.0 uses a two-layer guard:
if (function_exists('did_action') && did_action('woocommerce_loaded') > 0) {
$this->bootWoo(); // WC already loaded โ wire up immediately.
} else {
add_action('woocommerce_loaded', [$this, 'bootWoo'], 10);
}
woocommerce_loaded fires after the WC main class is
fully registered, so by the time bootWoo() runs every
class_exists('WooCommerce') check passes
deterministically.
Logged-in shopper context
When a WooCommerce customer is signed in and visits a page that
loads the widget, the plugin attaches a short-lived signed
data-shopper-token to the widget script tag. The
widget forwards it to /api/v1/widget/init; Pitchbar
verifies it and bakes wp_user_id +
email_hash claims into the widget JWT for the chat
session.
What's encoded in the token:
wp_user_idโ the WordPress user ID (integer, sent as string in the token).email_hashโ SHA-256 of the lowercased email. Never the raw email.sourceโ literal"wordpress".expโ unix timestamp, ~24h out so the same token is valid across a normal browsing session.
The token is signed with the workspace's shopper_signing_secret (per-token plaintext, captured by the plugin on first handshake). A bad signature is silently dropped โ the visitor still chats anonymously, no error surfaces.
The lookup_order tool
EcommercePreset's order_status capability maps to a
real tool. When the visitor asks about orders, shipping, returns,
or refunds, the LLM may call
lookup_order(limit, order_number?). The tool:
- Reads the conversation's stored shopper claims (set at
/widget/init). - Resolves the agent's
woocommerce_productsSource'ssite_url. - Pulls a
shopper_signing_secretfrom any active workspace API token. - POSTs an HMAC-signed body to
{site_url}/wp-json/pitchbar/v1/orders/lookupwith a 5-second hard timeout. - Returns the orders payload to the LLM verbatim for it to summarise.
Refusal paths (no callback fired):
- No shopper claim on conversation โ
not_signed_in. The LLM asks the visitor to sign in. - No
woocommerce_productssource โno_wordpress_source. - No active API token with a signing secret โ
no_signing_secret.
The plugin's OrderLookupController returns up to 10
orders (default 5), each with id, number, status, total,
currency, items array, tracking URL (best-effort across
AfterShip, ST tracking, generic _tracking_url
meta), and the customer's order view URL.
Filter to order_number if the visitor asks about a
specific order โ the plugin filters the result set after the
customer-id query so you never expose another customer's data.
Lead push-back
When the chat agent captures a lead (a visitor leaves an email or submits a lead form), Pitchbar mirrors the contact back into the WordPress site:
| Trigger | What happens |
|---|---|
LeadCapturedEvent |
PushLeadToWordPress queued listener fires. It resolves the agent's WordPress or WooCommerce source, signs an HMAC body with the workspace's shopper signing secret, and POSTs to /wp-json/pitchbar/v1/leads. |
| WP receives the call | If WooCommerce is active, plugin creates a WC customer via wc_create_new_customer. Otherwise creates a WP subscriber. pitchbar_lead_id + pitchbar_conversation_id land in user meta so the store owner can correlate the WP user with the Pitchbar transcript. |
Idempotent on email: a second push for the same address updates
the existing user's first_name, billing_phone,
pitchbar_lead_id, and pitchbar_conversation_id
meta instead of creating a duplicate user. Transport failures
are logged and swallowed โ the listener never blocks the
visitor's chat turn.
Coupon emission & apply
Ecommerce agents can emit a <coupon/> block in
chat replies when a visitor's intent + an active WC coupon line
up. The full data flow:
- Plugin's
CouponSyncerruns after every product sync, enumerates publish-statusshop_couponposts, filters expired / over-limit, and POSTs the snapshot to/api/v1/wp/coupons/sync. - Pitchbar persists the list on the source's
config['coupons']array (max 50). PromptBuilderreads them when assembling the system prompt for ecommerce agents. The LLM gets the full code list and an explicit instruction never to invent codes.- When the LLM decides a coupon helps the visitor, it emits an XML marker:
<coupon code="WELCOME10" label="10% off your first order" discount="10%"/>. InlineBlockParserextracts the marker post-stream and emits ablockSSE event of kindcoupon_card.- The widget's
blocks.tsxrenderer materialises a card with a Copy button (writes the code to the clipboard) and an Apply button. - The Apply button POSTs to
/api/v1/widget/coupon/applyon Pitchbar. - Pitchbar HMAC-signs the body with the workspace's shopper signing secret and forwards it to
/wp-json/pitchbar/v1/cart/couponon the WP site. - Because a REST request usually doesn't have a loaded
WC()->cart, the plugin stages the code in a 15-minute transient:pitchbar_pending_coupon_{conversation_id}. - On the visitor's next page load, the plugin's
woocommerce_load_cart_from_sessionhook reads the transient via thepitchbar_conv_idcookie (set by the widget at init) and callsWC()->cart->apply_coupon($code). - The transient is deleted on successful apply; expired transients self-clean after 15 minutes.
Net result: the visitor clicks Apply in chat, walks to the cart page, and sees the discount line already applied โ no copy-paste, no missed codes.
Abandoned cart re-engagement
A BehaviorRule.kind = "abandoned_cart" lets you
re-engage a visitor whose cart has been sitting idle.
Mirror cart state to localStorage
The plugin enqueues cart-state.js on every front-end
page when WooCommerce is active. It mirrors every WC jQuery cart
event into localStorage.pitchbar_cart_state:
{
"items": 2, // running count
"timestamp": 1715472000000 // ms since epoch of last mutation
}
Listens for:
added_to_cartjQuery event โ increment count, refresh timestamp.removed_from_cartjQuery event โ decrement; clear if <= 0.- DOM
clickon.add_to_cart_button/.single_add_to_cart_buttonas a jQuery-free fallback. - URL pathname includes
/checkout,/order-received,/order-pay,/thank-youโ clear the cart state entirely.
Storage failures (private browsing, quota exhausted) are silent no-ops โ the trigger just doesn't fire for that visitor.
Configure the trigger
On the agent's Behavior triggers admin page, add
a new rule of kind abandoned_cart:
{
"kind": "abandoned_cart",
"conditions": { "idle_minutes": 5 },
"action": {
"kind": "open_with_message",
"message": "Still thinking it over? Happy to help you decide โ want a hand?"
}
}
The widget's TriggerEngine polls the localStorage
entry every 30 seconds (and once immediately on mount). When it
finds a non-empty cart older than the rule's
idle_minutes threshold, it fires the rule's action
โ typically a proactive engagement bubble.
A 5-minute global trigger cooldown still applies, so the visitor isn't ambushed on every page transition.
Page-context payload (for Woo products)
When the visitor is on a single-product page (is_product()),
the plugin enriches the data-page-context JSON with
a woo object:
{
"source": "wordpress",
"page_url": "https://shop.example/product/blue-tee",
"post_id": 142,
"post_type": "product",
"permalink": "โฆ",
"categories": ["tees", "summer"],
"tags": [],
"woo": {
"id": 142,
"sku": "T-BLU-M",
"name": "Blue tee",
"permalink": "https://shop.example/product/blue-tee",
"price": "29.00",
"regular_price": "39.00",
"sale_price": "29.00",
"currency": "USD",
"stock_status": "instock",
"on_sale": true
}
}
The widget folds this into the retrieval envelope so the agent prefers content from the page the visitor is currently on (current-page boost of +0.15 on chunk score).
What WooCommerce versions are supported?
The plugin's WooCommerce dependencies are all on stable public classes:
WC_Couponโ public since WC 2.0.WC_Productโ public since WC 2.0.WC_Orderโ public since WC 2.0.WC_Customer+wc_create_new_customerโ public since WC 2.5.wc_get_productsโ public since WC 2.7.wc_get_ordersโ public since WC 2.7.
Effective floor: WooCommerce 3.0+. The plugin
actively tests on 8.x and 9.x. HPOS (High-Performance Order
Storage) is supported because the plugin uses the public
wc_get_orders() + WC_Order API rather
than reading order posts directly.