Skip to content
HeapByte — Senior Shopify Engineering Studio

Architecture · 20 October 2026

Delivery customization functions in Shopify Plus: gating shipping methods by freight, address, and cart lines

An offroad automotive outfitter sells heavy suspension lift kits, steel recovery winches, and auxiliary lithium power packs alongside standard branded apparel and cabin air filters. A retail customer adds an 85-kilogram steel bumper, two 12V lithium-ion auxiliary battery modules, and a t-shirt to their cart, entering a remote rural PO Box as the destination address. At checkout, Shopify's default shipping configuration presents an overnight express air courier option for £32. The customer completes the transaction, only for the warehouse dispatch team to discover twenty-four hours later that commercial freight couriers refuse to deliver heavy pallets to post office boxes, while aviation safety regulations strictly prohibit high-capacity lithium batteries on standard air cargo flights without specialized hazardous material declarations.

8 min read
Written by the HeapByte engineering team

Shopify Delivery Customization Shipping rules

The death of legacy shipping scripts and client DOM hacks

For over a decade, complex Shopify Plus logistics relied on Script Editor. Developers wrote small snippets of Ruby that iterated over shipping rates, inspecting cart attributes and deleting air couriers or standard post whenever heavy items were detected.

Merchants on standard plans or those attempting to cut corners frequently resorted to theme JavaScript: listening for address field changes on the cart page and attempting to hide shipping methods via CSS selectors or redirecting buyers before checkout reached the payment step.

Both mechanisms are dead or dying: legacy Shopify Scripts are permanently retired under Checkout Extensibility, executing in an isolated sandbox where Ruby scripts do not run, while the Checkout Extensibility DOM is entirely closed to external script tags, jQuery listeners, and CSS hacks.

If shipping options must adapt dynamically based on line-item characteristics, weight distributions, or destination address constraints, the rule must execute server-side directly within Shopify's checkout engine.

How Delivery Customization Functions execute

Shopify Functions replace legacy scripts with compiled WebAssembly binaries executed directly on Shopify's distributed infrastructure. When a customer inputs or updates their shipping address during checkout, Shopify gathers candidate delivery rates from your configured Shipping Profiles and external Carrier Service APIs.

Before those shipping methods are presented to the buyer, Shopify triggers your registered Delivery Customization Function (purchase.delivery-customization.run).

This execution pipeline imposes strict system constraints: a deterministic execution budget of under 5 milliseconds and 256 KB memory, zero network I/O (cannot query external carrier APIs at runtime), declarative mutations only (hide, rename, move), and a limit of 5 active delivery customization functions per store.

Evaluating cart lines: hazmat, freight tiers, and address geometry

To enforce accurate logistics rules, your checkout function must evaluate three intersecting data planes:

1. Line-Item Merchandise Metafields: Standard Shopify variants provide only gross weight in grams. A 20-kilogram steel recovery winch and a 20-kilogram container of shock damper fluid represent completely distinct logistics risks. By binding structured metafields (custom.requires_freight, custom.is_hazardous) to variants, the checkout function can identify whether items require LTL freight or face civil aviation cargo embargoes.

2. Destination Address Geometry: Parcel carriers deliver to Post Office boxes, but commercial freight carriers operating 18-tonne lorries require physical roadside access or commercial loading docks. A delivery customization rule must inspect address lines using robust international regex patterns that recognise standard PO Boxes, German Postfach, Dutch Postbus, and French Boîte Postale entries.

3. Aggregate Weight Floors: While an individual line item may be small, a customer purchasing ten cast-iron brake rotors creates an aggregate shipment weight exceeding 70 kilograms, necessitating an automatic switch to palletised freight.

Pure WebAssembly delivery rules without checkout freezes

The delivery customization function must be pure and deterministic. It evaluates line-item hazmat classifications, detects PO Box address entries, suppresses ineligible shipping methods, prioritises freight services, and enforces a defensive fallback to ensure checkout never freezes:

ts
export interface DeliveryOption {
  handle: string;
  title: string;
  description?: string;
  cost: number;
}

export interface DeliveryAddress {
  address1?: string;
  address2?: string;
  city?: string;
  provinceCode?: string;
  countryCode: string;
  zip?: string;
}

export interface CartLineItem {
  id: string;
  sku: string;
  quantity: number;
  weightGrams: number;
  requiresFreight?: boolean;
  isHazardous?: boolean; // Lithium batteries, aerosols, winches, damper fluid
}

export interface DeliveryCustomizationInput {
  deliveryOptions: DeliveryOption[];
  deliveryAddress?: DeliveryAddress;
  lines: CartLineItem[];
}

