Hide Cash on Delivery by country, province or ZIP code

A Shopify payment customization Function that hides Cash on Delivery for countries, provinces or ZIP prefixes you list in a metafield. Tested JavaScript.

Function API
Payment Customization
Target
cart.payment-methods.transform.run
Plan
Any plan in a public app; Shopify Plus in a custom app
App scope
write_payment_customizations
API version
2026-10
Last checked

In short

This payment customization Function hides Cash on Delivery when the shipping address is in a country, province or ZIP code range you exclude. The lists live in a JSON metafield, so you can change them without a deploy. It uses the cart.payment-methods.transform.run target of the Payment Customization API, version 2026-10, and works on any plan when shipped in a public app.

How to use it

  1. Create an app with the Shopify CLI (shopify app init), then add the extension: shopify app generate extension --template payment_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/excluded-province.input.json (and the others).
  4. Deploy with shopify app deploy, install the app and create the customization with paymentCustomizationCreate.
  5. Write the regions to the customization’s $app:function-configuration metafield, for example {"provinces": ["US:AK", "US:HI"], "zipPrefixes": ["969"]}.

Code

shopify.extension.toml
api_version = "2026-10"

[[extensions]]
name = "Hide Cash on Delivery by region"
handle = "hide-cod-by-region"
type = "function"

  [[extensions.targeting]]
  target = "cart.payment-methods.transform.run"
  input_query = "src/cart_payment_methods_transform_run.graphql"
  export = "cart-payment-methods-transform-run"

  [extensions.build]
  command = ""
  path = "dist/function.wasm"
src/cart_payment_methods_transform_run.graphql
query CartPaymentMethodsTransformRunInput {
  cart {
    deliveryGroups {
      deliveryAddress {
        countryCode
        provinceCode
        zip
      }
    }
  }
  paymentMethods {
    id
    name
  }
  paymentCustomization {
    metafield(namespace: "$app", key: "function-configuration") {
      jsonValue
    }
  }
}
src/cart_payment_methods_transform_run.js
// @ts-check

/**
 * @typedef {import("../generated/api").CartPaymentMethodsTransformRunInput} CartPaymentMethodsTransformRunInput
 * @typedef {import("../generated/api").CartPaymentMethodsTransformRunResult} CartPaymentMethodsTransformRunResult
 */

/**
 * Settings stored as JSON in the customization's metafield. The region lists
 * are empty by default, so nothing is hidden until the merchant sets them.
 */
const DEFAULT_CONFIG = {
  // Matched case-insensitively against the start of the payment method name.
  methodNames: ["Cash on Delivery", "Kapıda Ödeme"],
  // ISO country codes, e.g. "US".
  countries: [],
  // Country and province code joined by a colon, e.g. "US:AK" or "CA:NU".
  provinces: [],
  // ZIP or postal code prefixes, e.g. "99" or "34450". Spaces are ignored.
  zipPrefixes: [],
};

/**
 * Lowercase for matching, with Turkish dotless/dotted i folded to "i", so
 * "KAPIDA ÖDEME" and "Kapıda Ödeme" match the same configured name.
 * @param {string} text
 */
const fold = (text) => text.toLowerCase().replace(/ı/g, "i").replace(/\u0307/g, "");

/** @type {CartPaymentMethodsTransformRunResult} */
const NO_CHANGES = { operations: [] };

/** @param {string | null | undefined} value */
const normalizeZip = (value) => (value ?? "").replace(/\s+/g, "").toUpperCase();

/**
 * @param {CartPaymentMethodsTransformRunInput} input
 * @returns {CartPaymentMethodsTransformRunResult}
 */
