Guides

Developer guide

Shopify Delivery Customization Function deep dive: hide, rename, sort, and safely control shipping options at checkout

A technical Shopify developer guide to Delivery Customization Functions covering hide, rename, and move operations, delivery groups, subscription edge cases, merchant configuration, and the checkout pitfalls that appear in production.

Published by Addora
Updated March 21, 2026
18 min read
Editorial note: Delivery Customization is not a general-purpose shipping engine. It is the checkout delivery-option layer for hiding, renaming, and moving options that Shopify already calculated. The important design question is not just what you can change, but what this API never owns in the first place.

What Delivery Customization actually owns

Delivery Customization is Shopify’s checkout delivery-option layer. Its job is narrow: take the delivery options that Shopify already calculated for the checkout and then hide them, rename them, or move them. If the requirement is “change how existing shipping or pickup options are presented or filtered at checkout,” this is the right Function API.

That boundary matters because teams often treat it like a general shipping engine. It is not. The Delivery Customization Function API is specifically for renaming, sorting, and hiding the delivery options available to customers during checkout.

“A delivery customization enables you to rename, sort, and hide the delivery options available to customers during checkout.”

It also works on delivery options, not delivery-method generation in the broader sense. Shopify separates these concerns. Delivery options are choices like standard, express, local pickup, or pickup point variants. Delivery methods are the broader fulfillment modes that checkout supports, such as shipping, local pickup, and pickup points.

The clean mental model

Delivery Customization is the delivery-option presentation and filtering layer. Discounts own shipping savings. Validation owns checkout blocking. Delivery-option generator APIs own creating new pickup-style choices. Most shipping logic gets messy when those responsibilities are mixed.

If you are mapping old Shopify Scripts behavior into modern Functions, the

Shopify Scripts to Shopify Functions migration guide

is the right companion piece. For adjacent checkout server-side surfaces, the

Cart Transform Function deep dive

covers the upstream cart-shaping layer, while the

Shopify Discount Function deep dive

covers shipping-price logic that should stay out of Delivery Customization.

Hard constraints to design around first

Before you write the function, these constraints decide whether the design is even a fit:

  • a store can activate a maximum of 25 delivery customization functions
  • checkout is supported, cart is not
  • draft-order checkout is supported, draft order admin is not
  • POS support is partial and only available when shipping to a store
  • subscription recurring orders are not supported
  • pre-order and try-before-you-buy surfaces are not supported

“You can activate a maximum of 25 delivery customization functions on each store.”

The 25-function limit is more generous than Cart Transform’s single-slot model, but it still matters. If multiple apps hide, move, or rename the same rates, you now have a composition problem. Delivery logic becomes operationally fragile long before it becomes technically impossible.

The surface support matrix matters even more than most teams expect. Delivery Customization runs in checkout, not cart. If a merchant wants the customer to see the exact filtered delivery set earlier in the journey, that is a storefront UX problem, not something Delivery Customization itself solves.

Another easy miss is recurring subscriptions. The latest Delivery Customization docs still mark subscription recurring orders as unsupported, but the function input can now expose deliveryGroups.groupType with ONE_TIME_PURCHASE and SUBSCRIPTION. That means you can reason more clearly about mixed checkouts, but you should not mistake that for blanket recurring-shipment support.

Do not discover platform boundaries halfway through

Before building anything, answer four questions: is checkout the surface that matters, are subscription groups involved, will more than one app manipulate rates, and does the product requirement depend on selection behavior rather than just visibility or ordering. Those answers usually decide the implementation path.

The three operations and how they differ

Delivery Customization exposes three core operations:

  • deliveryOptionHide removes an option from the set the customer can choose from

  • deliveryOptionRename changes the displayed title of an option

  • deliveryOptionMove reorders an option within its delivery group

These operations sound simple, but they solve very different problems.

Hide is a policy filter. Rename is a presentation adjustment. Move is a prioritization hint. Teams get into trouble when they use one as a substitute for another. Renaming a rate to communicate eligibility is not the same thing as actually hiding ineligible options. Moving an option is not the same thing as reliably forcing a checkout default.

