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.
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:
deliveryOptionHideremoves an option from the set the customer can choose fromdeliveryOptionRenamechanges the displayed title of an optiondeliveryOptionMovereorders 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.
| Scenario | What goes wrong | Safer pattern |
|---|---|---|
| One-time plus subscription cart | Global hide rules remove a rate that only one group can safely use | Branch logic by groupType and treat subscription groups separately |
| Split shipping checkout | Assuming one flat option list causes wrong operations on the wrong group | Always iterate per delivery group and log group IDs |
| Draft-order checkout | Selection-tracking assumptions can become unstable | Avoid business-critical logic that depends on stable selected-option handles |
| Carrier plus pickup mix | Title-based matching hides or renames the wrong thing | Prefer 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:
Discount Functions
for shipping discounts and pricing policy
Checkout UI Extensions
for checkout-surface UI on supported Plus targets
Cart and Checkout Validation
for blocking invalid checkouts with messages
Pickup Point Delivery Option Generator
when you need to generate pickup-point options rather than merely filter existing ones
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:
Design by delivery group, not by shipping label. Handles, method types, and group boundaries are durable enough for production logic. Titles are not.
Treat move as ranking, not selection control. If the business requirement depends on a default staying selected, Delivery Customization is the wrong owner.
Keep shipping economics in Discount Functions. Once hide, rename, and pricing policy bleed together, the system becomes much harder to reason about.
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.
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.
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.
Cart Transform Function deep dive
for the upstream cart-structure layer that often gets confused with downstream checkout option logic.
Shopify Scripts to Shopify Functions migration guide
for the cleanest mapping from legacy shipping scripts into modern Functions.
Shopify Discount Function deep dive
for the pricing layer that should own shipping discounts instead of this API.
Shopify Checkout UI Extensions migration guide
for understanding where checkout interface work belongs once server-side delivery logic is in place.
Shopify Admin GraphQL patterns in Rails
for implementing activation and configuration mutations cleanly when this function lives inside a Rails-backed Shopify app.
Sources and further reading
Shopify Dev, Delivery Customization Function API
Shopify Dev, Build the delivery options function
Shopify Dev, About delivery and shipping functions
Shopify Dev, Add configuration to your delivery options function
Shopify Dev, UX for delivery options
Shopify Dev, deliveryCustomizationCreate
Shopify Dev, deliveryCustomizationUpdate
Shopify Dev, Migrating from Shopify Scripts to Shopify Functions
Shopify Developer Community, Use cart.deliveryGroups.groupType in Function APIs to determine the type of delivery group
Shopify Developer Community, Subscription Checkouts + Delivery Customization Function
Shopify Developer Community, Reordered shipping methods and default selection discussion
Shopify Developer Community, selectedDeliveryOption always null discussion
Shopify Developer Community, selectedDeliveryOption handle changes on every render for draft orders
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
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.
Shopify Discount Function deep dive: product, order, shipping, combination rules, code rejection, and how it intersects with Cart Transform
A technical Shopify developer guide to Discount Functions covering the unified API, product, order, and shipping discounts, selection strategies, combination rules, code rejection, admin configuration, and Cart Transform-safe architecture.
Cart Transform Function deep dive: bundles, line expansion, merge logic, and when it conflicts with discounts
A technical Shopify developer guide to Cart Transform Functions covering lineExpand, linesMerge, lineUpdate, bundle pricing, metafield-driven configuration, discount conflicts, limits, and production design patterns.