Skip to main content
Version: next

Capabilities & host actions

The bot can search your catalog, pick a product variant, filter a listing, compare products, add to cart, and more — on any storefront, not just WooCommerce. It does this through one capability contract: you declare what your host can do once (the same way you declare your catalog once through the ingest protocol), and the bot only ever offers and dispatches actions your host actually supports.

There is no branching on host identity anywhere in the engine. A capability is the single key that gates a host-acting tool. WooCommerce is just one host whose signals are auto-mapped into the same vocabulary; a custom SPA, a Laravel storefront, or a headless frontend declares the same capabilities directly and gets the same behaviour.

This is the contract a non-WooCommerce host integrates against

If you are not on WooCommerce, this page is your integration guide. Work top to bottom: understand the vocabulary → declare your capabilities → push variant data → wire the client bridge → verify. The end-to-end walkthrough at the bottom ties it together.

The capability vocabulary

Capabilities are a closed, versioned set of identifiers. Each one gates one or more tools the bot may call. You cannot invent new capability strings — the server ignores anything outside this set (adding a capability is a versioned change to the contract, not a per-host extension).

CapabilityGatesKindIn the baseline?
catalog.searchsearch_catalogread✅ always
catalog.get_productget_product (incl. variant axes / attributes)read✅ always
catalog.comparecompare_productsread✅ always
catalog.facetsapply_filter, filter_by_attributeread✅ always
catalog.sortapply_sortcontrol (reversible)declare
nav.openopen_product, open_category, open_page, navigatenav✅ always
nav.search_pageopen_search_with_query, searchnavdeclare
variant.selectselect_variantselection (reversible)declare
cart.addadd_to_cartwritedeclare
cart.removeremove_from_cartwritedeclare
cart.update_qtyupdate_cart_qtywritedeclare
cart.viewview_cartnavdeclare
checkout.assistlist_checkout_fields, validate_checkout_fields, focus_input, fill_input, set_select, set_checkbox, submit_checkoutassistdeclare
form.submitsubmit_formwritedeclare

The catalog-ingested baseline

The moment you have an ingested catalog, the bot always has the read/navigation baseline — catalog.search, catalog.get_product, catalog.compare, catalog.facets, and nav.open — with no declaration at all. So a freshly-synced host can already search, recommend, compare, and link to products. You only declare the capabilities beyond the baseline: variant selection, sort (catalog.sort), the cart writes, cart.view, checkout.assist, nav.search_page, and form.submit.

Reads, selections, and writes

  • read / nav capabilities are safe and unconfirmed.
  • variant.select is a reversible selection (the bot resolves which variation the shopper wants); it is not a write and is not confirmation-gated.
  • Cart writescart.add, cart.remove, cart.update_qty — execute after the shopper gives conversational consent (voice or chat). There is no extra UI confirmation button for cart actions; the spoken or typed agreement is sufficient.
  • form.submit (checkout and form submissions) is confirmation-gated with a required explicit user action — the bot highlights the submit control and waits for a click, never auto-submitting.

The distinction matters: cart actions flow naturally in conversation; irreversible submit actions require a physical interaction from the user.

Writes are server-authoritative

A client may narrow the granted set (advertise fewer capabilities than the server resolved — for example to disable cart actions on a read-only page) but can never widen it. A page that advertises cart.add it wasn't granted does not get it. This is enforced server-side; the client advertisement can only remove.

1. Declare your capabilities — the host_profile entity

A host declares its capabilities by pushing a host_profile ingest entity through the exact same signed-ingest / sync channel you already use for products and categories (see the Ingest Protocol Reference). It is a singleton entity — external_id is "host".

{
"kind": "host_profile",
"external_id": "host",
"payload": {
"capabilities": ["cart.add", "cart.remove", "cart.update_qty", "cart.view", "variant.select"],
"cart_endpoint": null,
"metadata": {}
}
}
FieldTypeNotes
capabilitiesstring[]Capability ids from the closed vocabulary above. Unknown strings are dropped; duplicates are de-duped. The baseline is granted regardless, so list only what's beyond the baseline.
cart_endpointstring | nullOptional. A server-side cart endpoint, if your host exposes one. Omit for client-bridge-only carts.
metadataobjectOptional free-form host metadata.
WooCommerce hosts don't need this

