Shipping
List shipping methods and fetch live carrier quotes for the checkout picker
Behio supports two pricing modes per shipping method, set by the merchant in the admin:
- Fixed price: the merchant sets a per-currency price (e.g. PPL = 120 CZK). Free-shipping thresholds work.
- Live quote: the price is fetched from a meta-provider (Zaslat.cz today, others coming) per destination. The merchant configures markup + rounding rules; the customer sees the adjusted price.
Both routes are exposed through the same SDK module, so pick the call that matches your checkout flow.
Always-on listing
When you need a quick list of available methods (sidebar widget, info page, default cart summary), use listMethods. Fixed-price methods come back fully resolved; live-quote rows return price: null here, so call quote() once you have a destination address. An unknown quote is not free shipping.
const {data, error} = await client.shipping.listMethods({
currency: 'CZK',
country: 'CZ',
cartTotal: 1850,
cartWeightKg: 1.2,
});
if (data) {
data.items.forEach((m) => {
console.log(m.name, m.price, m.currency, m.deliveryType);
});
}Live quote at checkout
Once the customer enters their delivery address, call quote() to get destination-specific prices. The endpoint dispatches each live-quote method to its upstream meta-provider and applies the merchant's markup + rounding rules. Fixed-price methods are returned unchanged for completeness.
const {data, error} = await client.shipping.quote({
destinationAddress: {country: 'CZ', zip: '11000', city: 'Praha'},
currency: 'CZK',
});
// A failed quote is not "no shipping": keep the methods from listMethods on
// screen and let the customer pick one of those instead.
if (error) return fallbackToListMethods();
// Filter unavailable rows for the picker
const offers = (data?.items ?? []).filter((q) => q.available);The cart itself is not part of the request. The API reads the visitor's cart from the X-Cart-Session the client already sends with every call and derives the weight and the subtotal from it, so the free-shipping threshold is always computed from what is really in the cart. Pass items (whItemId + quantity) only when you quote something that is not in the cart yet, for example a shipping estimate on a product page. Any client-supplied cartTotal or weightKg is accepted for backwards compatibility and ignored.
Quote response shape
Each entry extends the ShippingMethodSummary returned by listMethods with quote-only fields:
| Field | Type | Notes |
|---|---|---|
strategy | "fixed" | "live_quote" | How the price was resolved |
available | boolean | false when the upstream provider didn't return a rate (carrier not contracted, destination outside coverage, max-price cap exceeded, provider not implemented yet) |
reason | string | null | Machine-readable hint when unavailable: "no_rate_returned", "live_quote_not_implemented" |
price | number | Final customer price (already includes markup/rounding for live quotes) |
basePrice | number | Raw price before adjustments, informational only |
etaDaysMin / etaDaysMax | number | null | Live providers fill these from the upstream response when available |
currency | string | null | Resolved currency for the price |
Free shipping thresholds
Fixed-price methods support per-currency free-shipping thresholds set by the merchant. The shop-wide threshold is converted from the default currency using the configured FX source; the lower of this value and the method threshold applies. At or above that threshold, the response carries isFreeShipping: true + price: 0. The threshold itself is on freeShippingThreshold so you can render hints like "Add 150 CZK more for free shipping".
data.items.forEach((q) => {
if (q.freeShippingThreshold && !q.isFreeShipping) {
const remaining = q.freeShippingThreshold - (cart.shippingSubtotal ?? Math.max(0, cart.subtotal - cart.promotionDiscountTotal));
console.log(`${q.name}: ${remaining} ${q.currency} away from free shipping`);
}
});Currency handling
The quote request currency should match the cart's currency. The merchant decides which methods are available per currency:
- A method's
currencies[]allow-list restricts which currencies see it (empty = inferred frompricing[]rows). - For fixed pricing, the method needs a
pricing[]row in the requested currency or it's skipped. - For live quote, the meta-provider returns the currency in its response. It usually matches the cart, but always check
q.currencyto be safe.
Pickup points
Methods with supportsPickupPoints: true (Zásilkovna, Packeta, PPL ParcelShop, …) require the customer to pick a specific drop-off location:
const { data } = await client.shipping.getPickupPoints({
methodId: shippingMethod.id,
query: 'Praha', // optional free-text search over name / city / zip
country: 'CZ', // optional ISO 3166-1 alpha-2 filter
limit: 30, // optional, capped at 100
});
// data.items: PickupPoint[] with { externalId, name, street, city, zip, country,
// latitude, longitude, cashOnDelivery, cardPayment, openingHours }
// openingHours: { day, from1, to1, from2, to2 }[], empty when the provider gives noneSend the chosen externalId as pickupPointId in checkout.createOrder. The backend rejects the order without it when the method requires a pickup point. Client-side there is a ready hook:
import { usePickupPoints } from '@behio/storefront-sdk/react';
const { data, isLoading } = usePickupPoints({ methodId, query, country: 'CZ' });Pickup point map
A method can also offer a map in publicConfig.pickupWidget (typed as PickupWidgetConfig):
provider | What to render | Extra fields |
|---|---|---|
"packeta" | Packeta widget (widget.packeta.com) | apiKey (public widget key) |
"zaslat" | Zaslat map widget (app.zaslat.cz/map/zaslat-map.umd.min.js, zslt.openMap) | carrier (Zaslat carrier code or null), territory ("cz", "sk" or null = take it from the delivery country) |
"behio" | Your own map from getPickupPoints() coordinates (PPL, GLS, Aramex) | none |
For Zaslat send place.code || String(place.id) as pickupPointId (the same id getPickupPoints() returns as externalId) and always add the pickupPoint snapshot, because the widget lists more points than Behio has cached. Keep the search list as a fallback when the widget script fails to load. Full example in Checkout, Pickup point map.
Meta-providers under the hood
Behio's storefront SDK is provider-agnostic: your storefront code never names Zaslat, Shippo, Sendcloud, or any specific aggregator. The merchant picks a provider per shipping method in the admin; the SDK just sees provider as an internal id. As Behio adds providers, your existing code keeps working without changes.
Quote ownership and price changes
The free-shipping threshold uses the displayed goods subtotal after per-product
promotions and before order coupons, loyalty and gift cards. This follows the shop's
priceDisplay mode. Customer price lists, quantity tiers and destination price rules
are resolved by the cart pricing pipeline for both cart quotes and legacy estimates.
An estimate using warehouse IDs requires an enabled, unambiguous product in this shop
with a price in the requested currency. An amount in another currency is never relabeled.
Stored carrier quotes belong to the cart that requested them. A changed cart, goods
subtotal, physical weight, destination or merchant shipping configuration returns
be.storefront.shippingQuoteStale. Fetch shipping options again and let the shopper
review the updated total before placing the order. An estimate made without a cart is
informational; request a new quote with the customer's cart before checkout.
When cart.requiresShipping === false, skip shipping methods and send a billing address
without shippingAddress. A downloadable bonus does not make physical goods exempt
from shipping. Each product's explicit requiresShipping flag controls this behavior.
cart.shippingSubtotal is the authoritative delivery threshold basis in the cart currency and displayed price mode, after per-product promotions and including bundles, before order coupons, loyalty and gift cards. Use it for progress indicators instead of grandTotal, which includes other discounts and may include extra VAT. Hide delivery progress for requiresShipping: false, an active shippingPromotionApplied, or a FREE_SHIPPING coupon. List methods for the actual destination. Without a destination, do not promise a country-restricted or unquoted live carrier.
A null shop threshold adds no global waiver; zero waives all available delivery methods. A global waiver never makes an unavailable live carrier selectable. Stored quotes bind the converted shop threshold, so changing the limit or its currency conversion requires a fresh selection.
Central delivery restrictions
The shop setting allowedShippingCountries restricts physical delivery in addition to
each shipping method's coverage. An empty central list is unrestricted. The public
shippingCountries value is the intersection with enabled shipping methods: null
means worldwide, while [] means there is no supported delivery destination.
Shipping lists, quotes and pickup searches do not offer forbidden countries. Direct
cart destination updates, checkout previews and orders reject them with HTTP 400.
Digital-only orders keep using the actual billing country for tax and payment rules.
For a method with configured country rows, only enabled rows provide coverage. Disabling its last country removes the method from delivery lists, quotes and pickup searches; checkout also rejects it. Removing all country rows restores legacy unrestricted coverage, still bounded by the shop's central restriction.