Skip to main content

Shipping Rate Functions

A shipping_rate function returns extra shipping options for the customer to pick at checkout. They render under the merchant’s configured shipping zones with your appName attribution and the customer can select them exactly like a built-in rate. Runs at both cart verification (cart preview) and order placement for parity. The customer’s selection is sent back to the order as payload.customShippingRate.

How It Works

Function Manifest

{
  "handle": "my-shipping-app",
  "name": "Custom Shipping",
  "version": "1.0.0",
  "functions": {
    "shipping_rate": {
      "handle": "custom-rates",
      "name": "Custom Shipping Rates",
      "config": {
        "freeShippingThreshold": 50,
        "expressRate": 12.99
      }
    }
  }
}

Input Schema

interface ShippingRateInput {
  cart: {
    items: CartItem[];
    totalPrice: number;   // Subtotal in display currency (e.g. 199.99)
    itemCount: number;
    currency: string;     // e.g. "INR", "USD"
  };
  destination: {
    country: string;
    city: string;
    province: string;
    zip: string;
  };
  shippingAddress?: {     // Only present on the order-placement dispatch
    firstName: string;
    lastName: string;
    address1: string;
    address2: string;
    city: string;
    province: string;
    country: string;
    zip: string;
    phone: string;
  };
  currency: string;
}

interface CartItem {
  id: string;             // lineItemId
  variantId: string;
  productId: string;
  title: string;
  quantity: number;
  price: number;          // Effective unit price (display currency)
  originalPrice: number;  // Compare-at unit price
}
All prices are in display currency units, not minor units (cents). price: 19.99 means ₹19.99 / $19.99 — return your shipping prices the same way.

Output Schema

interface ShippingRateOutput {
  rates: ShippingRate[];
}

interface ShippingRate {
  name: string;            // Required — shown as the radio label
  price: number;           // Required — in display currency. 0 = free
  currency?: string;       // Defaults to the cart currency
  description?: string;    // Sub-line under the name
  code?: string;           // Internal id, helpful for tracking

  // Optional — what kind of fulfillment this rate represents. Defaults
  // to "standard". Local pickup and pickup point rates render with an
  // extra indicator strip under the radio label.
  kind?: 'standard' | 'local_pickup' | 'pickup_point';
  pickup?: {
    location_id?: string;  // For local_pickup — your warehouse / store
    provider?: string;     // For pickup_point — e.g. "DHL", "UPS Access Point"
    address?: {
      address1?: string;
      city?: string;
      province?: string;
      country?: string;
      zip?: string;
    };
    window?: string;       // Free-text, e.g. "Mon–Fri 10am–6pm"
  };
}

Pickup variants

Return kind: 'local_pickup' when the customer collects from a store location. Set pickup.location_id to the warehouse/store gid. The checkout renders a “Local pickup” label and shows the address. Return kind: 'pickup_point' when handing off to a third-party network (parcel lockers, retail partners). Set pickup.provider to the network name. The checkout renders “Pickup point — <provider>” and shows the address and pickup window.
rates.push({
  name: 'Pick up at our flagship',
  price: 0,
  kind: 'local_pickup',
  pickup: {
    location_id: 'gid://launchmystore/Location/flagship-mumbai',
    address: { address1: '101 Linking Rd', city: 'Mumbai', country: 'IN', zip: '400050' },
    window: 'Mon–Sat 10am–8pm',
  },
});
Each rate is decorated by the platform with:
{
  ...yourRate,
  appId: '<app-handle>',
  source: 'app_function',
}
and surfaces on the checkout in the verified-cart response’s customShippingRates. The cart UI groups them by appId so a single app’s rates appear under a shared “Powered by <appName>” header.

Examples

Free shipping above a threshold

function calculateShippingRates(input, config) {
  const rates = [];
  const threshold = config.freeShippingThreshold || 50;

  if (input.cart.totalPrice >= threshold) {
    rates.push({
      name: 'Free Shipping',
      price: 0,
      description: '5–7 business days'
    });
  } else {
    rates.push({
      name: 'Standard Shipping',
      price: 5.99,
      description: '5–7 business days'
    });
  }

  return { rates };
}

Zone-based pricing

function calculateShippingRates(input) {
  const country = input.destination.country;
  const zones = {
    domestic:     { match: ['IN'],                 std: 5.99,  fast: 12.99 },
    neighbour:    { match: ['NP', 'BD', 'LK'],     std: 9.99,  fast: 19.99 },
    intl:         { match: null,                   std: 24.99,             }
  };

  const zone =
    Object.values(zones).find(z => z.match?.includes(country)) || zones.intl;

  const rates = [{ name: 'Standard', price: zone.std, description: '5–10 days' }];
  if (zone.fast) rates.push({ name: 'Express', price: zone.fast, description: '2–3 days' });
  return { rates };
}

Third-party carrier proxy

async function fetchCarrierRates(input, config) {
  const res = await fetch(config.apiEndpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${config.apiKey}` },
    body: JSON.stringify({
      destination: input.destination,
      packages: input.cart.items.map(i => ({
        quantity: i.quantity,
        price: i.price * i.quantity
      })),
      currency: input.cart.currency
    })
  });
  const carrier = await res.json();

  return {
    rates: carrier.map(r => ({
      name: r.serviceName,
      price: r.totalPrice,        // already in display currency from your API
      description: r.estimate,
      code: r.serviceCode
    }))
  };
}

How it renders in the UI

Shipping Method
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

○ Standard Shipping — ₹49.00          ← Merchant zone
○ Express Shipping  — ₹149.00         ← Merchant zone

  Powered by Custom Shipping
○ Same-Day Delivery — ₹299.00         ← Your function
○ Pickup at Store   — FREE            ← Your function
The customer can select any rate (merchant or app). When an app rate is selected, the frontend posts customShippingRate at order placement; the backend uses that price for the order total instead of the zone price.

Best Practices

Return price in the same currency the cart is using (input.cart.currency). Mismatched currencies log a warning and the rate may be applied without FX conversion.
On early cart-verification calls before the customer has typed their address, destination.country may be empty. Return { rates: [] } in that case to avoid surfacing a wrong rate.
Apps that proxy to carrier APIs should keep a static “best-guess” rate available. If your function throws or returns no rates, the customer only sees the merchant’s zones.
Drive /checkout on a development store with a headless browser and confirm your rate appears under the “Powered by <appName>” header. JSON-only tests are not proof — only the rendered radio is.

See Also