Shopify Payment Customization Function: Hide COD

A payment customization function is a Shopify Function that runs during checkout and returns operations against the payment methods a buyer can see: hide one, rename it, move it up the list. You write it against the cart.payment-methods.transform.run target, ship it inside an app, then switch it on with the paymentCustomizationCreate mutation.

Merchants ask for this constantly. Hide Cash on Delivery above 500. Hide bank transfer for anyone shipping outside the UK. Put Shopify Payments at the top of the list for tagged wholesale buyers. Each of those used to mean another app subscription on the invoice.

What a payment customization function can and cannot do

The run target returns an ordered list of operations. Five exist in the 2026-07 schema: paymentMethodHide, paymentMethodRename, paymentMethodMove, paymentTermsSet and orderReviewAdd. The first three are the ones most merchants want. Payment terms need a Plus plan, and the review requirement applies only to B2B checkouts, so a standard store is working with hide, rename and move.

What it cannot do is add anything. If a payment method is not already enabled under Settings > Payments, no function will conjure it into checkout. Renaming has a hole in it too: per the payment customization limitations, “You can’t rename payment methods that have logos as a name, such as Shop Pay, Apple Pay and Google Pay. This also includes all wallets and the Shopify native gift card field.” You can remove those wallets from checkout. You just cannot reorder or relabel them.

One store can have twenty-five payment customization functions active at once.

Functions are also pure. No network calls, no clock, no random numbers. Every fact the logic depends on has to arrive through the input query, and that query is capped at 3000 bytes and a calculated cost of 30.

Which target do you write against?

cart.payment-methods.transform.run, on API version 2025-07 or later. The old name was purchase.payment-customization.run, renamed in Shopify’s standardisation of target and operation names on 23 June 2025. That changelog is explicit that the change only affects functions on 2025-07 and later, so older extensions keep working until you bump the version in shopify.extension.toml. When you do bump, four things change together: the target, the exported function name, the source filename and the name of the input query operation.

Scaffold it with the CLI:

shopify app generate extension --template payment_customization --name payment-customization

The input query lives in src/cart_payment_methods_transform_run.graphql. Ask only for what the logic reads:

query CartPaymentMethodsTransformRunInput {
  cart {
    cost {
      totalAmount {
        amount
      }
    }
  }
  paymentMethods {
    id
    name
  }
  paymentCustomization {
    metafield(namespace: "payment-customization", key: "function-configuration") {
      jsonValue
    }
  }
}

The operation name matters. Codegen derives the JavaScript types from it, which is why the JavaScript flavour uses CartPaymentMethodsTransformRunInput where the Rust flavour of the same query is named Input. Get it wrong and shopify app function typegen hands you types that do not match what you wrote.

How do you write the run function?

The export is the camel-cased target, cartPaymentMethodsTransformRun, in src/cart_payment_methods_transform_run.js:

// @ts-check
const NO_CHANGES = { operations: [] };

export function cartPaymentMethodsTransformRun(input) {
  const config = input.paymentCustomization.metafield?.jsonValue;
  if (!config) {
    return NO_CHANGES;
  }

  const cartTotal = parseFloat(input.cart.cost.totalAmount.amount ?? "0.0");
  if (cartTotal < config.cartTotal) {
    return NO_CHANGES;
  }

  const method = input.paymentMethods.find(
    (paymentMethod) => paymentMethod.name === config.paymentMethodName,
  );
  if (!method) {
    return NO_CHANGES;
  }

  return {
    operations: [{ paymentMethodHide: { paymentMethodId: method.id } }],
  };
}

Two details in there are easy to get wrong. totalAmount.amount is a Decimal, which reaches JavaScript as a string, so it needs parsing before any comparison. And every path that decides to do nothing has to return { operations: [] } rather than falling off the end of the function.

Matching on name is what Shopify’s own tutorial does, because the IDs in the input are PaymentCustomizationPaymentMethod GIDs that appear nowhere else in the Admin API. The cost of that is real: a merchant who renames “Cash on Delivery (COD)” in their payment settings breaks the rule without touching your code. Keep the name in configuration, never in the source.

How do you configure and activate it?

