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
- Create an app with the Shopify CLI (shopify app init), then add the extension: shopify app generate extension --template payment_customization, with JavaScript.
- Replace the generated shopify.extension.toml and the two files in src/ with the files below.
- Run the fixtures with shopify app function run --input tests/excluded-province.input.json (and the others).
- Deploy with shopify app deploy, install the app and create the customization with paymentCustomizationCreate.
- Write the regions to the customization’s $app:function-configuration metafield, for example {"provinces": ["US:AK", "US:HI"], "zipPrefixes": ["969"]}.
Code
shopify.extension.tomlapi_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.graphqlquery 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.
Shopify documentation
- 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.
