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[] arrayconst 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 choiceawait 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_optionsis empty for a shipment — that shipment cannot ship to the address. Checkcart.noticesforITEM_NOT_SHIPPABLEcodes 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-availabilityis 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.
Related
- Shipping & Fulfillment concept — the architectural picture
- Get shipping availability — endpoint reference
- Set shipping method — endpoint reference
- Multi-product purchase flow — the broader flow this recipe sits inside