Cart integration
The widget never touches the host cart directly. Instead, it defines a callbacks contract that your integration implements against the real cart (WooCommerce, custom, headless). This keeps the widget platform-agnostic and leaves all cart-state control on your side.
When cart.enabled = true, the widget:
- Reads the cart via
onGetCarton mount and after everyneuroon:cart-update. - Mutates the cart via
onAddToCart,onRemoveFromCart,onUpdateQuantitywhen the user interacts with widget CTAs. - Sends a
cartSnapshotwith everyPOST /api/widget/search, which activates the cart-aware branch of the conversational agent (cross-sell, remembering destinations, etc.).
CartConfig contract
interface CartConfig {
enabled: boolean
initialCount?: number
onGetCart: () => Promise<CartState>
onAddToCart: (productId: string, quantity: number, externalProductId?: string) => Promise<CartOperationResult>
onRemoveFromCart: (itemKey: string) => Promise<CartOperationResult>
onUpdateQuantity: (itemKey: string, quantity: number) => Promise<CartOperationResult>
onCheckout: () => void
canAddToCart?: (product: Product) => boolean
externalUpdateEvent?: string // default 'neuroon:cart-update'
onSetDestinationCountry?: (countryCode: string) => Promise<CartOperationResult | void>
}
CartState
interface CartState {
items: CartItem[]
totalItems: number
subtotal: string // host-formatted: "232,48 €"
total: string // host-formatted
currency: string // ISO 4217: "EUR", "USD"
shipping?: CartShippingInfo
}
interface CartItem {
key: string // unique item id in the cart
id: string // Neuroon productId
externalId?: string // host-platform id (e.g. WC post_id)
name: string
price: string // host-formatted
quantity: number
image?: string
url?: string
variant?: string // "Size: 42, Color: Black"
maxQuantity?: number
}
subtotal, total and price are already-formatted strings by the host. This avoids rounding and currency-formatting disputes across regions.
CartOperationResult
type CartOperationResult =
| { success: true; cart: CartState; notice?: string }
| { success: false; cart?: CartState; error?: string }
If your callback returns success: true, the widget updates its internal state with the returned cart. If it returns success: false, the widget shows the error in a toast and does not mutate its state.
neuroon:cart-update bridge
The host must emit neuroon:cart-update after any cart mutation outside the widget (mini-cart, cart page, another plugin).
window.dispatchEvent(new CustomEvent('neuroon:cart-update'))
When it receives the event, the widget:
- Verifies that no widget-originated operations are in flight (avoids overwriting optimistic state).
- Applies a 300 ms debounce (some hosts emit several events in a row:
added_to_cart,wc_fragments_refreshed). - Calls
cart.onGetCartand updates its internal state.
Implementation at.
Example: WooCommerce with jQuery
<script>
(function() {
function notifyNeuroon() {
window.dispatchEvent(new CustomEvent('neuroon:cart-update'))
}
if (window.jQuery) {
jQuery(document.body).on(
'added_to_cart removed_from_cart wc_fragments_refreshed updated_cart_totals',
notifyNeuroon
)
}
})()
</script>
The official Neuroon plugin for WordPress already ships this bridge; you only need it for a custom integration.
Example: CartConfig implementation
window.NeuroonWidget.init({
container: '#neuroon-search',
token: 'WIDGET_TOKEN',
cart: {
enabled: true,
initialCount: window.MyShop.cartCount, // from cookie/SSR
async onGetCart() {
const r = await fetch('/api/cart', { credentials: 'include' })
const data = await r.json()
return {
items: data.items.map(i => ({
key: i.line_item_key,
id: i.product_id,
externalId: String(i.post_id),
name: i.title,
price: i.formatted_price,
quantity: i.qty,
image: i.thumbnail,
url: i.permalink,
variant: i.variant_label,
maxQuantity: i.stock_qty,
})),
totalItems: data.total_items,
subtotal: data.formatted_subtotal,
total: data.formatted_total,
currency: data.currency,
}
},
async onAddToCart(productId, quantity, externalProductId) {
const r = await fetch('/api/cart/add', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productId: externalProductId || productId,
quantity,
}),
})
if (!r.ok) return { success: false, error: 'Could not add item' }
return { success: true, cart: await this.onGetCart() }
},
async onRemoveFromCart(itemKey) {
await fetch(`/api/cart/${itemKey}`, { method: 'DELETE', credentials: 'include' })
return { success: true, cart: await this.onGetCart() }
},
async onUpdateQuantity(itemKey, quantity) {
await fetch(`/api/cart/${itemKey}`, {
method: 'PATCH',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ quantity }),
})
return { success: true, cart: await this.onGetCart() }
},
onCheckout() {
window.location.href = '/checkout'
},
},
})
cartSnapshot sent to the backend
When cart.enabled = true and there are items, the widget includes a snapshot in the body of POST /api/widget/search:
interface CartSnapshot {
currency: string
totalItems: number
subtotal: number // numeric (not formatted)
total: number
formattedSubtotal?: string
formattedTotal?: string
items: CartSnapshotItem[]
truncated: boolean
hash: string
updatedAt: string
shipping?: CartShippingInfo
}
Agent actions on the cart
The backend can ask the widget to mutate the cart via SearchResponse.cartAction:
cartAction.type | Meaning | Confirmation |
|---|---|---|
ADD_SUGGESTION | Add a suggested product | The widget shows a premium card with confirmationPrompt and CTA. |
REMOVE | Remove an item by itemKey | Inline confirmation. |
UPDATE_QUANTITY | Change quantity | Inline confirmation. |
CLEAR | Empty the cart | Destructive confirmation. |
SET_DESTINATION_COUNTRY | Persist country (shipping) | Silent, no confirmation. Calls onSetDestinationCountry. |
Types at.
The widget always confirms with the user before executing ADD/REMOVE/UPDATE/CLEAR. The host is the one that performs the actual mutation.
Further reading
- Configuration —
CartConfigin context. - Widget events —
neuroon:cart-updateand others. - Reference → Data models — full schemas.