Docs
Firmly Agentic Commerce
Set theme to dark (⇧+D)

Multi-Product Purchase

​​ Scope

A multi-product Publisher Commerce cart holds items from a single merchant — a shoppable article with multiple Buy Now buttons (gift guide, product roundup), or a related-product upsell in the in-context checkout panel.

The cart-mutation, shipping, and order-completion mechanics are the shared Firmly cart flow — because the cart is built across those calls, checkout finalizes with complete-order, not the one-shot place-order. See Agentic Commerce → Multi-Product Purchase for the cart-modification table, multi-shipment handling, and the race-condition retry pattern. This page covers the Publisher-specific discovery and cart-review surfaces.

​​ What’s different from single-product

  1. Multi-button discovery — the article has multiple Buy Now buttons, each adding to the same cart; a persistent cart badge shows the running count.
  2. Cart Review — an in-context cart panel (slide-in or inline overlay) with quantity controls and a Checkout CTA.

​​ Multi-button discovery in code

For a shoppable article with multiple Buy Now buttons, each click is an independent POST /cart/line-items call:


// callFirmly: fetch wrapper adding x-firmly-authorization; see the Quickstart
// DOMAIN: the merchant domain (e.g., staging.luma.gift)
// User clicks Buy on the hoodie
async function onBuyClick(productRef) {
const cart = await callFirmly(
`/api/v2/domains/${DOMAIN}/cart/line-items`,
{ add_to_cart_ref: productRef, quantity: 1 }
);
// line_items.length is the count of distinct lines; sum quantities for a total-units badge
updateCartBadge(cart.line_items.reduce((n, li) => n + li.quantity, 0));
}
// Each button on the article wires up its own onBuyClick
document.querySelectorAll('[data-buy-now]').forEach(btn => {
btn.addEventListener('click', () => onBuyClick(JSON.parse(btn.dataset.buyNow)));
});

A persistent cart badge (“3 items in cart”) tells the reader their adds are accumulating across Buy Now clicks within the same article.

​​ In-context cart panel

The cart panel slides in from the side or appears as an inline overlay on the article — line items, quantity controls, remove buttons, subtotal, and a Checkout CTA:


// Paths below are relative to /api/v2/domains/{domain}
const cart = await callFirmly(`/api/v2/domains/${DOMAIN}/cart`, null, 'GET');
renderCartPanel(cart);
// Reader clicks -1 on the hoodie
const hoodieLine = cart.line_items.find(li => li.line_item_id === 'li_hoodie');
const updated = await callFirmly(
`/api/v2/domains/${DOMAIN}/cart/line-items/${hoodieLine.line_item_id}`,
{ quantity: hoodieLine.quantity - 1 },
'PUT'
);
renderCartPanel(updated);

The panel can also offer a “save for later” affordance for browsable shoppable articles where the reader doesn’t want to check out right away.

​​ Express checkout

In-context surfaces lean on express checkout to minimize time away from the article — Google Pay, PayPal Express, Klarna BNPL, or Click to Pay, each spanning the full cart rather than per-item.

​​ Attribution

Identical to Single Product — UTM, publisher identifier, and article reference pass through on complete-order. The merchant sees one order with multiple line items; the publisher’s analytics can credit each line item to the article that drove the click.