The security collapse of frontend payment gating
The persistent mistake in B2B payment gating is treating payment methods as visual components that can be toggled with CSS or JavaScript. In legacy themes, developers inserted Liquid snippets that checked customer.tags contains 'wholesale' and rendered a hidden input hiding the credit card form. When checkout moved to checkout extensibility, teams adapted this by authoring Checkout UI Extensions to unmount payment options.
This approach fails fundamentally. As documented in our analysis of checkout custom calculation extensions, UI extensions run in an isolated worker with strict render budgets and zero access to the checkout DOM. A Checkout UI Extension cannot intercept, suppress, or modify the native payment gateway radio list rendered by Shopify's core checkout pipeline.
Furthermore, cart attributes and storefront customer state are untrusted inputs. When developers write rules that read cart.attributes._tier == 'trade', any automated script or retail shopper can append ?attributes[_tier]=trade to a cart permalink or trigger a fetch call to the Storefront API. Gating Net 30 terms behind client-provided data exposes the store to fraudulent order placement where inventory is committed without credit authorization.
True payment gating requires server-side authority. The platform must inspect the verified session identity and evaluate business constraints in a closed loop before generating the available payment methods list.
The Payment Customization API contract
The Payment Customization Function API operates on the purchase.payment-customization.run extension target. Unlike checkout validation functions which emit blocking error messages that stop the buyer from completing checkout, a payment customization function transforms the list of payment methods presented to the customer.
The function receives a PaymentCustomizationRunInput query containing the checkout cart total, buyer identity, cart lines, and the complete array of active payment methods configured in the Shopify Admin. In return, the function emits declarative operations: hide (removes a gateway by ID), rename (modifies user-facing labels with company account details), and move (reorders methods to place approved corporate terms at index 0).
export interface PaymentCustomizationPaymentMethod {
id: string; // e.g. "gid://shopify/PaymentCustomizationPaymentMethod/1"
name: string;
}
export interface PaymentCustomizationRunInput {
cart: {
cost: {
subtotalAmount: { amount: string; currencyCode: string };
totalAmount: { amount: string; currencyCode: string };
};
buyerIdentity?: {
isB2BAuthenticated?: boolean;
purchasingCompany?: { id: string; name: string };
purchasingCompanyLocation?: {
id: string;
paymentTerms?: { type: string };
creditLimit?: { amount: string; currencyCode: string };
};
};
lines: Array<{
merchandise: {
productType?: string;
isHazardousFreight?: boolean;
};
}>;
};
paymentMethods: PaymentCustomizationPaymentMethod[];
paymentCustomization: {
metafield?: { value: string };
};
}
export function paymentCustomizationRun(input: PaymentCustomizationRunInput) {
const operations: Array<
| { hide: { paymentMethodId: string } }
| { rename: { paymentMethodId: string; name: string } }
| { move: { paymentMethodId: string; index: number } }
> = [];
const buyer = input.cart.buyerIdentity;
const isB2B = Boolean(buyer?.isB2BAuthenticated && buyer.purchasingCompany);
const location = buyer?.purchasingCompanyLocation;
const subtotal = parseFloat(input.cart.cost.subtotalAmount.amount);
const total = parseFloat(input.cart.cost.totalAmount.amount);
const creditLimit = location?.creditLimit ? parseFloat(location.creditLimit.amount) : Infinity;
// Determine if cart carries oversized freight or hazardous gear
const hasFreight = input.cart.lines.some(l =>
l.merchandise.isHazardousFreight ||
(l.merchandise.productType ?? "").toLowerCase().includes("freight")
);
for (const method of input.paymentMethods) {
const isNetTerms = method.name.toLowerCase().includes("net 30");
const isCOD = method.name.toLowerCase().includes("cash on delivery");
// Rule 1: Gate Net 30 to authenticated B2B companies with approved terms
if (isNetTerms) {
if (!isB2B || location?.paymentTerms?.type === "DUE_ON_RECEIPT") {
operations.push({ hide: { paymentMethodId: method.id } });
continue;
}
// Rule 2: Minimum order subtotal floor (£500)
if (subtotal < 500) {
operations.push({ hide: { paymentMethodId: method.id } });
continue;
}
// Rule 3: Credit limit enforcement
if (total > creditLimit) {
operations.push({ hide: { paymentMethodId: method.id } });
continue;
}
// Approved B2B buyer: Rename to reflect trading account and move to top
operations.push({
rename: {
paymentMethodId: method.id,
name: `${method.name} — Approved Account (${buyer.purchasingCompany.name})`,
},
});
operations.push({ move: { paymentMethodId: method.id, index: 0 } });
}
// Rule 4: Prohibit Cash on Delivery for freight shipments
if (isCOD && hasFreight) {
operations.push({ hide: { paymentMethodId: method.id } });
}
}
return { operations };
}Evaluating authenticated B2B company identity over cart attributes
On Shopify Plus B2B stores, company accounts operate through the purchasingCompany and purchasingCompanyLocation data graph. When a B2B representative logs into the store using new customer accounts (passwordless email authentication), Shopify binds their session to an approved company entity.
In our build for Forged 4x4, custom commercial accounts required tailored payment options based on pre-negotiated wholesale contracts. Trade customers with Net 30 payment terms needed to settle orders without credit card processing fees, whereas provisional trade accounts were placed on DUE_ON_RECEIPT terms until credit insurance cleared.
The Payment Customization Function queries buyerIdentity.purchasingCompanyLocation.paymentTerms.type directly from the platform. If a customer is logged in as an individual or guest, buyerIdentity.purchasingCompany is null. The function immediately hides the Net 30 gateway. No amount of client-side cookie manipulation or URL query injection can synthesise an authenticated purchasing company record.
Furthermore, company locations specify discrete payment terms: one branch of a national fleet operator may have authorized invoice terms, while a regional depot must pay upon ordering. Gating against the location object guarantees that payment terms adhere to corporate credit governance rather than individual staff logins.
Compound rules: credit limits, order minimums, and hazardous freight
Real-world commerce requires compound business logic where payment method availability depends on order valuation and physical logistics simultaneously. Single-variable gating invariably produces edge-case leakages.
Extending trade terms carries an administrative processing overhead. Generating invoices, running credit collections, and matching remittance advices costs money. Merchants typically establish an order threshold—such as £500—below which wholesale buyers must check out via standard corporate credit cards. The function compares cart.cost.subtotalAmount.amount against configured thresholds before exposing deferred terms.
Similarly, when an ERP integration synchronises customer credit limits into Shopify company location metafields, the Payment Customization Function reads creditLimit.amount. If the current checkout grand total exceeds the buyer's remaining credit allowance, the function hides Net terms and leaves credit cards or immediate wire transfer as the only selectable options. This stops B2B buyers from exceeding credit lines during unmonitored weekend ordering surges.
Physical product characteristics also dictate payment constraints. Cash on Delivery (COD) or localized post-pay methods are unsuitable for freight-forwarded pallets, custom CNC-machined chassis components, or hazardous chemicals. By inspecting cart.lines[].merchandise.productType or line-item metafields, the function dynamically removes high-risk payment methods when heavy freight lines are present in checkout.
Deterministic execution in the WebAssembly sandbox
Payment Customization Functions execute within Shopify's WebAssembly sandbox. This environment imposes rigorous performance and architectural boundaries:
The WebAssembly binary must parse the input, evaluate all conditional rules, and emit the operations array in under five milliseconds. Any function that exceeds execution budgets is terminated by the host system. Furthermore, memory allocation is constrained to a 256KB footprint, prohibiting large in-memory caches or heavy external parsing libraries.
Most critically, Functions have zero network access. They cannot perform asynchronous HTTP fetches to third-party credit check APIs or external ERPs during checkout. All data required to make the gating decision must already exist in Shopify's data store—either on the cart, the customer identity, or in the function's configuration metafield. Real-time credit balances must be synchronised asynchronously into Shopify B2B company metafields via background webhooks from your ERP, rather than queried synchronously at the moment of payment.
What Payment Customization Functions cannot do
Understanding platform constraints prevents architectural dead-ends during checkout design:
Cannot inject new payment gateways: A Payment Customization Function can only hide, rename, or reorder payment methods that have already been activated in your Shopify Admin (Settings > Payments). It cannot synthesise an arbitrary gateway out of thin air.
Cannot manipulate credit card processing logic: Functions have no access to card numbers, tokenized gateway responses, or 3D Secure verification flows. They control visibility and sequencing; payment settlement remains strictly handled by Shopify Payments or your configured gateway.
Maximum of 5 active functions: Shopify permits up to five active payment customization functions per store. Logic should be consolidated into a single cohesive function to prevent rule collisions.
Exclusion from backend Admin orders: Functions govern online storefront checkouts. When customer service agents create draft orders manually in the Shopify Admin, admin payment processing bypasses the storefront function pipeline unless invoice checkout links are sent to the customer.
When this needs an engineer
You do not need a custom payment function if your store uses basic Shopify Payments and you simply want to turn off Cash on Delivery globally in your store settings. The standard Shopify Admin provides native switches for enabling and disabling payment providers across entire markets.
You need an engineer when payment method availability must be governed by contractual business rules: gating Net 30 or bank draft options to verified B2B companies, preventing wholesale accounts from invoicing sub-threshold baskets, automatically hiding post-pay options for heavy freight items, and reordering payment methods to streamline corporate procurement workflows.
Explore our custom Shopify applications collection to examine production Checkout Extensibility architectures, or discover how our Shopify Plus development team builds secure checkout pipelines. Send us the store and the symptom.
