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

Handle multi-shipment carts

When a merchant ships items from multiple warehouses or with different fulfillment timelines, Firmly groups them into separate shipments inside the same cart. Each shipment needs its own shipping-method selection before checkout.

This recipe is the canonical pattern for handling multi-shipment.

​​ When this applies

  • Apparel merchants where items ship from different warehouses
  • Furniture merchants with white-glove items separate from accessories
  • Electronics merchants with main product + accessory bundles from different fulfillment centers
  • Anytime cart.shipments[] has more than one entry

​​ The pattern

After calling Set shipping info, inspect cart.shipments[]. Each shipment already carries its available methods in shipment.shipping_method_options. For each shipment_id, pick a method and record it independently.

The snippets below assume a few things are already set up:


// Base URL for the cart API, scoped to your merchant domain.
const API = `https://api.firmly.work/api/v2/domains/${domain}`;
// The access_token from POST /api/v1/browser-session, sent as
// x-firmly-authorization on every call.
const token = 'YOUR_TOKEN';
// The shipping address you collected from the buyer.
const shippingInfo = { /* first_name, last_name, address1, ... */ };
// callFirmly() is a thin fetch wrapper that POSTs JSON to `${API}${path}`
// with the auth header and returns the parsed response.

// After setting shipping info, the cart has a shipments[] array
const cart = await fetch(`${API}/cart/shipping-info`, {
method: 'POST',
headers: { 'x-firmly-authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify(shippingInfo),
}).then(r => r.json());
// One shipment → simple. Many shipments → walk each.
for (const shipment of cart.shipments) {
// Step 1: Read the methods for THIS shipment — populated inline on the
// shipment by set-shipping-info (no separate call needed).
const cheapest = shipment.shipping_method_options
.sort((a, b) => a.price.value - b.price.value)[0];
// Step 2: Record the choice
await fetch(`${API}/cart/shipment/methods`, {
method: 'POST',
headers: { 'x-firmly-authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify({
shipment_id: shipment.shipment_id,
shipping_method_id: cheapest.id,
}),
});
}

You can also parallelize since each shipment is independent:


await Promise.all(cart.shipments.map(async (shipment) => {
const method = pickMethod(shipment.shipping_method_options);
await callFirmly('/cart/shipment/methods', {
shipment_id: shipment.shipment_id,
shipping_method_id: method.id,
});
}));

​​ How to pick a method for each shipment

The simplest defaults:

  • Cheapest — minimize cost; good for replenishment and price-optimized agents
  • Fastest — pick the shipment method with the earliest estimated delivery
  • User-specified — let the user pick per shipment

For buyer-facing surfaces, the agent typically picks defaults and surfaces them: “Two of your items ship from different warehouses — standard for both, $12 total. Want to upgrade either to expedited?”

​​ Surface what’s happening to the user

Multi-shipment can confuse users if they’re not warned. A helpful pattern:


if (cart.shipments.length > 1) {
const summary = cart.shipments.map(s => {
const items = s.line_item_ids.map(id => {
// Guard the lookup — a missing match would otherwise throw.
// The documented line-item label field is `description` (per Get Cart).
const li = cart.line_items.find(li => li.line_item_id === id);
return li?.description ?? id;
}).join(', ');
return `${items} (shipment ${s.shipment_id})`;
}).join('; ');
agent.say(`Your order will ship in ${cart.shipments.length} packages: ${summary}.`);
}

​​ Edge cases to handle

  • shipping_method_options is empty for a shipment — that shipment cannot ship to the address. Check cart.notices for ITEM_NOT_SHIPPABLE codes and either remove the item or change the address.
  • Get-availability returns 501 NotImplemented — some merchants don’t implement the availability endpoint. This does not affect method selection: shipping methods always come from shipment.shipping_method_options. get-availability is only for delivery dates, time slots, and pickup locations on scheduled-delivery or in-store-pickup shipments, so treat a 501 as “no scheduled-delivery/pickup detail for this merchant” and proceed with the inline methods.
  • Methods differ across shipments — sometimes Shipment A has overnight available but Shipment B only has standard. Surface this so the user understands.