The function input is grouped around cart.deliveryGroups, not one flat list of rates. That is a crucial data-model detail. Each delivery group can have its own address context, its own delivery options, and its own selected option metadata.

That also explains where this guide sits in the broader Functions cluster. The

Cart Transform Function deep dive

is about how the cart gets shaped before checkout, while the

Shopify Discount Function deep dive

is about what savings apply once those delivery options exist. Delivery Customization lives between those concerns as the option-filtering and presentation layer.

query RunInput {
  cart {
    deliveryGroups {
      id
      groupType
      deliveryAddress {
        countryCode
        provinceCode
      }
      selectedDeliveryOption {
        handle
        title
        cost {
          amount
        }
      }
      deliveryOptions {
        handle
        title
        description
        deliveryMethodType
        cost {
          amount
        }
      }
    }
    buyerIdentity {
      customer {
        hasAnyTag(tags: ["vip", "wholesale"])
      }
    }
    lines {
      quantity
      merchandise {
        __typename
        ... on ProductVariant {
          product {
            hasAnyTag(tags: ["perishable", "local-only"])
          }
        }
      }
    }
  }
  deliveryCustomization {
    metafield(namespace: "$app:shipping" key: "function-configuration") {
      jsonValue
    }
  }
}

That grouping is why naive “loop all rates and hide what looks wrong” logic becomes dangerous in production. A checkout with one shipping group is easy. A checkout with one-time and subscription groups, or multiple fulfillment splits, is where hidden assumptions start breaking.

Building a hide rule safely

Hide operations are the most common pattern and the easiest to misuse. They look simple, but they directly affect whether checkout remains completable.

A strong hide rule is group-aware, deterministic, and conservative. It should read only the fields it truly needs, avoid fuzzy title matching where possible, and never assume there is always another valid fallback option in the group.

Shopify’s own examples show a safe pattern: read configuration from a JSON metafield, inspect address or product context, and return explicit deliveryOptionHide operations only when the condition is clearly met.

// @ts-check
 
/**
 * @typedef {import("../generated/api").RunInput} RunInput
 * @typedef {import("../generated/api").CartDeliveryOptionsTransformRunResult} CartDeliveryOptionsTransformRunResult
 * @typedef {import("../generated/api").Operation} Operation
 */
 
const NO_CHANGES = { operations: [] };
 
/**
 * @param {RunInput} input
 * @returns {CartDeliveryOptionsTransformRunResult}
 */
export function cartDeliveryOptionsTransformRun(input) {
  const config = input?.deliveryCustomization?.metafield?.jsonValue;
  if (!config) return NO_CHANGES;
 
  const buyerIsVip = input.cart?.buyerIdentity?.customer?.hasAnyTag ?? false;
  const groups = input.cart?.deliveryGroups ?? [];
  if (!groups.length) return NO_CHANGES;
 
  /** @type {Operation[]} */
  const operations = [];
 
  for (const group of groups) {
    const shouldHideExpressForThisGroup =
      group.groupType === "ONE_TIME_PURCHASE" &&
      !buyerIsVip &&
      group.deliveryAddress?.countryCode === "NL";
 
    if (!shouldHideExpressForThisGroup) continue;
 
    for (const option of group.deliveryOptions ?? []) {
      if (option.handle === config.expressHandle) {
        operations.push({
          deliveryOptionHide: {
            deliveryOptionHandle: option.handle,
          },
        });
      }
    }
  }
 
  return operations.length ? { operations } : NO_CHANGES;
}

The important production rule is not “hide the bad rate.” It is “hide the bad rate without accidentally removing the only viable option in a group.” That is especially important when subscription groups or checkout-created draft-order flows are involved.

Original insight

A hide rule is never just a filter. It is also an availability decision. Treat every hide operation as though you might be removing the group’s only survivable checkout path, because in some flows that is exactly what you are doing.

Renaming delivery options without lying to customers