A WooCommerce host's environment entity already carries wc_capabilities (ajax_add_to_cart, rest_store_api, cart_fragments, checkout_blocks). Those are auto-mapped into the vocabulary (ajax_add_to_cart/rest_store_api → the four cart.* capabilities, checkout_blockscheckout.assist, …). A WooCommerce store gets cart/checkout capabilities for free. A non-WooCommerce host declares them explicitly via host_profile. Either path lands in the same vocabulary — the engine never sees the difference.

2. Push catalog data the bot can act on — variants & attributes

For the bot to pick a colour/size or filter by a facet, that structure has to be in the catalog you push. A configurable product carries variant_axes (the choice dimensions), optional attributes (non-variant facets), and optional concrete variations:

{
"kind": "product",
"external_id": "laravel:product:123",
"payload": {
"name": "Кросівки Runner",
"product_type": "variable",
"variant_axes": [
{
"name": "Колір",
"slug": "color",
"values": [
{ "label": "Червоний", "slug": "red", "external_id": "opt:red" },
{ "label": "Синій", "slug": "blue" }
]
},
{ "name": "Розмір", "values": [{ "label": "41" }, { "label": "42" }, { "label": "43" }] }
],
"attributes": [{ "name": "Матеріал", "values": ["Текстиль"] }],
"variations": [
{
"external_id": "laravel:variation:123-red-41",
"attributes": { "Колір": "Червоний", "Розмір": "41" },
"price_amount": 189900,
"stock_status": "instock"
}
]
}
}
  • product_type is inferred to variable when variant_axes or variations are present, so you don't strictly have to set it — but set it if you have it. Without axes/variations a product is simple.
  • variant_axes[].values[].external_id is optional but recommended: it's the stable id the bot passes back when it resolves a variation.
  • Canonical is preferred. If your data only has flat keys (colors, sizes, color, size, material, option…), the ingest boundary will normalise common synonyms into variant_axes as a fallback — but send the canonical shape when you can; it's unambiguous and locale-correct.

Once this lands, get_product returns the axes/attributes and they appear in the per-tenant grounding block the bot sees, so it filters and picks variants from a known vocabulary instead of guessing.

Category controls — ground the bot on real filters & sort

Filtering and sorting are grounded on what each category actually exposes, declared on the category ingest entity (both fields are additive and optional):

{
"external_id": "laravel:category:12",
"available_filters": [
{ "taxonomy": "brand", "label": "Бренд", "values": [
{ "value": "apple", "label": "Apple" },
{ "value": "samsung", "label": "Samsung" }
] },
{ "taxonomy": "color", "label": "Колір", "values": [
{ "value": "red", "label": "Червоний" }
] }
],
"available_sorts": [
{ "sort_key": "price", "label": "Спочатку дешевші" },
{ "sort_key": "price-desc", "label": "Спочатку дорожчі" },
{ "sort_key": "rating", "label": "За рейтингом" }
]
}

The bot is grounded only on these declared options for the current category — it offers your real filters and sorts and cannot invent ones that don't exist (closed-vocabulary grounding). A category that omits them simply has nothing to offer there, and the bot says so.

  • available_filters makes filtering category-accurate — without it, faceted filtering falls back to guessing from product attributes, which can offer filters the page doesn't have and miss ones it does.
  • available_sorts is what makes apply_sort reachable at all — a category with no declared sorts is not sortable by voice.

The sort_key / filter value the bot picks are passed straight back to your applySort / applyFilter handler unchanged — they are your keys, so they map 1:1 onto your sort control and filter UI. On WooCommerce these are auto-derived (registered pa_* attributes → filters; the standard orderby set → sorts); non-WC hosts (Laravel / Node / custom) declare them directly.

3. Wire the client — the host bridge

