> ## Documentation Index
> Fetch the complete documentation index at: https://docs.launchmystore.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Delivery Customization Functions

> Rename, hide, or reorder the merchant's shipping zones at checkout

# 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](/functions/shipping-rate).
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

```mermaid theme={null}
sequenceDiagram
    participant Checkout
    participant Backend
    participant YourFunction

    Checkout->>Backend: Verify cart
    Backend->>YourFunction: cart + deliveryOptions + shippingAddress
    YourFunction->>Backend: { operations: [...] }
    Backend->>Checkout: Modified zones (hide/rename/reorder)
```

## Function Manifest

```json theme={null}
{
  "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

```typescript theme={null}
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;
}
```

<Note>
  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.
</Note>

## Output Schema

```typescript theme={null}
interface DeliveryCustomizationOutput {
  operations: DeliveryOperation[];
}

type DeliveryOperation =
  | { hide:    { handle: string } }              // by shippingZoneId
  | { hide:    { deliveryOption: string } }      // by shippingName
  | { rename:  { handle: string; title: string } }
  | { rename:  { deliveryOption: string; title: 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. On a `move` op,
`index` is accepted as an alias for `position` (either field works).

<Note>
  `reorder` is accepted as an alias for `move` — both produce the same
  result. New code should use `move`.
</Note>

## Examples

### Add delivery date to the name

```js theme={null}
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,
        title: `${opt.title} — arrives by ${fmt(addDays(opt.title.includes('Express') ? 3 : 7))}`
      }
    }))
  };
}
```

### Hide express on weekends

```js theme={null}
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

```js theme={null}
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

```js theme={null}
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,
        title: `${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

<AccordionGroup>
  <Accordion title="Don't hide everything">
    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.
  </Accordion>

  <Accordion title="Use `handle` (the zone UUID) for stable matching">
    `deliveryOption` (the name) works but merchants can rename their
    zones — `handle` doesn't change.
  </Accordion>

  <Accordion title="Keep renames short">
    Long names wrap badly on mobile checkout. 1–2 lines max.
  </Accordion>

  <Accordion title="Test against the real checkout">
    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.
  </Accordion>
</AccordionGroup>

## See Also

* [Shipping Rate Functions](/functions/shipping-rate) — add new rates
  rather than customise existing zones.
* [Payment Customization Functions](/functions/payment-customization) —
  same operation contract applied to payment methods.
