Introduction
Your bank partner offers co-marketing funds. Your payment processor runs a cashback promotion. Your regional bank wants to drive card usage with an exclusive discount for their cardholders. The deal is simple: customers who pay with [Bank X] card get 10% off.
The implementation, however, is not.
Shopify's discount system is powerful — but it is fundamentally designed around what a customer buys, who they are, and when they buy. It is not natively designed around how they pay. The payment method a customer selects is only known at the final stage of checkout — after cart contents, discounts, and pricing have already been calculated.
This creates a fundamental timing problem: by the time Shopify knows the customer is paying with a specific bank card, the discount system has already run.
This guide explains exactly why this is hard, what the platform actually supports, every available workaround, and how to build a robust bank-specific discount system using Shopify Functions — the only approach that comes close to a native solution.
Who This Guide Is For
- Merchants with bank co-marketing partnerships requiring card-specific discounts
- Developers building payment-method-aware checkout experiences
- Merchants running buy-now-pay-later or specific processor promotions
- Technical teams evaluating Shopify Functions for payment discount logic
- Anyone who has tried to use discount codes tied to payment methods and hit a wall
Why This Is Architecturally Difficult
Understanding the constraint makes the solutions clearer.
Shopify's Checkout Sequence
1. Customer adds items to cart
2. Customer proceeds to checkout
3. Customer enters contact info
4. Customer enters shipping address
5. Shopify calculates shipping rates
6. Discount codes applied / automatic discounts calculated ← discounts run here
7. Customer selects payment method ← payment known here
8. Customer completes purchase
The gap between steps 6 and 7 is the core problem. Shopify's discount engine runs before the payment method is selected. This means:
- You cannot natively condition a discount on payment method within Shopify's standard discount system
- Automatic discounts cannot query "what card is the customer using"
- Discount codes can be applied regardless of payment method — there's no native payment-method gate
Additionally, Shopify never exposes the specific bank or card issuer to the merchant during checkout — only the payment processor and card type (Visa, Mastercard) in some contexts, and only after the order is placed.
What Shopify Natively Supports (And What It Doesn't)
Natively supported discount conditions:
| Condition | Supported? |
|---|---|
| Minimum order value | ✅ Yes |
| Specific products or collections | ✅ Yes |
| Customer segments / tags | ✅ Yes |
| Discount code (customer enters manually) | ✅ Yes |
| Specific payment method | ❌ No |
| Specific bank or card issuer | ❌ No |
| BIN range (first 6 digits of card) | ❌ No (natively) |
| Specific payment gateway | ❌ No (natively) |
Approach 1 — Manual Discount Code Distribution (Simplest)
Complexity: ⭐ Easy Real payment gating: No — honor system Cost: Free Best for: Small promotions, trusted customer bases, bank-distributed codes
The simplest approach bypasses the technical problem entirely: the bank distributes a unique discount code directly to their cardholders through their own channels (banking app, email, SMS), and customers enter it manually at checkout.
How it works:
- Create a discount code in Shopify Admin → Discounts → Create discount
- Set the discount type (percentage, fixed amount)
- Share the code exclusively with the bank partner
- The bank distributes the code to eligible cardholders via their own app/email
- Customers apply it at checkout regardless of payment method
Pros:
- Zero development required
- Works immediately
- Fully supported by Shopify's native discount system
- Bank handles eligibility verification on their end
Cons:
- Code can be shared outside the intended audience — no technical enforcement
- Customers can apply the code without using the bank's card
- No fraud prevention at the Shopify level
- Discount stacking may be an issue if not configured correctly
Limiting abuse:
In Shopify discount settings:
→ Set "Usage limits" → Limit number of times this discount can be used total
→ Set "Limit to one use per customer"
→ Set expiry date matching the promotion period
→ Consider a unique code per customer (requires bulk code generation)
Bulk unique codes for each cardholder:
- Go to Discounts → Create discount → Discount code
- Scroll to "Generate codes automatically" (or import via CSV)
- Generate as many unique codes as cardholders in the promotion
- Export the codes as CSV
- Provide the CSV to your bank partner for one-to-one distribution
This gives each cardholder a single-use code — significantly reducing misuse.
Approach 2 — Customer Tag Gating via Bank Authentication
Complexity: ⭐⭐⭐ Intermediate Real payment gating: Partial (based on verified bank account, not payment method at checkout) Cost: Development cost + bank API access Best for: Banks with customer verification APIs, loyalty integrations
Instead of gating on the payment method at checkout, gate on verified bank account ownership before checkout. When a customer proves they hold an account with the partner bank, tag their Shopify customer account — then use Shopify's native customer segment discounts to give tagged customers the discount automatically.
Architecture:
Customer clicks "Verify my [Bank] account"
↓
Redirect to bank's OAuth / verification portal
↓
Bank confirms customer is a valid cardholder
↓
Your app receives confirmation callback
↓
Shopify API: Add tag "bank-partner-verified" to customer
↓
Shopify automatic discount: Apply 10% off for customers tagged "bank-partner-verified"
↓
Customer checks out — discount applies automatically
Step 1 — Create the automatic discount:
- Go to Discounts → Create discount → Automatic discount
- Set discount value (e.g., 10% off)
- Under Customer eligibility → select Specific customer segments
- Create or select a segment for tagged customers
Customer segment query:
customer_tags CONTAINS 'bank-partner-verified'
Step 2 — Build the verification flow:
// After bank OAuth callback confirms cardholder status
async function verifyAndTagCustomer(shopifyCustomerId, bankVerificationToken) {
// Verify the bank token is legitimate
const bankVerification = await verifyWithBankAPI(bankVerificationToken);
if (!bankVerification.isValidCardholder) {
return { success: false, reason: 'Not a valid cardholder' };
}
// Check if already tagged (avoid duplicate API calls)
const customer = await shopifyGraphQL(`
{
customer(id: "gid://shopify/Customer/${shopifyCustomerId}") {
tags
}
}
`);
const currentTags = customer.data.customer.tags;
if (currentTags.includes('bank-partner-verified')) {
return { success: true, alreadyVerified: true };
}
// Add verification tag via Admin API
await shopifyGraphQL(`
mutation {
customerUpdate(input: {
id: "gid://shopify/Customer/${shopifyCustomerId}",
tags: ${JSON.stringify([...currentTags, 'bank-partner-verified'])}
}) {
customer { id tags }
userErrors { field message }
}
}
`);
// Log verification event for audit
await logVerificationEvent({
customerId: shopifyCustomerId,
verifiedAt: new Date().toISOString(),
bankReference: bankVerification.referenceId
});
return { success: true, alreadyVerified: false };
}
Step 3 — Handle tag expiry for time-limited promotions:
// Schedule tag removal when promotion ends
async function scheduleTagExpiry(customerId, expiryDate) {
await db.scheduledJobs.insert({
type: 'REMOVE_CUSTOMER_TAG',
customerId,
tag: 'bank-partner-verified',
scheduledFor: expiryDate
});
}
// Cron job that runs daily
async function processTagExpiries() {
const expiredJobs = await db.scheduledJobs.findDue('REMOVE_CUSTOMER_TAG');
for (const job of expiredJobs) {
const customer = await getCustomer(job.customerId);
const updatedTags = customer.tags.filter(t => t !== job.tag);
await shopifyGraphQL(`
mutation {
customerUpdate(input: {
id: "gid://shopify/Customer/${job.customerId}",
tags: ${JSON.stringify(updatedTags)}
}) {
customer { id }
userErrors { field message }
}
}
`);
}
}
Pros:
- Proper identity verification — discount only goes to real bank customers
- Works with Shopify's native automatic discount system
- No checkout extension required
- Discount applies automatically — no code for customer to remember
Cons:
- Requires bank to have an OAuth or verification API you can integrate with
- Customer must verify before shopping (adds friction)
- Does not technically enforce payment method — verified customer could pay with any card
Approach 3 — Shopify Functions: Payment Customization Discount
Complexity: ⭐⭐⭐⭐ Advanced Real payment gating: Closest to native enforcement Cost: Shopify Plus required + development cost Best for: Enterprise merchants, bank partnership programs, high-volume promotions
Shopify Functions allow you to extend checkout logic with custom code that runs on Shopify's infrastructure. The Payment Customization function type lets you modify payment options at checkout — and when combined with a Cart Transform or Discount function, you can build the closest thing to a true payment-method-gated discount.
Important limitation upfront: Even with Shopify Functions, you cannot read the customer's card BIN number or bank issuer during checkout. What you CAN do is:
- Hide or reorder payment methods based on cart attributes
- Apply discounts when customers select a specific payment gateway (e.g., a dedicated gateway for the bank's card)
- Use cart attributes set by a pre-checkout verification flow to gate discounts
Architecture: Gateway-Based Discount Function
If the bank has its own payment gateway on Shopify (e.g., a dedicated "Bank X Pay" gateway), you can apply a discount when that gateway is selected:
Customer selects "Bank X Pay" at checkout
↓
Shopify Functions: Discount function evaluates cart
↓
Cart attribute "payment_gateway_selected" = "bank_x_pay" ← set via checkout UI extension
↓
Discount function applies 10% off
↓
Order total updated before payment confirmation
Step 1 — Create a Checkout UI Extension to Detect Payment Selection
// extensions/payment-detector/src/index.jsx
import { useEffect } from 'react';
import {
reactExtension,
useApplyCartLinesChange,
useCartLineTarget,
usePaymentMethodTarget,
useApplyAttributeChange,
useExtensionCapability
} from '@shopify/ui-extensions-react/checkout';
export default reactExtension(
'purchase.checkout.payment-method-list.render-after',
() => <PaymentMethodDetector />
);
function PaymentMethodDetector() {
const applyAttributeChange = useApplyAttributeChange();
// Listen for payment method selection changes
useEffect(() => {
// This extension renders after the payment method list
// Detect when "Bank X Pay" gateway is visible/selected
const paymentInputs = document.querySelectorAll(
'[data-payment-method-name]'
);
paymentInputs.forEach(input => {
input.addEventListener('change', async (e) => {
if (e.target.checked) {
const methodName = e.target.dataset.paymentMethodName;
// Set cart attribute when bank payment selected
await applyAttributeChange({
type: 'updateAttribute',
key: 'selected_payment_gateway',
value: methodName
});
}
});
});
}, []);
return null; // No visible UI — just event listening
}
Step 2 — Create the Discount Function
// extensions/bank-payment-discount/src/index.js
// Shopify Functions run on Shopify's edge — not your server
export function run(input) {
const cartAttributes = input.cart.attribute;
const lines = input.cart.lines;
// Read the selected payment gateway from cart attributes
const selectedGateway = cartAttributes.find(
attr => attr.key === 'selected_payment_gateway'
)?.value ?? '';
// Only apply discount for the bank partner gateway
const BANK_GATEWAY_NAME = 'Bank X Pay'; // Must match exactly
if (selectedGateway !== BANK_GATEWAY_NAME) {
return { discounts: [], discountApplicationStrategy: 'FIRST' };
}
// Apply percentage discount to all line items
const DISCOUNT_PERCENTAGE = 10;
const discounts = lines.map(line => ({
targets: [{
productVariant: { id: line.merchandise.id }
}],
value: {
percentage: { value: DISCOUNT_PERCENTAGE }
},
message: `10% off with Bank X Pay`
}));
return {
discounts,
discountApplicationStrategy: 'FIRST'
};
}
shopify.extension.toml for the function:
[[extensions]]
type = "function"
name = "Bank Payment Discount"
handle = "bank-payment-discount"
api_version = "2024-10"
[[extensions.targeting]]
module = "./src/index.js"
target = "purchase.checkout.cart-lines-discount-offers.generate"
[extensions.input.variables]
$schema = "https://shopify.dev/docs/api/functions/reference/cart-lines-discount-offers/schema"
Step 3 — Register the Function as a Discount
// In your app's backend — register the function as an automatic discount
const CREATE_AUTOMATIC_DISCOUNT = `
mutation discountAutomaticAppCreate($automaticAppDiscount: DiscountAutomaticAppInput!) {
discountAutomaticAppCreate(automaticAppDiscount: $automaticAppDiscount) {
automaticAppDiscount {
discountId
title
startsAt
endsAt
}
userErrors {
field
message
code
}
}
}
`;
await shopifyGraphQL(CREATE_AUTOMATIC_DISCOUNT, {
automaticAppDiscount: {
title: 'Bank X Pay — 10% Off',
functionId: 'YOUR_FUNCTION_ID', // From Partner Dashboard after deploy
startsAt: '2026-10-01T00:00:00Z',
endsAt: '2026-12-31T23:59:59Z',
combinesWithProductDiscounts: false,
combinesWithOrderDiscounts: false,
combinesWithShippingDiscounts: true
}
});
Step 4 — Combine with BIN Verification (Advanced)
For the strongest enforcement — where you actually verify card BIN ranges — add a pre-checkout verification step:
// BIN range lookup — first 6 digits identify the issuing bank
async function verifyCardBIN(binNumber) {
// Use a BIN lookup service (e.g., binlist.net or a commercial provider)
const response = await fetch(`https://lookup.binlist.net/${binNumber}`, {
headers: { 'Accept-Version': '3' }
});
if (!response.ok) return null;
const data = await response.json();
return {
bank: data.bank?.name,
country: data.country?.alpha2,
type: data.type, // credit / debit
scheme: data.scheme // visa / mastercard / amex
};
}
// Checkout UI extension — verify BIN when card number entered
export function CardBINVerifier() {
const applyAttributeChange = useApplyAttributeChange();
const handleCardNumberChange = async (cardNumber) => {
if (cardNumber.length >= 6) {
const bin = cardNumber.replace(/\s/g, '').substring(0, 6);
const binData = await verifyCardBIN(bin);
if (binData?.bank === 'Bank X') {
// Mark cart as eligible for bank discount
await applyAttributeChange({
type: 'updateAttribute',
key: 'bank_verified',
value: 'bank_x'
});
} else {
// Remove eligibility if different card entered
await applyAttributeChange({
type: 'updateAttribute',
key: 'bank_verified',
value: ''
});
}
}
};
// Render card number input with BIN detection
return (
<TextField
label="Card number"
onChange={handleCardNumberChange}
/>
);
}
Privacy note: Never log or store full card numbers. BIN lookup uses only the first 6 digits — never the full PAN. Ensure your BIN lookup service is PCI-compliant.
Approach 4 — Post-Purchase Cashback via Order Tags
Complexity: ⭐⭐ Moderate Real payment gating: Yes — verified after payment Cost: Low Best for: Cashback promotions, bank partnerships where verification happens post-payment
Instead of discounting before payment, apply the benefit after the customer pays — as cashback, store credit, or a future discount code. This completely sidesteps the timing problem because you know the payment method after the order is placed.
How it works:
Customer places order
↓
Shopify webhook: orders/create fires
↓
Your app reads payment_gateway field on the order
↓
If gateway = "bank_x_pay" → trigger cashback flow
↓
Issue store credit OR send unique discount code for next purchase
Webhook handler:
app.post('/webhooks/orders/create', async (req, res) => {
if (!verifyShopifyWebhook(req)) {
return res.status(401).send('Unauthorized');
}
const order = req.body;
// Check payment gateway
const paymentGateway = order.payment_gateway;
const BANK_GATEWAY = 'bank_x_pay'; // Match Shopify's gateway identifier
if (paymentGateway === BANK_GATEWAY) {
await processBankCashback(order);
}
res.status(200).send('OK');
});
async function processBankCashback(order) {
const cashbackPercent = 10;
const cashbackAmount = (order.total_price * cashbackPercent / 100).toFixed(2);
// Option A: Issue store credit (requires gift cards or credit app)
await issueStoreCredit(order.customer.id, cashbackAmount, order.currency);
// Option B: Send a discount code for next purchase
const discountCode = await createUniqueDiscountCode({
amount: cashbackAmount,
currency: order.currency,
customerId: order.customer.id,
expiryDays: 30,
note: `Cashback from order #${order.order_number}`
});
await sendCashbackEmail({
to: order.email,
customerName: order.customer.first_name,
cashbackAmount,
discountCode,
orderId: order.id
});
// Log for reconciliation with bank partner
await logBankCashbackEvent({
orderId: order.id,
orderNumber: order.order_number,
cashbackAmount,
currency: order.currency,
processedAt: new Date().toISOString()
});
}
Creating the discount code programmatically:
async function createUniqueDiscountCode(params) {
const code = `CASHBACK-${generateUniqueId()}`;
await shopifyGraphQL(`
mutation {
discountCodeBasicCreate(basicCodeDiscount: {
title: "Bank X Cashback — Order ${params.orderId}",
code: "${code}",
startsAt: "${new Date().toISOString()}",
endsAt: "${getExpiryDate(params.expiryDays)}",
customerSelection: {
customers: {
add: ["gid://shopify/Customer/${params.customerId}"]
}
},
customerGets: {
value: {
discountAmount: {
amount: "${params.amount}",
appliesOnEachItem: false
}
},
items: { all: true }
},
usageLimit: 1,
appliesOncePerCustomer: true
}) {
codeDiscountNode {
id
codeDiscount {
... on DiscountCodeBasic {
title
codes(first: 1) {
edges { node { code } }
}
}
}
}
userErrors { field message }
}
}
`);
return code;
}
Pros:
- Actually verifies payment method — no honor system
- No checkout complexity
- Works on all Shopify plans (no Plus required)
- Cashback model is trusted and familiar to customers
Cons:
- Benefit is received after purchase — less compelling than upfront discount
- Requires email communication and follow-up
- Cashback codes can still be used without the bank card on the next purchase
Approach 5 — Payment Provider Co-Integration
Complexity: ⭐⭐⭐⭐⭐ Expert Real payment gating: Full enforcement Cost: Significant development + payment provider partnership Best for: Large-scale bank partnership programs with dedicated payment infrastructure
If your bank partner operates their own Shopify payment gateway (registered as a payment provider through Shopify's Payments Platform), they can embed discount logic directly into their payment flow:
- Customer selects the bank's payment gateway
- Bank's payment UI (rendered in checkout) verifies card eligibility
- Bank's backend communicates eligibility to your app via a webhook or shared session
- Your app applies the discount via the Storefront API or a Checkout UI extension
- Customer completes payment with discount already applied
This requires the bank to be a registered Shopify payment provider — not a standard merchant integration. This is an enterprise-level partnership engagement with Shopify directly.
When this makes sense:
- You're running a national-scale bank co-marketing program
- The bank has existing Shopify payment provider infrastructure
- Transaction volume justifies the engineering investment on both sides
Choosing the Right Approach
How large is the promotion?
↓
Small / one-time campaign
→ Approach 1 (unique codes per cardholder) — done in a day
↓
Ongoing promotion with verified bank customers
→ Approach 2 (bank OAuth verification + customer tags)
↓
Need discount visible during checkout (not post-purchase)
→ Are you on Shopify Plus?
YES → Approach 3 (Shopify Functions)
NO → Approach 1 or 2
↓
Cashback model acceptable to bank partner?
→ Approach 4 (post-purchase cashback via order webhook)
↓
National-scale program with bank as payment provider?
→ Approach 5 (co-integrated payment provider)
Fraud Prevention Across All Approaches
Regardless of approach, implement these fraud controls:
// Rate limiting — prevent code generation abuse
const rateLimit = new RateLimiter({
maxRequests: 3,
windowMs: 60 * 60 * 1000 // 3 verifications per hour per IP
});
// Duplicate prevention — one cashback per order
async function hasOrderBeenProcessed(orderId) {
return await db.cashbackEvents.exists({ orderId });
}
// Reconciliation report for bank partner
async function generateBankReconciliationReport(startDate, endDate) {
const events = await db.cashbackEvents.findBetween(startDate, endDate);
return {
totalOrders: events.length,
totalCashbackIssued: events.reduce((sum, e) => sum + e.cashbackAmount, 0),
currency: 'USD',
period: { startDate, endDate },
orders: events.map(e => ({
orderId: e.orderId,
orderNumber: e.orderNumber,
cashbackAmount: e.cashbackAmount,
processedAt: e.processedAt
}))
};
}
Summary
| Approach | Plan Required | Dev Effort | Enforcement | Time to Launch |
|---|---|---|---|---|
| Unique codes per cardholder | Any | None | Honor system | 1 day |
| Bank OAuth + customer tags | Any | Medium | Verified identity | 1–2 weeks |
| Shopify Functions + gateway | Plus only | High | Gateway-level | 3–6 weeks |
| Post-purchase cashback | Any | Low–Medium | Full (post-payment) | 3–5 days |
| Payment provider co-integration | Plus + partner | Very high | Full enforcement | Months |