Read/nav/compare tools run entirely on the backend — the bot just speaks the result, your app does nothing. The acting tools (add to cart, select a variant, apply a filter, …) are pushed to the widget, which routes them to your host through window.VoiceBotSyncBridge. You provide that bridge either directly (vanilla) or via the SDK's onAction prop (React/Vue/Next).

The embed wrappers forward onAction, capabilities, and toolHandlers onto the bridge for you:

import { VoiceBotWidget } from '@monoverse/voicebot-react';
import { cart } from './my-store';

<VoiceBotWidget
publicKey="pk_…"
capabilities={['cart.add', 'cart.remove', 'cart.update_qty', 'variant.select', 'catalog.facets']}
onAction={{
addToCart: ({ product_external_id, variation_external_id, quantity }) => {
cart.add(product_external_id, variation_external_id, quantity);
return { status: 'executed_ok' };
},
removeFromCart: ({ product_external_id }) => cart.remove(product_external_id),
updateCartQty: ({ product_external_id, quantity }) => cart.setQty(product_external_id, quantity),
selectVariant: ({ product_external_id, axis_values }) => {
// axis_values e.g. { "Колір": "Червоний", "Розмір": "41" } — drive your PDP selectors
pdp.selectVariant(product_external_id, axis_values);
return { status: 'executed_ok' };
},
applyFilter: ({ taxonomy, value, mode }) => productList.filter(taxonomy, value, mode),
}}
/>

A handler returns true / void (ok), false (failed), or a result object { status, result?, error? }.

@monoverse/voicebot-vue exposes the same onAction / capabilities / toolHandlers props with full parity — Vue example:

<script setup lang="ts">
import { VoiceBotWidget } from '@monoverse/voicebot-vue';
import { cart } from './my-store';
</script>

<template>
<VoiceBotWidget
public-key="pk_…"
:capabilities="['cart.add', 'cart.remove', 'cart.update_qty', 'variant.select']"
:on-action="{
addToCart: ({ product_external_id, variation_external_id, quantity }) => {
cart.add(product_external_id, variation_external_id, quantity);
return { status: 'executed_ok' };
},
removeFromCart: ({ product_external_id }) => cart.remove(product_external_id),
updateCartQty: ({ product_external_id, quantity }) => cart.setQty(product_external_id, quantity),
selectVariant: ({ product_external_id, axis_values }) => {
pdp.selectVariant(product_external_id, axis_values);
return { status: 'executed_ok' };
},
applyFilter: ({ taxonomy, value, mode }) => productList.filter(taxonomy, value, mode),
}"
/>
</template>

@monoverse/voicebot-next (App Router) wires the same options from a Client Component. The SDK's installHostBridge helper does the same wiring if you mount the widget yourself.

Argument shapes

ActionArgs your handler receives
add to cart{ product_external_id, variation_external_id?, quantity }
remove from cart{ product_external_id }
update cart qty{ product_external_id, quantity }
view cart{ fallback_url? }
select variant{ product_external_id, axis_values: Record<string,string> }
apply filter{ taxonomy, value, mode: 'set' | 'add' | 'remove' }
apply sort{ sort_key, category_external_id? }

Handlers always receive the clean product_external_id name (the bridge maps it from the wire shape for you).

Raw runtime key for qty is update_cart_quantity, not update_cart_qty

The cart-quantity capability is cart.update_qty and the model-facing tool the bot calls is update_cart_qty, but the runtime action dispatched to the host is update_cart_quantity. The typed cart.updateQty field and the updateCartQty SDK prop already target it — use them. Only if you route the raw runtime action through the low-level toolHandlers map (or a mobile native callback) do you register the runtime name, and it must be update_cart_quantity — keying it update_cart_qty yields No handler for update_cart_qty and the quantity change silently fails. (cart.add / cart.remove have no such gap: their runtime actions are add_to_cart / remove_from_cart, matching the tool names.)

Wire selectVariant, not toolHandlers.select_variant

select_variant is not a merchant-overridable handler — it is dispatched to the first-class bridge.selectVariant (or SDK onAction.selectVariant). Registering it under the low-level toolHandlers map will silently never fire. Same first-class path for the typed cart.*, applyFilter, and applySort entries; toolHandlers is only the escape hatch for the older canonical action names.

