> ## 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.

# Shipping Rate Functions

> Add custom shipping rates that appear alongside merchant zones

# 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

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

    Customer->>Checkout: Enters / changes shipping address
    Checkout->>Backend: Verify cart
    Backend->>YourFunction: cart + destination
    YourFunction->>Backend: { rates: [...] }
    Backend->>Checkout: customShippingRates[]
    Checkout->>Customer: Renders rates beneath store zones
```

## Function Manifest

```json theme={null}
{
  "handle": "my-shipping-app",
  "name": "Custom Shipping",
  "version": "1.0.0",
  "extensions": {
    "functions": [
      {
        "type": "shipping_rate",
        "handle": "custom-rates",
        "title": "Custom Shipping Rates",
        "entrypoint": "dist/shipping-rate.wasm"
      }
    ]
  }
}
```

## Input Schema

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

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

## Output Schema

```typescript theme={null}
interface ShippingRateOutput {
  rates: ShippingRate[];
}

interface ShippingRate {
  name: string;            // Required — shown as the radio label
  price: number;           // Required — in display currency. 0 = free
  description?: string;    // Sub-line under the name
  minDeliveryDays?: number; // Optional — estimated delivery window (min)
  maxDeliveryDays?: number; // Optional — estimated delivery window (max)

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

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

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

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

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

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

## 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

<AccordionGroup>
  <Accordion title="Keep currency consistent">
    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.
  </Accordion>

  <Accordion title="Return early when destination is unknown">
    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.
  </Accordion>

  <Accordion title="Provide fallbacks for carrier API outages">
    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.
  </Accordion>

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

## See Also

* [Delivery Customization Functions](/functions/delivery-customization) —
  rename/hide/reorder existing shipping zones (don't add new ones).
* [Cart Transform Functions](/functions/cart-transform) — per-line price
  changes; runs before shipping rate calculation.