Rename operations are best for clarification, not reinvention. They are useful when the merchant’s carrier or rate labels are technically correct but too vague for customers. Good rename logic makes the option more understandable without changing what the option really is.

Shopify documents an important limitation here: carrier names are automatically prepended when you rename carrier-backed shipping options, and you cannot remove that carrier portion through the API.

“The carrier name is automatically prepended to the shipping method title at checkout ... and can't be altered or omitted through the API.”

That means rename is a suffixing and clarification tool much more than a full title replacement mechanism for carrier-calculated rates.

const NO_CHANGES = { operations: [] };
 
/**
 * @param {import("../generated/api").RunInput} input
 * @returns {import("../generated/api").CartDeliveryOptionsTransformRunResult}
 */
export function cartDeliveryOptionsTransformRun(input) {
  const groups = input.cart?.deliveryGroups ?? [];
  const operations = [];
 
  for (const group of groups) {
    const provinceCode = group.deliveryAddress?.provinceCode;
 
    for (const option of group.deliveryOptions ?? []) {
      if (option.title === "Standard Shipping" && provinceCode === "NH") {
        operations.push({
          deliveryOptionRename: {
            deliveryOptionHandle: option.handle,
            title: "Standard Shipping (1-2 business days)",
          },
        });
      }
    }
  }
 
  return operations.length ? { operations } : NO_CHANGES;
}

The anti-pattern is using rename to create misleading promise language. Do not use this API to imply guaranteed delivery timing, service levels, or business rules that your fulfillment operation does not actually support.

Rename is strongest when it adds concrete, low-risk clarity:

  • timeframe context the merchant can actually meet
  • pickup wording that explains what the option means
  • local terminology the merchant’s audience already understands
  • disambiguation when two rates look too similar in checkout

Reordering options and the default-selection trap

Move operations are where many teams overestimate what the platform will let them control.

Yes, you can move a delivery option to a different index within the group. No, that does not make Delivery Customization a reliable “set the default shipping method” API.

Shopify’s Delivery Customization docs are explicit that if you reorder shipping delivery options, you are prohibited from automatically selecting higher-priced alternatives by default, and the cheapest shipping option must always be the first selected option.

“If you reorder shipping delivery options, then you are prohibited from automatically selecting higher-priced delivery alternatives by default. The cheapest shipping delivery option must always be the first option selected.”

In practice, that means move is useful for shaping the list, not for taking ownership of checkout selection behavior.

const NO_CHANGES = { operations: [] };
 
/**
 * Move the most expensive one-time delivery option to the second slot.
 * This is a prioritization choice, not a default-selection guarantee.
 */
export function cartDeliveryOptionsTransformRun(input) {
  const operations = [];
 
  for (const group of input.cart?.deliveryGroups ?? []) {
    if (group.groupType !== "ONE_TIME_PURCHASE") continue;
 
    let mostExpensive = null;
    for (const option of group.deliveryOptions ?? []) {
      if (!mostExpensive || Number(option.cost.amount) > Number(mostExpensive.cost.amount)) {
        mostExpensive = option;
      }
    }
 
    if (mostExpensive) {
      operations.push({
        deliveryOptionMove: {
          deliveryOptionHandle: mostExpensive.handle,
          index: 1,
        },
      });
    }
  }
 
  return operations.length ? { operations } : NO_CHANGES;
}

There is also a real production trap here: developers often build logic as if reorder and selection stay perfectly in sync. Community reports in late 2025 show merchants and app developers still running into confusing behavior when rates are reordered and the selected option does not update in the way they expected. That is not a safe place to build business-critical assumptions.

The working rule is simple: use move for ranking, not for coercion.

Subscription and multi-group edge cases

This is where otherwise clean delivery logic breaks.

Delivery Customization now exposes deliveryGroups.groupType, which gives you a much better way to distinguish ONE_TIME_PURCHASE groups from SUBSCRIPTION groups. That is a major improvement, because mixed carts are not hypothetical anymore. They are normal.

But group-type visibility does not remove the edge cases. It just makes them visible.