export function cartPaymentMethodsTransformRun(input) {
  const config = { ...DEFAULT_CONFIG, ...input.paymentCustomization?.metafield?.jsonValue };
  const countries = config.countries.map((code) => code.toUpperCase());
  const provinces = config.provinces.map((code) => code.toUpperCase());
  const zipPrefixes = config.zipPrefixes.map(normalizeZip).filter(Boolean);

  // A cart can ship to several addresses (split delivery); one excluded
  // address is enough, because COD applies to the whole order.
  const addresses = input.cart.deliveryGroups
    .map((group) => group.deliveryAddress)
    .filter((address) => address != null);
  const excluded = addresses.some((address) => {
    const country = (address.countryCode ?? "").toUpperCase();
    const province = `${country}:${(address.provinceCode ?? "").toUpperCase()}`;
    const zip = normalizeZip(address.zip);
    return (
      countries.includes(country) ||
      provinces.includes(province) ||
      (zip !== "" && zipPrefixes.some((prefix) => zip.startsWith(prefix)))
    );
  });
  if (!excluded) return NO_CHANGES;

  const names = config.methodNames.map(fold);
  const operations = input.paymentMethods
    .filter((method) => names.some((name) => fold(method.name).startsWith(name)))
    .map((method) => ({ paymentMethodHide: { paymentMethodId: method.id } }));

  return { operations };
}
Test input and expected output
tests/allowed-region.input.json
{
  "cart": {
    "deliveryGroups": [
      { "deliveryAddress": { "countryCode": "US", "provinceCode": "NY", "zip": "10001" } }
    ]
  },
  "paymentMethods": [
    { "id": "gid://shopify/PaymentCustomizationPaymentMethod/1", "name": "Credit card" },
    { "id": "gid://shopify/PaymentCustomizationPaymentMethod/2", "name": "Cash on Delivery (COD)" }
  ],
  "paymentCustomization": {
    "metafield": {
      "jsonValue": { "countries": ["IS"], "provinces": ["US:AK", "US:HI"], "zipPrefixes": ["969"] }
    }
  }
}
tests/allowed-region.output.json
{ "operations": [] }
tests/excluded-province.input.json
{
  "cart": {
    "deliveryGroups": [
      { "deliveryAddress": { "countryCode": "US", "provinceCode": "AK", "zip": "99501" } }
    ]
  },
  "paymentMethods": [
    { "id": "gid://shopify/PaymentCustomizationPaymentMethod/1", "name": "Credit card" },
    { "id": "gid://shopify/PaymentCustomizationPaymentMethod/2", "name": "Cash on Delivery (COD)" }
  ],
  "paymentCustomization": {
    "metafield": {
      "jsonValue": { "countries": ["IS"], "provinces": ["US:AK", "US:HI"], "zipPrefixes": ["969"] }
    }
  }
}
tests/excluded-province.output.json
{
  "operations": [
    { "paymentMethodHide": { "paymentMethodId": "gid://shopify/PaymentCustomizationPaymentMethod/2" } }
  ]
}
tests/excluded-zip-prefix.input.json
{
  "cart": {
    "deliveryGroups": [
      { "deliveryAddress": { "countryCode": "US", "provinceCode": "GU", "zip": "96910" } }
    ]
  },
  "paymentMethods": [
    { "id": "gid://shopify/PaymentCustomizationPaymentMethod/1", "name": "Credit card" },
    { "id": "gid://shopify/PaymentCustomizationPaymentMethod/2", "name": "Cash on Delivery (COD)" }
  ],
  "paymentCustomization": {
    "metafield": {
      "jsonValue": { "countries": ["IS"], "provinces": ["US:AK", "US:HI"], "zipPrefixes": ["969"] }
    }
  }
}
tests/excluded-zip-prefix.output.json
{
  "operations": [
    { "paymentMethodHide": { "paymentMethodId": "gid://shopify/PaymentCustomizationPaymentMethod/2" } }
  ]
}
tests/no-address-yet.input.json
{
  "cart": { "deliveryGroups": [{ "deliveryAddress": null }] },
  "paymentMethods": [
    { "id": "gid://shopify/PaymentCustomizationPaymentMethod/1", "name": "Credit card" },
    { "id": "gid://shopify/PaymentCustomizationPaymentMethod/2", "name": "Cash on Delivery (COD)" }
  ],
  "paymentCustomization": {
    "metafield": {
      "jsonValue": { "countries": ["IS"], "provinces": ["US:AK", "US:HI"], "zipPrefixes": ["969"] }
    }
  }
}
tests/no-address-yet.output.json
{ "operations": [] }

Written by Lintel. MIT license: free to use in your own store and in client work.

Limits and edge cases

  • Nothing is hidden until the customer has entered a shipping address; before that the delivery groups have no address to check.
  • Province codes are the ones Shopify stores on the address (provinceCode). Place a test order to a region you want to exclude and copy the code from the order before adding it.
  • ZIP matching is by prefix with spaces removed, so "969" covers every code that starts with 969. Use full codes when a prefix is too broad.
  • When the cart ships to several addresses, one excluded address hides COD for the whole order.

Questions

How do I disable Cash on Delivery for certain postcodes in Shopify?
Shopify’s payment settings can’t filter by postcode. A payment customization Function like this one reads the shipping address and hides the COD method when the ZIP code starts with a prefix you list.
Can I hide COD for one province or state only?
Yes. Add the country and province code to the provinces list, for example "US:AK". Other provinces of the same country keep Cash on Delivery.
Why does COD still show before the customer enters an address?
The Function only knows the region once there is a shipping address. Checkout runs it again after the address is entered, and COD disappears then.
Can I combine this with a cart total limit?
Yes. Install the cart total example as a second payment customization, or add the total check from it to this Function. A store can run up to 25 payment customizations.
  • Written for Function API version 2026-10 and checked against Shopify’s reference on the date shown. Shopify releases a new version every quarter.
  • Custom apps that contain Functions only run on Shopify Plus stores; a public app with the same Function runs on any plan unless the page says otherwise.
  • Every example ships with test fixtures, and we run them before publishing. Run them again after you change the code: shopify app function run.
  • MIT license. Use the code in your own store or in client work.

Need this built, tested and maintained for your store? See Shopify Functions development.

Have something to build? Let’s talk it through.

Taking a limited number of projects at a time. Every brief is read by an engineer, not a salesperson.

Gencer KarakayaShopify Engineer

An engineer reads your brief, and we reply by email.