export type Operation =
  | { hide: { deliveryOptionHandle: string } }
  | { rename: { deliveryOptionHandle: string; title: string } }
  | { move: { deliveryOptionHandle: string; index: number } };

export interface DeliveryCustomizationResult {
  operations: Operation[];
  errors: string[];
  warnings: string[];
  retainedHandles: string[];
}

const PO_BOX_REGEX = /\b(p[\s.]*o[\s.]*box|post[\s.]*office[\s.]*box|p\.?o\.?b\.?|box\s+\d+|postfach|postbus|bo[iî]te\s+postale)\b/i;
const AIR_EXPRESS_REGEX = /\b(air|express|overnight|next\s*day|priority|aviation)\b/i;
const PARCEL_GROUND_REGEX = /\b(standard|ground|economy|parcel|royal\s*mail)\b/i;
const FREIGHT_REGEX = /\b(freight|ltl|pallet|heavy|cargo|curbside)\b/i;

/**
 * Pure WebAssembly function handler for Shopify Checkout Extensibility Delivery Customization.
 * Evaluates cart line hazmat/freight properties and destination address rules,
 * emitting hide, rename, and move operations while strictly guarding against
 * the "all options hidden" checkout freeze trap.
 */
export function deliveryCustomizationRun(input: DeliveryCustomizationInput): DeliveryCustomizationResult {
  const operations: Operation[] = [];
  const errors: string[] = [];
  const warnings: string[] = [];

  const { deliveryOptions = [], deliveryAddress, lines = [] } = input;

  if (deliveryOptions.length === 0) {
    return {
      operations: [],
      errors: ["No delivery options provided by carrier services or shipping profiles."],
      warnings: [],
      retainedHandles: [],
    };
  }

  // 1. Analyze cart contents
  let totalWeightGrams = 0;
  let hasFreightLines = false;
  let hasHazmatLines = false;

  for (const line of lines) {
    totalWeightGrams += line.weightGrams * line.quantity;
    if (line.requiresFreight || line.weightGrams >= 31500) {
      hasFreightLines = true;
    }
    if (line.isHazardous) {
      hasHazmatLines = true;
    }
  }

  const isOverweight = totalWeightGrams > 70000; // 70 kg freight threshold
  const requiresCommercialFreight = hasFreightLines || isOverweight;

  // 2. Analyze destination address
  const fullAddressString = [deliveryAddress?.address1, deliveryAddress?.address2].filter(Boolean).join(" ");
  const isPoBox = PO_BOX_REGEX.test(fullAddressString);

  // 3. Track candidates for hiding, renaming, and moving
  const handlesToHide = new Set<string>();
  const renames = new Map<string, string>();
  let moveToTopHandle: string | null = null;

  // Rule A: Hazardous goods cannot fly via Air / Express due to IATA / aviation safety regulations
  if (hasHazmatLines) {
    for (const option of deliveryOptions) {
      if (AIR_EXPRESS_REGEX.test(option.title)) {
        handlesToHide.add(option.handle);
      }
    }
  }

  // Rule B: Heavy Freight / Pallets cannot be delivered to PO Boxes
  if (requiresCommercialFreight) {
    if (isPoBox) {
      errors.push("PO Box addresses cannot accept heavy freight or pallet shipments. Physical street address required.");
    } else {
      // For heavy freight on valid street addresses, hide standard parcel methods in favour of freight
      for (const option of deliveryOptions) {
        if (PARCEL_GROUND_REGEX.test(option.title) && !FREIGHT_REGEX.test(option.title)) {
          handlesToHide.add(option.handle);
        }
      }
    }

    // Prioritize and annotate Freight options
    const freightOption = deliveryOptions.find((opt) => FREIGHT_REGEX.test(opt.title));
    if (freightOption && !handlesToHide.has(freightOption.handle)) {
      moveToTopHandle = freightOption.handle;
      if (!freightOption.title.includes("Liftgate")) {
        renames.set(freightOption.handle, `${freightOption.title} (Curbside Pallet & Liftgate)`);
      }
    }
  }

  // 4. Defensive Guard: The "All Options Hidden" Checkout Freeze Trap
  // If hiding candidates would eliminate every single shipping rate,
  // checkout completely freezes with an opaque error. We must preserve at least one viable rate.
  let remainingOptions = deliveryOptions.filter((opt) => !handlesToHide.has(opt.handle));

  if (remainingOptions.length === 0) {
    warnings.push("All delivery options matched suppression criteria. Safety fallback engaged to prevent checkout freeze.");

    // Find the safest fallback option (preferably ground or freight, avoiding air hazmat violations if possible)
    const fallbackOption =
      deliveryOptions.find((opt) => !AIR_EXPRESS_REGEX.test(opt.title)) || deliveryOptions[0];

    handlesToHide.delete(fallbackOption.handle);
    renames.set(fallbackOption.handle, `${fallbackOption.title} (Manual Logistics Review Required)`);
    remainingOptions = [fallbackOption];
  }

  // 5. Construct declarative operations payload
  for (const handle of handlesToHide) {
    operations.push({ hide: { deliveryOptionHandle: handle } });
  }

  for (const [handle, newTitle] of renames.entries()) {
    if (!handlesToHide.has(handle)) {
      operations.push({ rename: { deliveryOptionHandle: handle, title: newTitle } });
    }
  }

  if (moveToTopHandle && !handlesToHide.has(moveToTopHandle)) {
    operations.push({ move: { deliveryOptionHandle: moveToTopHandle, index: 0 } });
  }

  const retainedHandles = remainingOptions.map((opt) => opt.handle);

  return {
    operations,
    errors,
    warnings,
    retainedHandles,
  };
}
Why the function inspects surviving options before emitting hide operations: in an early release of checkout extensibility shipping logic, our rule engine suppressed standard parcel couriers whenever freight items were present and suppressed air couriers whenever hazardous batteries were detected. When a customer ordered both an offroad winch and a lithium battery pack to a remote postal zone where only express air and standard ground couriers were configured, the function hid both available options. Shopify checkout crashed with an opaque 'Shipping method not available' dialog, completely halting checkout. Introducing the defensive fallback check guarantees that if all options match hiding criteria, the least hazardous option is retained, renamed with a manual review notice, and preserved so the sale can complete.

