Skip to main content

Delivery Customization Functions

A delivery_customization function modifies the merchant’s existing shipping zones before they render at checkout. Use it to add delivery dates to a zone name, hide a zone for some addresses, or pin a free zone to the top of the list. To add a brand-new rate, use Shipping Rate Functions. To filter built-in zones, use this one. Runs at both cart verification (preview) and order placement (enforcement). A stale frontend that submits a hidden zone is rejected server-side with:
"Shipping option '<zone-name>' is not available for this order"

How It Works

Function Manifest

{
  "handle": "my-delivery-app",
  "name": "Delivery Customizer",
  "version": "1.0.0",
  "functions": {
    "delivery_customization": {
      "handle": "delivery-rules",
      "name": "Delivery Display Rules",
      "config": {
        "renameStandard": "Economy Shipping"
      }
    }
  }
}

Input Schema

interface DeliveryCustomizationInput {
  cart: {
    items: CartItem[];
    totalPrice: number;       // Subtotal in display currency
    itemCount: number;
    currency: string;
  };
  deliveryOptions: DeliveryOption[];
  shippingAddress: {
    firstName: string;
    lastName: string;
    address1: string;
    address2: string;
    city: string;
    province: string;
    country: string;
    zip: string;
    phone: string;
  };
}

interface DeliveryOption {
  handle: string;             // shippingZoneId (merchant zone primary key)
  title: string;              // shippingName (merchant-typed name)
  price: number;              // Display currency
}

interface CartItem {
  id: string;
  variantId: string;
  productId: string;
  title: string;
  quantity: number;
  price: number;              // Effective unit price (display currency)
  originalPrice: number;
}
The deliveryOptions list contains only merchant-configured shipping zones — not the app rates added by shipping_rate functions. To customise an app rate, return it differently from your shipping_rate function in the first place.

Output Schema

interface DeliveryCustomizationOutput {
  operations: DeliveryOperation[];
}

type DeliveryOperation =
  | { hide:    { handle: string } }              // by shippingZoneId
  | { hide:    { deliveryOption: string } }      // by shippingName
  | { rename:  { handle: string; name: string } }
  | { rename:  { deliveryOption: string; name: string } }
  | { move:    { handle: string; position: number } }
  | { move:    { deliveryOption: string; position: number } };
Either handle (zone UUID) or deliveryOption (zone name) works as the matcher — the backend checks both fields for every op.
reorder is accepted as an alias for move — both produce the same result. New code should use move.

Examples

Add delivery date to the name

function customizeDelivery(input) {
  const today = new Date();
  const fmt = d => d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
  const addDays = n => new Date(today.getTime() + n * 86400000);

  return {
    operations: input.deliveryOptions.map(opt => ({
      rename: {
        handle: opt.handle,
        name: `${opt.title} — arrives by ${fmt(addDays(opt.title.includes('Express') ? 3 : 7))}`
      }
    }))
  };
}

Hide express on weekends

function customizeDelivery(input) {
  const day = new Date().getDay();
  const isWeekend = day === 0 || day === 6;
  if (!isWeekend) return { operations: [] };

  return {
    operations: input.deliveryOptions
      .filter(o => /express|overnight|same.?day/i.test(o.title))
      .map(o => ({ hide: { handle: o.handle } }))
  };
}

Pin the cheapest to position 0

function customizeDelivery(input) {
  const cheapest = [...input.deliveryOptions].sort((a, b) => a.price - b.price)[0];
  if (!cheapest) return { operations: [] };
  return {
    operations: [{ move: { handle: cheapest.handle, position: 0 } }]
  };
}

Free-shipping nudge

function customizeDelivery(input, config) {
  const threshold = config.freeShippingThreshold || 50;
  const remaining = threshold - input.cart.totalPrice;
  if (remaining <= 0 || !input.deliveryOptions.length) return { operations: [] };

  const cheapest = [...input.deliveryOptions].sort((a, b) => a.price - b.price)[0];
  return {
    operations: [{
      rename: {
        handle: cheapest.handle,
        name: `${cheapest.title} — add ${remaining.toFixed(2)} more for FREE`
      }
    }]
  };
}

How it renders in the UI

The checkout shipping step reads the deliveryCustomizations returned by cart verification and applies hide/rename ops before rendering the radio list. Reorder is honoured the same way.
Shipping Method
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

○ Standard — add ₹150.00 more for FREE     ← renamed
○ Express — arrives by Mar 14              ← renamed
                                           ← Same-day hidden

Best Practices

With no shipping zone visible, the customer can’t place the order. Always leave one zone reachable, or pair the hide with a clear error in an order_validation function.
deliveryOption (the name) works but merchants can rename their zones — handle doesn’t change.
Long names wrap badly on mobile checkout. 1–2 lines max.
Drive /checkout with Puppeteer and screenshot the shipping radio. The hide/rename/reorder flow is wired through the checkout UI — JSON-only tests miss that path.

See Also