The hardest bug class is “reasonable hide logic that becomes unreasonable in a subscription group.” A common example is hiding a free or discounted rate for buyers who do not meet a condition. That can work perfectly for one-time groups and still strand a recurring shipment group with no viable visible option.

Shopify community discussions in 2025 highlighted exactly this failure mode: functions that sensibly hide a rate for most carts can leave a subscription checkout showing “Shipping not available” when the hidden rate was effectively the only selected option for the recurring shipment group.

Another practical issue is selected-option stability. The API exposes selectedDeliveryOption, but community reports show that it has been unreliable in some flows, including cases where it was always null in Delivery Customization and cases where draft-order checkout handles changed across renders. That means selection-aware logic should be treated as fragile unless you have tested it thoroughly in the exact checkout flow you care about.

ScenarioWhat goes wrongSafer pattern
One-time plus subscription cartGlobal hide rules remove a rate that only one group can safely useBranch logic by groupType and treat subscription groups separately
Split shipping checkoutAssuming one flat option list causes wrong operations on the wrong groupAlways iterate per delivery group and log group IDs
Draft-order checkoutSelection-tracking assumptions can become unstableAvoid business-critical logic that depends on stable selected-option handles
Carrier plus pickup mixTitle-based matching hides or renames the wrong thingPrefer handle- or config-based targeting and check deliveryMethodType

The real production rule

Do not write “shipping option logic.” Write “delivery-group logic.” The first phrase sounds harmless and is how teams end up shipping bugs that only appear when checkout splits the cart into multiple fulfillment realities.

Configuration design and merchant activation

Delivery Customization becomes maintainable only when configuration is explicit.

Shopify’s delivery-option tutorials recommend using metafields to store merchant-managed configuration. That is the right default. Delivery logic almost always includes store- specific data such as allowed countries, hidden handles, VIP-only rates, or province- based label suffixes. Hard-coding those values into the function is a short-lived win.

Merchants manage delivery customizations in Shopify admin under Settings > Shipping and Delivery > Delivery customizations. If your app provides a configuration UI, it should fit into that admin flow rather than inventing a disconnected mental model.

“Merchants can configure delivery customizations in the Shopify admin under Settings > Shipping and Delivery > Delivery customizations.”

Activation happens through the Admin GraphQL API. The core mutation is deliveryCustomizationCreate, which takes the function handle, title, enabled state, and optional metafields.

mutation DeliveryCustomizationCreate($deliveryCustomization: DeliveryCustomizationInput!) {
  deliveryCustomizationCreate(deliveryCustomization: $deliveryCustomization) {
    deliveryCustomization {
      id
      title
      enabled
    }
    userErrors {
      field
      message
    }
  }
}
{
  "deliveryCustomization": {
    "functionHandle": "shipping-rules-delivery-customization",
    "title": "Hide express for local-only conditions",
    "enabled": true,
    "metafields": [
      {
        "namespace": "$app:shipping",
        "key": "function-configuration",
        "type": "json",
        "value": "{\"expressHandle\":\"express-shipping\",\"countries\":[\"NL\",\"BE\"]}"
      }
    ]
  }
}

Strong configuration design usually follows these rules:

  • store handles, country lists, and targeting rules in JSON metafields
  • do not key critical logic off mutable display titles unless you have to
  • keep merchant-facing settings understandable enough to debug
  • treat configuration as policy, not as a dumping ground for every possible branch

If your UI becomes a giant rules engine, the product is probably trying to solve too much in one customization layer.

When Delivery Customization should not own the logic

Delivery Customization is useful precisely because it is narrow. The fastest way to make it brittle is to give it jobs it does not actually own.

Do not use it as your main solution for:

  • shipping discounts or free-shipping economics
  • creating brand new pickup-point or local-pickup options
  • blocking checkout with an error message
  • setting payment behavior
  • general checkout content or UI explanation on non-Plus storefront surfaces
  • guaranteeing a specific default shipping selection

The Scripts migration guide makes this split very clear. Shipping-script replacements in Shopify’s modern model map to both Delivery Customization and the Discounts API. Hide, rename, and reorder belong here. Shipping discounts do not.

