Order widgets
Read-only Python order panels for connected providers and custom order systems.
Show order details, products and tracking in the conversation sidebar. These helpers produce a basic read-only panel with no buttons. They do not cancel, refund, hold or update orders.
| Provider | Helper |
|---|---|
| Shopify | shopify_order_widget(context) |
| WooCommerce | woocommerce_order_widget(context) |
| Magento 2 | magento2_order_widget(context) |
| BigCommerce | bigcommerce_order_widget(context) |
| Shoptet Premium | shoptet_order_widget(context) |
| PrestaShop | prestashop_order_widget(context) |
Each helper uses the existing provider Python order lookups. It looks up the conversation's saved orderId, then the customer's known email, and deduplicates by provider ID. The saved order appears first. Email lookups retain their existing limit of ten orders. Missing identifiers make no requests. No results produce an empty panel; provider failures remain errors. Orders use Python actions and widgets exclusively. Disabled, missing or failing Python configuration never restores a built-in order service.
Installing one of these integrations creates editable Python order lookup actions and a basic Orders widget scoped to that business. Each business receives its own copies. Reconnecting never installs additional commerce defaults. Existing custom actions, including arbitrarily named or disabled policies, prevent automatic additions so installation cannot add a weaker lookup beside a verified action. An existing assigned Orders panel or unassigned organization widget also prevents adding both lookup actions and a panel. Ask Copilot to inspect and configure these cases explicitly. Standard defaults for a different resource, such as subscriptions, can coexist.
These are human-agent sidebar helpers. They do not replace customer-facing verification in bot actions. Provider connections remain scoped to the conversation's business. Widgets themselves belong to the organization, so explicitly restrict the business slugs as shown below.
Set up with Copilot
Ask Copilot: "Prepare an inactive read-only orders widget for BUSINESS using its connected provider. Preserve its existing order lookup rules and show no order buttons."
For a Shopify business:
def get_sidebar_data(context):
if (context.get("business") or {}).get("slug") not in {"your-business"}:
return {"title": "Orders", "items": []}
return shopify_order_widget(context)Use create_sidebar_widget with isActive=false, inspect the source with get_sidebar_widget_details, and verify with test_sidebar_widget. Widget configuration is available through MCP/Copilot and the dashboard; there is currently no equivalent public REST widget-management endpoint. No sidebar action needs creating.
The default fields are order number, provider status, date, total, products and available shipment tracking. Shopify order titles link to the order in the Shopify admin; a custom mapping can do the same by setting url. WooCommerce tracking plugins differ by store, so its basic helper does not guess tracking fields from notes or arbitrary metadata. PrestaShop returns a currency ID rather than a currency code, so its basic panel displays the amount without inventing a currency. Carrier scraping, extra tracking enrichment, provider-specific notes and addresses are not included in these basic panels.
Standard templates contain only shared provider behavior. Brand-specific rules, status overrides, identifier normalization and custom verification belong in the business's saved Python actions, widgets or modules. Configure them through Copilot; the old configuration-based migration tools have been retired.
Custom order systems
Customer-specific lookup logic belongs in the organization's Python widget or shared module. Copy the read-only lookup functions already used by its Python actions or condition providers, preserving identifier normalization, brand/store filtering, credentials and any required email verification. Do not invoke an entire bot action that also sets metadata, selects an order or performs other writes.
order_widget handles the saved ID and email lookups. Supply only the callbacks the custom system actually supports. Each callback receives the current context and one identifier; it returns a raw order (None if absent) or a list of raw orders. map_order converts one raw order to the small presentation contract below.
def map_order(raw):
return {
"id": raw["externalId"],
"number": raw["orderId"],
"status": raw.get("status"),
"total": (raw.get("totalCost") or {}).get("amount"),
"currency": (raw.get("totalCost") or {}).get("currencyCode"),
"placed_at": raw.get("orderPlacedAt"),
"products": raw.get("lineItems") or [],
"shipments": [{
"status": f.get("status"),
"carrier": f.get("courier"),
"tracking_number": f.get("trackingId"),
"tracking_url": f.get("trackingUrl"),
} for f in raw.get("fulfillments") or []],
}
def get_sidebar_data(context):
if (context.get("business") or {}).get("slug") not in {"your-business"}:
return {"title": "Orders", "items": []}
return order_widget(
context,
get_order=lookup_order, # Your copied read-only function.
find_orders=lookup_orders_by_email, # Omit if unsupported.
map_order=map_order,
)For a system requiring both order ID and email, keep that verification in the get_order callback and omit find_orders unless independent email lookup was already supported. Never fall back to an unverified lookup when verification fails. Preserve business-specific prefixes and status notes in the organization's code too.
For an existing custom lookup loop, simply call render_order_widget(mapped_orders). Its order fields are id, number, status, email, total, currency, placed_at, products and shipments. Products accept name, quantity, sku, total; shipments accept status, carrier, tracking_number, tracking_url. Unknown fields are omitted. Missing values stay hidden, zero values remain visible, and repeated IDs keep the first result. Use provider-qualified IDs when combining multiple sources. Orders without IDs are not deduplicated.
Migration and verification
Prepare widgets inactive and compare representative saved-ID, email-only, empty and provider-error cases before cutover. Check all businesses in a multi-brand organization, not only its first brand. Keep custom authentication and verification rules, and verify that source code performs no writes on panel load.
After verification, ask Copilot to assign the widget as the business's Orders panel and activate it. configure_sidebar_widget_panels accepts businessSlug and ordersWidgetId; get_sidebar_widget_panels inspects the assignment. Orders panels run through Python sidebar widgets only. Assign each intended business, and keep the widget's Python business guard consistent with those assignments. The assignment records which widget provides the panel; active organization widgets render according to their Python business guards.
Set ordersWidgetId to null to clear its assignment. To stop a widget appearing, deactivate or archive it, or update its Python business guard. None of these changes restores a built-in provider panel. A Python execution error stays visible and never falls back to the old service. Assignments do not change bot actions, macro data providers or other integration configuration.
REST clients can inspect or update the same assignment with GET or PATCH /rest/v1/businesses/{businessSlug}/sidebar-panels, using their organization's API key. PATCH accepts ordersWidgetId and subscriptionsWidgetId: omit a field to preserve it, or use null to clear an assignment. The response contains businessSlug and both nullable widget IDs. Full widget code/action management remains available through MCP/Copilot and the dashboard.
Shared source
The provider-specific source is on the Shopify, WooCommerce, Magento, BigCommerce, Shoptet and PrestaShop helper pages. Include this shared source too if you copy a provider presentation helper into your own code.
"""Read-only order presentation shared by provider and organization Python code."""
def _order_widget_value(value, unit=None):
if value is None or value == "":
return None
return f"{value} {unit or ''}".strip()
def render_order_widget(orders, title="Orders"):
"""Render normalized orders. Unknown provider fields and operation buttons are omitted.
Order fields: id, number, url, status, email, total, currency, placed_at,
products [{name, quantity, sku, total}], shipments [{status, carrier,
tracking_number, tracking_url}]. All fields are optional. url (e.g. the
order's page in the store admin) makes the order title a link. IDs deduplicate
results (first wins); records without IDs remain separate.
"""
items, seen = [], set()
for order in orders:
order_id = order.get("id")
if order_id is not None and order_id != "":
key = str(order_id)
if key in seen:
continue
seen.add(key)
number = order.get("number") or order_id
currency = order.get("currency")
sections = [{"title": "Order", "fields": {
"ID": _order_widget_value(number), "Email": order.get("email"),
"Date": order.get("placed_at"),
"Total": _order_widget_value(order.get("total"), currency),
}}]
for product in order.get("products") or []:
sections.append({"title": str(product.get("name") or "Product"), "fields": {
"Quantity": product.get("quantity"), "SKU": product.get("sku"),
"Total": _order_widget_value(product.get("total"), currency),
}})
for shipment in order.get("shipments") or []:
sections.append({"title": "Shipment", "fields": {
"Status": shipment.get("status"), "Carrier": shipment.get("carrier"),
"Tracking": shipment.get("tracking_number"), "Tracking URL": shipment.get("tracking_url"),
}})
item = {"title": f"Order {number}" if number is not None else "Order", "sections": sections}
if order.get("url"):
item["link"] = str(order["url"])
if order.get("status"):
item["badge"] = {"text": str(order["status"]), "color": "gray"}
items.append(item)
return {"title": title, "items": items}
def order_widget(context, *, map_order, get_order=None, find_orders=None):
"""Look up the saved order ID and customer email, then render basic order details.
Callbacks receive (context, order_id) / (context, email) and return a raw
order / list of raw orders. map_order(raw) returns render_order_widget's
normalized fields. Only pass read-only callbacks, never bot actions with
metadata updates or other side effects. Errors propagate to the sidebar.
Organization code owns business scope, ID normalization and verification.
"""
order_id = str((context.get("conversation") or {}).get("orderId") or "").strip()
email = str((context.get("customer") or {}).get("email") or "").strip()
orders = []
if order_id and get_order:
order = get_order(context, order_id)
if order is not None:
orders.append(order)
if email and find_orders:
orders.extend(find_orders(context, email))
return render_order_widget(map_order(order) for order in orders)