Putiikkipalvelu Docs
SDK

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:

ParamTypeRequiredDescription
paramsGetMethodsParamsNoOptional parameters including cartWeight
optionsFetchOptionsNoFetch options (caching, headers, etc.)

GetMethodsParams:

ParamTypeRequiredDescription
cartWeightnumberNoTotal 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:

ParamTypeRequiredDescription
postalCodestringYesCustomer's postal code (e.g., "00100")
paramsGetWithLocationsParamsNoOptional parameters including cartWeight
optionsFetchOptionsNoFetch options (caching, headers, etc.)

GetWithLocationsParams:

ParamTypeRequiredDescription
cartWeightnumberNoTotal cart weight in kg. When provided, filters out shipping methods and pickup locations that cannot handle this weight.

Returns: Promise<ShipmentMethodsWithLocationsResponse>

Throws:

  • ValidationError if postal code is missing
  • NotFoundError if Shipit integration not configured
  • StorefrontError (502) if external shipping service fails

Fetch Options

All methods accept an optional options object:

OptionTypeDescription
signalAbortSignalCancel the request
cacheRequestCacheFetch cache mode
next.revalidatenumber | falseNext.js ISR revalidation time
next.tagsstring[]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 cartWeight is provided, the API filters out Shipit methods where maxWeight < cartWeight
  • Manual shipping methods (without Shipit integration) always pass the filter - they have no weight limits
  • If cartWeight is not provided, all active shipping methods are returned
  • The maxWeight field on ShipitShippingMethod indicates 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 weight field
  • 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;
}

On this page