Skip to main content

Payment Customization Functions

A payment_customization function modifies the merchant’s payment-method list before it renders at checkout. Use it to hide a method conditionally (e.g. block COD for high-value orders), rename one for clarity, or pin a preferred method to the top of the radio list. Runs at both cart verification (preview) and order placement (enforcement) so a stale frontend can’t bypass a hide op — the backend rejects the order if a hidden method is submitted.

How It Works

Function Manifest

{
  "handle": "my-payment-app",
  "name": "Payment Customizer",
  "version": "1.0.0",
  "functions": {
    "payment_customization": {
      "handle": "payment-rules",
      "name": "Payment Rules",
      "config": {
        "hideCODAbove": 500
      }
    }
  }
}

Input Schema

interface PaymentCustomizationInput {
  cart: {
    items: CartItem[];
    totalPrice: number;     // Subtotal in display currency
    itemCount: number;
    currency: string;
  };
  paymentMethods: PaymentMethod[];
  shippingAddress: {
    firstName: string;
    lastName: string;
    address1: string;
    address2: string;
    city: string;
    province: string;
    country: string;
    zip: string;
    phone: string;
  };
}

interface PaymentMethod {
  id: string;             // paymentProviderId (UUID)
  name: string;           // Provider key (e.g. "stripe", "razorpay", "cod")
  type: string;           // Same as name in current backend
}

interface CartItem {
  id: string;             // lineItemId
  variantId: string;
  productId: string;
  title: string;
  quantity: number;
  price: number;          // Effective unit price (display currency)
  originalPrice: number;
}
price and totalPrice are in display currency units, not cents. totalPrice: 599.99 means ₹599.99 / $599.99.The CLAUDE input is intentionally minimal — no customer, tags, productType, or vendor is passed. If your rules need that data, look the customer up via your own backend using the customer email from verify-cart payload (which your app receives via the App Bridge).

Output Schema

interface PaymentCustomizationOutput {
  operations: PaymentOperation[];
}

type PaymentOperation =
  | { hide:    { paymentMethod: string } }    // Match by provider key
  | { hide:    { paymentMethodId: string } }  // Match by paymentProviderId
  | { rename:  { paymentMethod: string; name: string } }
  | { rename:  { paymentMethodId: string; name: string } }
  | { move:    { paymentMethod: string; position: number } }
  | { move:    { paymentMethodId: string; position: number } };
Either paymentMethod (the provider key like "cod") or paymentMethodId (the UUID) works for all ops. The backend matches against both.
reorder is accepted as an alias for move — both produce the same result. New code should use move.

Operations

hide

Removes the method from the radio list (preview) and rejects the order if the customer submits it.
{ operations: [{ hide: { paymentMethod: 'cod' } }] }
The order-place error message is: "Payment method 'cod' is not available for this order"

rename

Overrides the displayed name. The underlying provider key is unchanged — charging behaviour is identical, only the radio label is different.
{ operations: [{
  rename: { paymentMethod: 'stripe', name: 'Credit Card (incl. 3-D Secure)' }
}] }
On the frontend the renamed plan carries __renamed: true so the checkout payment list displays methodName instead of the provider’s built-in label.

move

Sets the index of the method in the radio list. Lower positions render first.
{ operations: [{ move: { paymentMethod: 'stripe', position: 0 } }] }

Examples

Hide COD for high-value orders

function customizePayments(input, config) {
  if (input.cart.totalPrice <= (config.hideCODAbove || 500)) {
    return { operations: [] };
  }
  return {
    operations: [{ hide: { paymentMethod: 'cod' } }]
  };
}

Region-based methods

function customizePayments(input) {
  const country = input.shippingAddress?.country;
  const ops = [];

  if (country !== 'IN') ops.push({ hide: { paymentMethod: 'razorpay' } });
  if (!['NL'].includes(country)) ops.push({ hide: { paymentMethod: 'ideal' } });
  return { operations: ops };
}

Rename for clarity, pin preferred

function customizePayments() {
  return {
    operations: [
      { move:    { paymentMethod: 'stripe', position: 0 } },
      { rename:  { paymentMethod: 'stripe', name: 'Credit / debit card' } },
      { rename:  { paymentMethod: 'cod',    name: 'Cash on delivery (₹49 handling)' } }
    ]
  };
}

How it renders in the UI

Payment Method
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

○ Credit / debit card                ← renamed + reordered
○ Cash on delivery (₹49 handling)    ← renamed
                                     ← bank_transfer hidden
The checkout reads:
  • paymentMethods in the verified-cart response — filtered list (after hide)
  • Each plan carries __renamed: true + methodName when a rename op fired.

Best Practices

The backend doesn’t auto-recover — if all methods are hidden, the customer literally can’t pay. Always leave at least one method available for any reachable cart state.
Provider UUIDs differ per store. paymentMethod: 'cod' is portable across installs; paymentMethodId: '<uuid>' is not.
Drive /checkout with Puppeteer and screenshot the payment radio. JSON-only tests don’t catch the __renamed flag wiring; only the rendered label does.
Functions re-run on every cart change. Always derive ops from the current cart + config, not from cached state.

See Also