The 'All Options Hidden' crash and defensive fallback design

The single most dangerous failure in Delivery Customization Functions is over-suppression. When you configure Shopify Shipping Profiles, you define rates per shipping zone. If a merchant sets up standard ground and express air profiles, but forgets to configure a dedicated freight rate for remote regional destinations, what happens when a customer orders a heavy pallet?

If your function blindly emits hide operations for both parcel methods because the order exceeds 70kg, the remaining options array becomes empty. In Shopify checkout, an empty delivery options array causes checkout to freeze with a generic error banner: 'There are no shipping methods available for your address.' The customer cannot select a delivery method and cannot complete payment.

As illustrated in our implementation of Shopify checkout validation functions and B2B payment customization rules, checkout extensions must fail gracefully.

Our defensive architecture inspects candidate deletions against the full set of provided delivery options. If the suppression logic would zero out all rates, it cancels the hide operation on the safest fallback, appends '(Manual Logistics Review Required)' via rename, and permits checkout to proceed so revenue is captured while alerting warehouse dispatch.

What native Shopify delivery customization cannot do

While Delivery Customization Functions provide high-speed, server-side rule execution, they operate within strict platform boundaries:

  • Cannot calculate real-time shipping prices: Delivery Customization Functions cannot compute dynamic currency amounts or adjust shipping charges. They can only hide, rename, or move rates that already exist. Dynamic dimensional pricing requires an external Carrier Service API or Shopify Shipping Profiles.
  • Cannot query external logistics APIs: Functions execute in a zero-network sandbox and cannot call DPD, FedEx, or Royal Mail APIs at checkout runtime to check live route disruptions or localized postal strikes.
  • Cannot enforce post-purchase split shipments: If a customer orders one parcel item and one freight pallet, splitting the order into separate shipments with individual dispatch tracking numbers must be handled downstream in ERP or WMS software.
  • Cannot inject interactive address pickers: Delivery Customization is a backend decision function, not a user interface. Interactive parcel locker maps or pickup point selectors must be built as Checkout UI Extensions rendered alongside delivery options.

When this needs an engineer

You do not need a custom engineering build if your catalogue consists of homogeneous consumer goods that fit comfortably into standard courier satchels. Standard Shopify Shipping Profiles and native weight-tier tables handle simple threshold shipping reliably.

You need an engineer when your products introduce physical transportation boundaries: when high-capacity lithium batteries, chemicals, or aerosols face strict civil aviation shipping bans; when heavy machinery, bumpers, or furniture require commercial pallet freight that cannot be delivered to PO Boxes; and when complex freight rules must execute inside Shopify Plus Checkout Extensibility without freezing conversion.

Explore our custom Shopify applications case studies to see how brands architect dedicated purchasing workflows, or inspect the Forged 4x4 Shopify Plus case study to see how custom checkout calculations and business rules were integrated into enterprise commerce. You can also explore our Shopify Plus development services.

Send us the store and the symptom. We will audit your shipping profile architecture, review your hazmat metadata, and build a deterministic delivery customization function that protects your logistics margins without stranding customers at checkout.

Insights

Apply this to your store.

An audit turns the general principle into a specific list of changes, ordered by what actually pays back.