# Hide shipping rates by product tag

> A Shopify delivery customization Function that hides chosen shipping options when the cart has a product with a given tag, such as oversized or hazmat.

Page: https://lintelagency.com/resources/shopify-functions/hide-shipping-rates-by-product-tag

## In short

This delivery customization Function hides the shipping options you name when a shipment contains a product with one of your tags, such as oversized or hazmat. Tags and option names live in one JSON metafield that also feeds an input query variable. It uses the cart.delivery-options.transform.run target of the Delivery Customization API, version 2026-10.

## Facts

- **Function API:** Delivery Customization
- **Target:** cart.delivery-options.transform.run
- **Plan:** Any plan in a public app; Shopify Plus in a custom app
- **App scope:** write_delivery_customizations
- **API version:** 2026-10
- **Last checked:** 2 November 2026

## How to use it

1. Create an app with Shopify CLI 4.9.0 or later (needed for target-level input_variables), then add the extension: shopify app generate extension --template delivery_customization, with JavaScript.
2. Replace the generated shopify.extension.toml and the two files in src/ with the files below.
3. Run the fixtures with shopify app function run --input tests/tagged-product.input.json.
4. Deploy with shopify app deploy and create the customization with deliveryCustomizationCreate.
5. Set the $app:function-configuration metafield, for example {"productTags": ["oversized"], "optionTitles": ["Express", "Overnight"]}.

## Code

### shopify.extension.toml

```toml
api_version = "2026-10"

[[extensions]]
name = "Hide shipping rates by product tag"
handle = "hide-rates-by-product-tag"
type = "function"

  [[extensions.targeting]]
  target = "cart.delivery-options.transform.run"
  input_query = "src/cart_delivery_options_transform_run.graphql"
  input_variables = { namespace = "$app", key = "function-configuration" }
  export = "cart-delivery-options-transform-run"

  [extensions.build]
  command = ""
  path = "dist/function.wasm"
```

### src/cart_delivery_options_transform_run.graphql

```graphql
query CartDeliveryOptionsTransformRunInput($productTags: [String!]) {
  cart {
    deliveryGroups {
      cartLines {
        merchandise {
          __typename
          ... on ProductVariant {
            product {
              hasAnyTag(tags: $productTags)
            }
          }
        }
      }
      deliveryOptions {
        handle
        title
      }
    }
  }
  deliveryCustomization {
    metafield(namespace: "$app", key: "function-configuration") {
      jsonValue
    }
  }
}
```

### src/cart_delivery_options_transform_run.js

```js
// @ts-check

/**
 * @typedef {import("../generated/api").CartDeliveryOptionsTransformRunInput} CartDeliveryOptionsTransformRunInput
 * @typedef {import("../generated/api").CartDeliveryOptionsTransformRunResult} CartDeliveryOptionsTransformRunResult
 */

/**
 * Settings stored as JSON in the customization's metafield. The same
 * metafield feeds the query's $productTags variable (see input_variables in
 * shopify.extension.toml), so tags are checked by Shopify, not in this code.
 */
const DEFAULT_CONFIG = {
  // Matched case-insensitively anywhere in the delivery option title.
  optionTitles: /** @type {string[]} */ ([]),
};

/**
 * @param {CartDeliveryOptionsTransformRunInput} input
 * @returns {CartDeliveryOptionsTransformRunResult}
 */
export function cartDeliveryOptionsTransformRun(input) {
  const config = { ...DEFAULT_CONFIG, ...input.deliveryCustomization?.metafield?.jsonValue };
  const titles = config.optionTitles.map((title) => title.toLowerCase());

  // Only the shipment that contains a tagged product loses the options; other
  // delivery groups in a split cart keep theirs.
  const operations = input.cart.deliveryGroups
    .filter((group) =>
      group.cartLines.some(
        (line) => line.merchandise.__typename === "ProductVariant" && line.merchandise.product.hasAnyTag,
      ),
    )
    .flatMap((group) => group.deliveryOptions)
    .filter((option) => titles.some((title) => (option.title ?? "").toLowerCase().includes(title)))
    .map((option) => ({ deliveryOptionHide: { deliveryOptionHandle: option.handle } }));

  return { operations };
}
```