Activation is one Admin GraphQL mutation, and it needs the write_payment_customizations access scope:

mutation CreatePaymentCustomization {
  paymentCustomizationCreate(
    paymentCustomization: {
      title: "Hide Cash on Delivery over 500"
      enabled: true
      functionHandle: "payment-customization"
    }
  ) {
    paymentCustomization {
      id
    }
    userErrors {
      message
    }
  }
}

The customization then shows up for the merchant under Settings > Payments, in the Payment customizations section, where they can toggle it off without touching your app.

Configuration rides on a metafield owned by that customization record, with the namespace and key your input query asked for. Write it with metafieldsSet, using the customization ID as ownerId and a type of json. The $value variable is the configuration document serialised to a string, so {"paymentMethodName": "Cash on Delivery (COD)", "cartTotal": 500} goes in as one JSON string:

mutation SetConfiguration($ownerId: ID!, $value: String!) {
  metafieldsSet(
    metafields: [
      {
        ownerId: $ownerId
        namespace: "payment-customization"
        key: "function-configuration"
        type: "json"
        value: $value
      }
    ]
  ) {
    metafields {
      id
    }
    userErrors {
      message
    }
  }
}

That indirection is what keeps the threshold out of a Wasm bundle. Change the number, and the next checkout picks it up with no build, no deploy and no app version. A settings page in your embedded app writes the same metafield, so the merchant changes the rule themselves. The same pattern shows up in our guide to metaobjects and metafields.

Where does the function not run?

This is the part that costs an afternoon. Nothing throws an error when an operation is ignored.

Plan and geographical restrictions apply to the API. When they bite, the function input still contains every payment method, and the operations you return against a restricted method simply have no effect at checkout. There is no error and nothing in the function log. The payment method stays on the page.

Point of Sale does not run payment customization functions at all. Shop Pay applies no operations except on the native gift card field.

Placements are the other Plus-only cliff. Each payment method carries a placements list, either PAYMENT_METHOD or ACCELERATED_CHECKOUT, and a hide operation can target one of them:

{
  "operations": [
    {
      "paymentMethodHide": {
        "paymentMethodId": "gid://shopify/PaymentCustomizationPaymentMethod/3",
        "placements": ["ACCELERATED_CHECKOUT"]
      }
    }
  ]
}

The docs are blunt about what happens elsewhere: “Only Shopify Plus stores support hiding specific placements. The placements field appears in the function input on all stores, but on non-Plus stores HideOperation ignores it and hides the entire payment method from checkout.” So a rule that means “keep PayPal in the wallet buttons, drop it from the payment list” quietly becomes “drop PayPal” on a standard plan.

While you are in there, check your PayPal handling against the 2026-01 change that made Venmo and PayPal separate payment methods. Hiding PayPal’s accelerated checkout placement no longer takes Venmo with it.

Rust or JavaScript for the function body?

Shopify’s own documentation publishes the instruction cost of its samples. A hide-by-name payment customization measures 23,929 instructions in Rust and 180,589 in JavaScript. The resource limit is 11 million instructions for a cart of up to 200 lines, with 128 kB of input and 20 kB of output.

So the JavaScript build of a function like this uses under two percent of the budget. The loop runs over payment methods, and a checkout has a handful of those, not hundreds.

Write it in JavaScript. Rust earns its place where the function iterates every cart line and does arithmetic on each one, which is the shape of a discount or a cart validation function, not a payment customization. Reach for the native Rust template when your instruction count gets close to the limit, and not before.

What I would not do is build one of these for a single rule that an existing app already covers well, unless the merchant is already paying you to maintain an app. The function itself is an afternoon. The app wrapper around it, the OAuth, the settings UI, the hosting and the upgrade for each quarterly API version, is the real cost, and it recurs.

Whoooop builds Shopify apps and checkout extensions for UK merchants, including the boring parts: the function, the settings page the merchant actually uses, and the API version upgrades afterwards. If you have a checkout rule that a stock app cannot express, our Shopify development work is where that fits.

Need this built properly?

Whoooop Ltd has spent 15+ years building and maintaining web applications in TypeScript, React, Node.js and serverless — the same ground this post covers.

Get in touch