Resolution order & graceful degradation

For each acting action the widget resolves a handler in this order, stopping at the first that exists:

toolHandlers[action] (low-level override, highest precedence)
→ bridge.cart.* / bridge.applyFilter / bridge.applySort / bridge.selectVariant (first-class, recommended)
→ WooCommerce built-in (only if WC globals are present on the page)
→ generic DOM fallback
→ unsupported

If nothing handles an action it resolves to unsupported — your app never crashes, and the bot adapts (it stops offering that action and steers elsewhere). You only wire what you support. Cart writes additionally pass through the shopper-confirmation gate before your handler runs.

Advertising capabilities from the client

Pass capabilities (SDK prop or bridge.capabilities) to advertise what the current page can do. This is narrow-only: it can hide capabilities the server granted (e.g. no cart on a blog page) but cannot grant new ones. Omit it to use the full server-resolved set.

window.VoiceBotSyncBridge reference

The bridge is the low-level surface. When you use the React/Vue/Next SDK, installHostBridge writes to this object for you — you never touch it directly. If you use vanilla JS or need manual control, set the bridge before the widget script boots:

window.VoiceBotSyncBridge = {
protocolVersion: 1, // required, always 1
capabilities: [ // optional narrow-only set (see §Advertising capabilities above)
'cart.add', 'cart.remove', 'cart.update_qty', 'cart.view',
'variant.select', 'catalog.facets', 'catalog.sort',
],

// --- cart actions ---
cart: {
add: ({ product_external_id, variation_external_id, quantity }) => {
return myCart.addItem(product_external_id, variation_external_id, quantity);
},
remove: ({ product_external_id }) => {
return myCart.removeItem(product_external_id);
},
updateQty: ({ product_external_id, quantity }) => {
return myCart.setQty(product_external_id, quantity);
},
view: ({ fallback_url }) => {
return myCart.open() || window.location.assign(fallback_url ?? '/cart');
},
},

// --- variant selection (reversible, no confirmation) ---
selectVariant: ({ product_external_id, axis_values }) => {
pdp.applySelection(product_external_id, axis_values);
return { status: 'executed_ok' };
},

// --- facet / attribute filtering ---
applyFilter: ({ taxonomy, value, mode }) => {
productList.filter(taxonomy, value, mode);
return { status: 'executed_ok' };
},

// --- sort the current listing (reversible) ---
applySort: ({ sort_key, category_external_id }) => {
productList.sort(sort_key, category_external_id);
return { status: 'executed_ok' };
},

// --- low-level escape hatch for canonical action names ---
// Use only for actions not covered by the typed fields above.
toolHandlers: {
navigate: ({ params }) => router.navigate(params?.url),
},
};

Full type reference

FieldTypeDescription
protocolVersion1Always 1. Required.
capabilitiesHostCapability[]Optional. Narrows the server-granted set for the current page.
cart.add(args: CartAddArgs) => HostActionOutcomeCalled when the bot adds a product to cart.
cart.remove(args: CartRemoveArgs) => HostActionOutcomeCalled when the bot removes a line item.
cart.updateQty(args: CartUpdateQtyArgs) => HostActionOutcomeCalled when the bot changes a line item's quantity.
cart.view(args: CartViewArgs) => HostActionOutcomeCalled when the bot opens the cart.
selectVariant(args: SelectVariantArgs) => HostActionOutcomeCalled when the bot selects a variant on a configurable product.
applyFilter(args: ApplyFilterArgs) => HostActionOutcomeCalled when the bot applies a facet filter.
applySort(args: ApplySortArgs) => HostActionOutcomeCalled when the bot sorts the current listing.
toolHandlersRecord<string, handler>Low-level overrides for canonical action names. Used only for actions not covered by the typed fields.

Argument types:

