Shipping
Fetch shipping methods and pickup locations using the Storefront SDK
Overview
The shipping resource provides methods for fetching available shipping options and nearby pickup locations. It integrates with Shipit API for Nordic shipping services.
Key Features:
- Fetch store's configured shipping methods
- Get nearby pickup locations for a postal code
- Carrier pricing with merchant markup
- GPS coordinates and opening hours for pickup points
- Weight-based filtering to show only methods that can handle cart weight
Methods
shipping.getMethods(params?, options?)
Get all available shipping methods for the store. Returns methods without pickup locations.
const { shipmentMethods } = await storefront.shipping.getMethods();
shipmentMethods.forEach(method => {
console.log(`${method.name}: ${method.price / 100}€`);
if (method.shipitMethod) {
console.log(` Carrier: ${method.shipitMethod.carrier}`);
console.log(` Max weight: ${method.shipitMethod.maxWeight} kg`);
}
});Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
params | GetMethodsParams | No | Optional parameters including cartWeight |
options | FetchOptions | No | Fetch options (caching, headers, etc.) |
GetMethodsParams:
| Param | Type | Required | Description |
|---|---|---|---|
cartWeight | number | No | Total cart weight in kg. When provided, filters out shipping methods that cannot handle this weight. |
Returns: Promise<ShipmentMethodsResponse>
Use cases:
- Display shipping options on product pages
- Show shipping info before customer enters postal code
- Calculate preliminary shipping costs
- Filter methods by cart weight during checkout
shipping.getWithLocations(postalCode, params?, options?)
Get shipping methods with nearby pickup locations for a specific postal code. Calls Shipit API to find parcel lockers and pickup points.
const { shipmentMethods, pricedLocations } = await storefront.shipping.getWithLocations("00100");
// Show pickup locations
pricedLocations.forEach(location => {
console.log(`${location.name} - ${location.carrier}`);
console.log(` ${location.address1}, ${location.city}`);
console.log(` ${location.distanceInKilometers.toFixed(1)} km away`);
console.log(` Price: ${(location.merchantPrice ?? 0) / 100}€`);
});Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
postalCode | string | Yes | Customer's postal code (e.g., "00100") |
params | GetWithLocationsParams | No | Optional parameters including cartWeight |
options | FetchOptions | No | Fetch options (caching, headers, etc.) |
GetWithLocationsParams:
| Param | Type | Required | Description |
|---|---|---|---|
cartWeight | number | No | Total cart weight in kg. When provided, filters out shipping methods and pickup locations that cannot handle this weight. |
Returns: Promise<ShipmentMethodsWithLocationsResponse>
Throws:
ValidationErrorif postal code is missingNotFoundErrorif Shipit integration not configuredStorefrontError(502) if external shipping service fails
Fetch Options
All methods accept an optional options object:
| Option | Type | Description |
|---|---|---|
signal | AbortSignal | Cancel the request |
cache | RequestCache | Fetch cache mode |
next.revalidate | number | false | Next.js ISR revalidation time |
next.tags | string[] | Next.js cache tags |
// Cache shipping methods for 5 minutes
const { shipmentMethods } = await storefront.shipping.getMethods({
next: { revalidate: 300, tags: ['shipping'] }
});
// Don't cache pickup locations (user-specific)
const { pricedLocations } = await storefront.shipping.getWithLocations(postalCode, {
cache: 'no-store'
});Response Types
ShipmentMethodsResponse
Response from getMethods():
interface ShipmentMethodsResponse {
shipmentMethods: ShipmentMethod[];
}ShipmentMethodsWithLocationsResponse
Response from getWithLocations():
interface ShipmentMethodsWithLocationsResponse {
shipmentMethods: ShipmentMethod[];
pricedLocations: PickupLocation[];
}ShipmentMethod
Store's shipping method configuration:
interface ShipmentMethod {
id: string;
name: string; // e.g., "Posti - Paketti"
description: string | null;
price: number; // Price in cents
active: boolean;
min_estimate_delivery_days: number | null;
max_estimate_delivery_days: number | null;
shipitMethod?: ShipitShippingMethod | null;
}ShipitShippingMethod
Shipit service details (if using Shipit integration):
interface ShipitShippingMethod {
id: string;
serviceId: string; // Shipit service identifier
name: string; // e.g., "Posti SmartPOST"
carrier: string; // e.g., "Posti", "Matkahuolto"
logo: string; // Carrier logo URL
pickUpIncluded: boolean;
homeDelivery: boolean;
worldwideDelivery: boolean;
fragile: boolean;
domesticDeliveries: boolean;
information: string | null;
description: string;
height: number; // Package height in cm
length: number; // Package length in cm
width: number; // Package width in cm
weight: number; // Package weight in kg
maxWeight: number; // Max weight this method can handle (kg) - alias for weight
type: string;
price: number; // Shipit price in cents
pickupPoint: boolean; // Pickup point selection available
onlyParchelLocker: boolean; // Only parcel locker delivery
shipmentMethodId: string;
createdAt: string;
updatedAt: string;
}Note: The maxWeight field is an alias for weight that indicates the maximum cart weight this shipping method can handle. Use this value to filter shipping methods based on the total weight of items in the cart.
PickupLocation
Pickup location (parcel locker, pickup point) with pricing:
interface PickupLocation {
id: string;
name: string; // e.g., "K-Market Kamppi"
address1: string; // Street address
zipcode: string;
city: string;
countryCode: string; // e.g., "FI"
serviceId: string; // Shipit service ID
carrier: string; // Carrier name
price: number | null; // Shipit price in cents
merchantPrice: number | null; // Store's price in cents
carrierLogo: string; // Carrier logo URL
openingHours: PickupLocationOpeningHours | null;
openingHoursRaw: string | null;
latitude: number; // GPS latitude
longitude: number; // GPS longitude
distanceInMeters: number;
distanceInKilometers: number;
metadata: unknown | null;
}PickupLocationOpeningHours
Opening hours structure:
interface PickupLocationOpeningHours {
monday: string[]; // e.g., ["08:00-21:00"]
tuesday: string[];
wednesday: string[];
thursday: string[];
friday: string[];
saturday: string[];
sunday: string[];
exceptions: string[]; // Holiday/special hours
}Examples
Shipping Method Selector
Display shipping options during checkout:
'use client';
import { useEffect, useState } from 'react';
import { storefront } from '@/lib/storefront';
import type { ShipmentMethod } from '@putiikkipalvelu/storefront-sdk';
interface ShippingMethodSelectorProps {
onSelect: (method: ShipmentMethod) => void;
}
export function ShippingMethodSelector({ onSelect }: ShippingMethodSelectorProps) {
const [methods, setMethods] = useState<ShipmentMethod[]>([]);
const [selected, setSelected] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function loadMethods() {
const { shipmentMethods } = await storefront.shipping.getMethods();
setMethods(shipmentMethods);
setIsLoading(false);
}
loadMethods();
}, []);
const handleSelect = (method: ShipmentMethod) => {
setSelected(method.id);
onSelect(method);
};
if (isLoading) return <div>Loading shipping options...</div>;
return (
<div className="space-y-2">
{methods.map(method => (
<label
key={method.id}
className={`flex items-center p-4 border rounded cursor-pointer ${
selected === method.id ? 'border-primary bg-primary/5' : ''
}`}
>
<input
type="radio"
name="shipping"
checked={selected === method.id}
onChange={() => handleSelect(method)}
className="mr-3"
/>
<div className="flex-1">
<div className="font-medium">{method.name}</div>
{method.description && (
<div className="text-sm text-gray-500">{method.description}</div>
)}
{method.min_estimate_delivery_days && method.max_estimate_delivery_days && (
<div className="text-sm text-gray-500">
{method.min_estimate_delivery_days}-{method.max_estimate_delivery_days} days
</div>
)}
</div>
<div className="font-medium">
{(method.price / 100).toFixed(2)} EUR
</div>
</label>
))}
</div>
);
}Pickup Location Selector
Let customer choose a pickup point:
'use client';
import { useState, useEffect } from 'react';
import { storefront } from '@/lib/storefront';
import type { PickupLocation } from '@putiikkipalvelu/storefront-sdk';
interface PickupLocationSelectorProps {
postalCode: string;
onSelect: (location: PickupLocation) => void;
}
export function PickupLocationSelector({ postalCode, onSelect }: PickupLocationSelectorProps) {
const [locations, setLocations] = useState<PickupLocation[]>([]);
const [selected, setSelected] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function loadLocations() {
if (!postalCode || postalCode.length < 5) {
setLocations([]);
setIsLoading(false);
return;
}
setIsLoading(true);
setError(null);
try {
const { pricedLocations } = await storefront.shipping.getWithLocations(postalCode, {
cache: 'no-store'
});
setLocations(pricedLocations);
} catch (err) {
setError('Failed to load pickup locations');
setLocations([]);
} finally {
setIsLoading(false);
}
}
loadLocations();
}, [postalCode]);
const handleSelect = (location: PickupLocation) => {
setSelected(location.id);
onSelect(location);
};
if (isLoading) return <div>Loading pickup locations...</div>;
if (error) return <div className="text-red-500">{error}</div>;
if (locations.length === 0) return <div>No pickup locations found</div>;
return (
<div className="space-y-2 max-h-96 overflow-y-auto">
{locations.map(location => (
<label
key={location.id}
className={`flex items-start p-4 border rounded cursor-pointer ${
selected === location.id ? 'border-primary bg-primary/5' : ''
}`}
>
<input
type="radio"
name="pickup-location"
checked={selected === location.id}
onChange={() => handleSelect(location)}
className="mr-3 mt-1"
/>
<div className="flex-1">
<div className="flex items-center gap-2">
{location.carrierLogo && (
<img
src={location.carrierLogo}
alt={location.carrier}
className="h-5 w-auto"
/>
)}
<span className="font-medium">{location.name}</span>
</div>
<div className="text-sm text-gray-500">
{location.address1}, {location.zipcode} {location.city}
</div>
<div className="text-sm text-gray-400">
{location.distanceInKilometers.toFixed(1)} km away
</div>
</div>
<div className="text-right">
<div className="font-medium">
{((location.merchantPrice ?? 0) / 100).toFixed(2)} EUR
</div>
<div className="text-xs text-gray-500">{location.carrier}</div>
</div>
</label>
))}
</div>
);
}Filter by Carrier
const { pricedLocations } = await storefront.shipping.getWithLocations("00100");
// Filter by carrier
const postiLocations = pricedLocations.filter(
loc => loc.carrier === "Posti"
);
const matkahuoltoLocations = pricedLocations.filter(
loc => loc.carrier === "Matkahuolto"
);
// Sort by distance
const sortedByDistance = [...pricedLocations].sort(
(a, b) => a.distanceInMeters - b.distanceInMeters
);
// Get closest location
const closestLocation = sortedByDistance[0];Weight-Based Filtering
Filter shipping methods based on the total weight of items in the cart. This ensures customers only see shipping options that can handle their order weight.
import { storefront } from '@/lib/storefront';
import type { CartItem, Product, ProductVariation } from '@putiikkipalvelu/storefront-sdk';
// Calculate total cart weight
function calculateCartWeight(items: CartItem[]): number {
return items.reduce((total, item) => {
// Use variation weight if available, otherwise product weight
const itemWeight = item.variation?.weight ?? item.product.weight ?? 0.5;
return total + (itemWeight * item.quantity);
}, 0);
}
// Fetch shipping methods filtered by cart weight
async function getShippingForCart(items: CartItem[], postalCode?: string) {
const cartWeight = calculateCartWeight(items);
if (postalCode) {
// Get methods with pickup locations, filtered by weight
const { shipmentMethods, pricedLocations } = await storefront.shipping.getWithLocations(
postalCode,
{ cartWeight }
);
return { shipmentMethods, pricedLocations, cartWeight };
} else {
// Get methods without locations, filtered by weight
const { shipmentMethods } = await storefront.shipping.getMethods({ cartWeight });
return { shipmentMethods, pricedLocations: [], cartWeight };
}
}
// Usage example
const cartItems = [
{ product: { weight: 0.5 }, quantity: 2 }, // 1.0 kg
{ product: { weight: 1.2 }, quantity: 1 }, // 1.2 kg
];
// Total: 2.2 kg
const { shipmentMethods, cartWeight } = await getShippingForCart(cartItems, "00100");
console.log(`Cart weight: ${cartWeight} kg`);
shipmentMethods.forEach(method => {
if (method.shipitMethod) {
console.log(`${method.name} - Max weight: ${method.shipitMethod.maxWeight} kg`);
} else {
console.log(`${method.name} - No weight limit (manual method)`);
}
});How Weight Filtering Works:
- When
cartWeightis provided, the API filters out Shipit methods wheremaxWeight < cartWeight - Manual shipping methods (without Shipit integration) always pass the filter - they have no weight limits
- If
cartWeightis not provided, all active shipping methods are returned - The
maxWeightfield onShipitShippingMethodindicates the maximum cart weight that method can handle
Default Product Weight:
- Products have a default weight of 0.5 kg if not specified
- Variations can override the product weight with their own
weightfield - If a variation has
weight: null, it inherits the product's weight
Show Opening Hours
function formatOpeningHours(hours: PickupLocationOpeningHours | null) {
if (!hours) return 'Hours not available';
const days = [
{ name: 'Mon', hours: hours.monday },
{ name: 'Tue', hours: hours.tuesday },
{ name: 'Wed', hours: hours.wednesday },
{ name: 'Thu', hours: hours.thursday },
{ name: 'Fri', hours: hours.friday },
{ name: 'Sat', hours: hours.saturday },
{ name: 'Sun', hours: hours.sunday },
];
return days.map(day => (
`${day.name}: ${day.hours.length > 0 ? day.hours.join(', ') : 'Closed'}`
)).join('\n');
}
// Usage
const { pricedLocations } = await storefront.shipping.getWithLocations("00100");
pricedLocations.forEach(location => {
console.log(location.name);
console.log(formatOpeningHours(location.openingHours));
});Checkout Integration
Complete checkout flow with shipping selection:
'use client';
import { useState } from 'react';
import { ShippingMethodSelector } from './ShippingMethodSelector';
import { PickupLocationSelector } from './PickupLocationSelector';
import type { ShipmentMethod, PickupLocation } from '@putiikkipalvelu/storefront-sdk';
export function CheckoutShipping() {
const [postalCode, setPostalCode] = useState('');
const [selectedMethod, setSelectedMethod] = useState<ShipmentMethod | null>(null);
const [selectedLocation, setSelectedLocation] = useState<PickupLocation | null>(null);
const needsPickupLocation = selectedMethod?.shipitMethod?.pickupPoint === true;
return (
<div className="space-y-6">
<div>
<h3 className="font-medium mb-2">Delivery Address</h3>
<input
type="text"
placeholder="Postal Code"
value={postalCode}
onChange={(e) => setPostalCode(e.target.value)}
className="px-4 py-2 border rounded"
/>
</div>
<div>
<h3 className="font-medium mb-2">Shipping Method</h3>
<ShippingMethodSelector onSelect={setSelectedMethod} />
</div>
{needsPickupLocation && postalCode.length >= 5 && (
<div>
<h3 className="font-medium mb-2">Select Pickup Point</h3>
<PickupLocationSelector
postalCode={postalCode}
onSelect={setSelectedLocation}
/>
</div>
)}
<div className="p-4 bg-gray-50 rounded">
<h4 className="font-medium">Shipping Summary</h4>
{selectedMethod && (
<p>Method: {selectedMethod.name} - {(selectedMethod.price / 100).toFixed(2)} EUR</p>
)}
{selectedLocation && (
<p>Pickup: {selectedLocation.name}, {selectedLocation.city}</p>
)}
</div>
</div>
);
}Error Handling
import {
NotFoundError,
ValidationError,
StorefrontError
} from '@putiikkipalvelu/storefront-sdk';
try {
const { pricedLocations } = await storefront.shipping.getWithLocations(postalCode);
} catch (error) {
if (error instanceof ValidationError) {
// Invalid or missing postal code
console.error('Invalid postal code');
}
if (error instanceof NotFoundError) {
// Shipit integration not configured for this store
console.error('Shipping service not available');
}
if (error instanceof StorefrontError && error.status === 502) {
// External shipping service unavailable
console.error('Shipping service temporarily unavailable');
}
throw error;
}