For adjacent requirements, use the right layer:

The anti-pattern to avoid

When a team says “we’ll just handle all the shipping logic in the delivery function,” that is usually the sentence that precedes the bug backlog. Keep this layer focused on option filtering, ordering, and naming.

Limits, debugging reality, and production guardrails

Delivery Customization lives inside the broader Shopify Functions execution model, so it inherits the normal function constraints around instruction budget, input shape, and output shape. Even when your logic is simple, your debugging posture needs to be much better than “we can just inspect the UI.”

The highest-value production guardrails are operational, not cosmetic:

  • log delivery group IDs, group types, and option handles at decision time
  • log the exact metafield configuration the function read
  • record which operation was emitted for which handle
  • treat selected-option data as observational, not always authoritative
  • test one-time, mixed, split-shipping, and draft-order checkout flows separately

You should also keep your input query tight. Asking for every possible field because it might be useful later is a lazy habit that makes functions harder to reason about and more expensive than they need to be.

Another subtle safeguard is matching by durable identifiers where possible. Titles are customer-facing strings and merchants love changing them. Handles, configured mappings, and explicit method-type checks are safer anchors than “hide anything with ‘Express’ in the name.”

Finally, do not assume the checkout UI is your debugger. Function behavior can be technically correct while still creating a bad checkout state. The only serious way to debug this class of issue is to preserve enough application-side context that you can reconstruct why a rate was hidden, renamed, or moved for a specific delivery group.

Original insights that save real engineering time

These are the patterns I would optimize for early:

  1. Design by delivery group, not by shipping label. Handles, method types, and group boundaries are durable enough for production logic. Titles are not.

  2. Treat move as ranking, not selection control. If the business requirement depends on a default staying selected, Delivery Customization is the wrong owner.

  3. Keep shipping economics in Discount Functions. Once hide, rename, and pricing policy bleed together, the system becomes much harder to reason about.

  4. Assume mixed carts will show up sooner than planned. One-time, subscription, split-shipping, and draft-order flows should be explicit test cases, not cleanup work after launch.

  5. Use metafields as policy input, not as a full rules engine. The more merchant config tries to encode every branch, the harder checkout behavior is to predict and support.

  6. Map the whole Functions boundary before you code. Cart Transform, Delivery Customization, Discount Functions, and Validation solve adjacent problems, but they are not interchangeable surfaces.

The deepest lesson

Delivery Customization is easiest to use when you stop thinking of it as shipping logic and start thinking of it as a narrow checkout option-governance layer.

Best internal links

These are the best follow-on reads when Delivery Customization is only one part of a larger Shopify checkout architecture.

Sources and further reading

FAQ

Can Delivery Customization create a brand new shipping option?

No. Delivery Customization can only hide, rename, or move delivery options that Shopify has already produced for the checkout. If you need to generate pickup-point or local-pickup options, that is a different Function API.

Can I use Delivery Customization to force a specific shipping option to be selected by default?

Not safely as a general strategy. You can move options, but Shopify still controls checkout selection behavior. If your requirement depends on reliably forcing a more expensive or specific option to stay selected, Delivery Customization is the wrong mental model.

Should shipping discounts live in Delivery Customization?

No. Delivery Customization is for hiding, renaming, and reordering options. Shipping discounts belong in the Discounts API. Treating Delivery Customization as a pricing engine makes the design brittle fast.

What is the biggest production mistake with Delivery Customization?

Teams often write one global hide rule and assume every delivery group behaves like a simple one-time shipping checkout. Mixed carts, subscription groups, draft-order checkout flows, and selected-option assumptions are where the real bugs show up.

Related resources

Keep exploring the playbook

Guides

Shopify Scripts to Shopify Functions migration guide

A technical Shopify developer guide to migrating line item, shipping, and payment Scripts to Shopify Functions, unified Discount API, delivery and payment customizations, validation, and modern deployment workflows.

guidesShopify developerShopify Functions