## Test input and expected output

### tests/no-tagged-product.input.json

```json
{
  "cart": {
    "deliveryGroups": [
      {
        "cartLines": [
          { "merchandise": { "__typename": "ProductVariant", "product": { "hasAnyTag": false } } }
        ],
        "deliveryOptions": [
          { "handle": "9f8e-ground", "title": "Ground" },
          { "handle": "9f8e-express", "title": "Express" },
          { "handle": "9f8e-overnight", "title": "Overnight" }
        ]
      }
    ]
  },
  "deliveryCustomization": {
    "metafield": { "jsonValue": { "productTags": ["oversized", "hazmat"], "optionTitles": ["Express", "Overnight"] } }
  }
}
```

### tests/no-tagged-product.output.json

```json
{ "operations": [] }
```

### tests/tagged-product.input.json

```json
{
  "cart": {
    "deliveryGroups": [
      {
        "cartLines": [
          { "merchandise": { "__typename": "ProductVariant", "product": { "hasAnyTag": false } } },
          { "merchandise": { "__typename": "ProductVariant", "product": { "hasAnyTag": true } } }
        ],
        "deliveryOptions": [
          { "handle": "9f8e-ground", "title": "Ground" },
          { "handle": "9f8e-express", "title": "Express" },
          { "handle": "9f8e-overnight", "title": "Overnight" }
        ]
      }
    ]
  },
  "deliveryCustomization": {
    "metafield": { "jsonValue": { "productTags": ["oversized", "hazmat"], "optionTitles": ["Express", "Overnight"] } }
  }
}
```

### tests/tagged-product.output.json

```json
{
  "operations": [
    { "deliveryOptionHide": { "deliveryOptionHandle": "9f8e-express" } },
    { "deliveryOptionHide": { "deliveryOptionHandle": "9f8e-overnight" } }
  ]
}
```

## Limits and edge cases

- productTags is read twice: Shopify uses it as the $productTags query variable, and the code reads optionTitles from the same JSON.
- If the metafield isn’t set, the variable is null and no option is hidden.
- A list variable can hold at most 100 tags, and the metafield must stay under 10,000 bytes or Shopify won’t return it.
- Option titles are matched anywhere in the title, ignoring case, so "Express" also matches "UPS Express Saver".

## Questions

### How do I hide a shipping method for certain products in Shopify?

Tag the products, then use a delivery customization Function that checks the tag with hasAnyTag and hides the option, as this example does.

### Why use input query variables instead of writing the tags in the query?

Tags written in the query need a new deploy to change and are the same for every store. With a variable, each customization reads its tags from its own metafield.

### Can I hide pickup instead of a carrier rate?

Yes. Put the pickup option’s title in optionTitles. To be stricter, add deliveryMethodType to the query and match PICK_UP.

## Shopify documentation

- [Delivery Customization Function API](https://shopify.dev/docs/api/functions/latest/delivery-customization)
- [Variables in input queries](https://shopify.dev/docs/apps/build/functions/input-queries/use-variables-input-queries)

## Related

- [Activate a delivery, payment or validation Function](https://lintelagency.com/resources/graphql/register-function-customization)
- [Hide express shipping for heavy carts](https://lintelagency.com/resources/shopify-functions/hide-express-shipping-for-heavy-carts)
- [Delivery customization: hide, rename and sort shipping options](https://lintelagency.com/journal/shopify-delivery-customization-function)
- [Configuring Pickup-Only Checkout Options in Shopify](https://lintelagency.com/journal/configuring-pickup-only-checkout-options-in-shopify)

License: MIT. Free to use in your own store and in client work.