CartAddArgs = { product_external_id: string; variation_external_id?: string; quantity: number }
CartRemoveArgs = { product_external_id: string }
CartUpdateQtyArgs = { product_external_id: string; quantity: number }
CartViewArgs = { fallback_url?: string }
SelectVariantArgs = { product_external_id: string; axis_values: Record<string, string> }
ApplyFilterArgs = { taxonomy: string; value: string; mode: 'set' | 'add' | 'remove' }
ApplySortArgs = { sort_key: string; category_external_id?: string }

Handler return values:

Return valueMeaning
void / trueSuccess.
falseFailed (generic).
{ status: 'executed_ok' }Success with structured result.
{ status: 'executed_partial' }Partial success.
{ status: 'user_cancelled' }User cancelled the action.
{ status: 'failed', error: { code, message } }Failed with detail.
{ status: 'unsupported' }This page cannot handle this action.

All handlers may return a Promise.

Variant selection flow

When a shopper says "show me the red one, size 41", the bot runs a two-step sequence before adding to cart:

  1. select_variant — the bot calls select_variant({ product_external_id, axis_values }) where axis_values is e.g. { "Колір": "Червоний", "Розмір": "41" }. The backend resolves the concrete variation_external_id from the ingested variant_axes / variations on the product. Your selectVariant handler receives the axes and drives your PDP UI so the user sees the selection applied visually. This is not a write — no confirmation prompt, fully reversible.

  2. add_to_cart — the bot follows up with add_to_cart({ product_external_id, variation_external_id, quantity }) where variation_external_id is the resolved variation id (e.g. "laravel:variation:123-red-41"). Your addToCart handler receives the complete set of arguments. This is a write; it executes after conversational consent.

End-to-end example for a configurable product (colour + size):

User: "Add the red runner in size 41 to my cart"

Bot → select_variant({ product_external_id: "laravel:product:123",
axis_values: { "Колір": "Червоний", "Розмір": "41" } })

→ backend resolves variation_external_id = "laravel:variation:123-red-41"
→ your bridge.selectVariant / onAction.selectVariant fires
→ your PDP highlights "Red / 41"

Bot: "Found it — Кросівки Runner, Red, size 41 for 1 899 ₴. Add to cart?"

User: "Yes"

Bot → add_to_cart({ product_external_id: "laravel:product:123",
variation_external_id: "laravel:variation:123-red-41",
quantity: 1 })

→ your bridge.cart.add / onAction.addToCart fires
→ item lands in the cart

Bot: "Done — added to your cart."
Wire selectVariant and addToCart together

For a smooth experience, selectVariant should visually select the option on the page and addToCart should use your standard cart API. If your cart API requires a variation_id, use variation_external_id directly — it is the same stable id you sent when ingesting the product's variations array.

End-to-end: bring your own host

A complete non-WooCommerce integration (this is exactly how a custom Laravel + React storefront wires up):

  1. Declare capabilities. Add a host_profile entity to your sync with the capabilities your store supports beyond the baseline — e.g. ["cart.add","cart.remove","cart.update_qty","variant.select","catalog.facets"]. (§1)
  2. Push variant-aware catalog. Send configurable products with variant_axes / attributes / variations so the bot can see and pick options. (§2)
  3. Wire the bridge. Provide onAction (SDK) or window.VoiceBotSyncBridge (vanilla) handlers for the acting capabilities you declared, mapping the bot's calls onto your cart / PDP / product-list APIs. (§3)
  4. Verify. Confirm with the checklist below.

Verify

  • Sync ran: your host_profile and variant products are ingested (check the dashboard sync coverage).
  • Ask the bot to find a configurable product → it should offer the real axes (colours/sizes) from your catalog, not deny them or invent values.
  • Ask it to "add the red one, size 41" → it resolves the variation (select_variant) and calls add_to_cart after conversational consent; your onAction.selectVariant fires first (PDP updates), then onAction.addToCart fires once you say yes.
  • Ask it to filter ("show only red") → it calls apply_filter with a facet value from your catalog.
  • Ask it to sort ("cheapest first") → it calls apply_sort with a sort_key from the category's available_sorts.
  • On a page that advertises a narrowed capabilities set, the hidden actions are not offered.

See also