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

Page: https://lintelagency.com/resources/shopify-functions/hide-cash-on-delivery-by-region

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

## Facts

- **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:** 2 November 2026

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

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

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

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

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

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

### tests/excluded-province.input.json

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

```json
{
  "operations": [
    { "paymentMethodHide": { "paymentMethodId": "gid://shopify/PaymentCustomizationPaymentMethod/2" } }
  ]
}
```

### tests/excluded-zip-prefix.input.json

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

```json
{
  "operations": [
    { "paymentMethodHide": { "paymentMethodId": "gid://shopify/PaymentCustomizationPaymentMethod/2" } }
  ]
}
```

### tests/no-address-yet.input.json

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

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

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

## Shopify documentation

- [Payment Customization Function API](https://shopify.dev/docs/api/functions/latest/payment-customization)
- [Function limits and app types](https://shopify.dev/docs/api/functions)

## Related

- [Activate a delivery, payment or validation Function](https://lintelagency.com/resources/graphql/register-function-customization)
- [Hide Cash on Delivery above a cart total](https://lintelagency.com/resources/shopify-functions/hide-cash-on-delivery-above-cart-total)
- [Configuring Functions with metafields, so rules change without deploys](https://lintelagency.com/journal/configuring-functions-with-metafields)
- [Shopify Scripts stopped running. Here is where each script’s logic goes now.](https://lintelagency.com/journal/shopify-scripts-to-functions)

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