Applies to: Web Payments SDK | Payments API | Bank Accounts API
Learn how to store and charge a bank account on file with the Web Payments SDK.
The Web Payments SDK and Square APIs enable you to securely store customer bank accounts for one-time or recurring ACH charges, eliminating the need for customers to re-link their accounts on each visit.
Common use cases include gyms processing monthly membership fees, utility companies streamlining recurring billing, or any business that needs to collect regular ACH payments. By integrating the Bank Accounts API with the Web Payments SDK, you can implement these payment features efficiently while maintaining security and compliance.
This guide covers storing bank accounts and processing both one-time and recurring ACH payments. For ACH payments without storing accounts, see Take ACH Bank Transfer Payments.
Did you know?
Bank accounts cannot be deleted once created due to compliance and record-keeping requirements. Instead, use the DisableBankAccount endpoint to prevent future charges while maintaining historical records.
Before implementing bank account storage, ensure your application meets these requirements:
- Location: ACH bank transfers via Web Payments SDK are only supported in the United States. For international bank account storage and payment processing, see International Development
- Permissions: Your application needs:
BANK_ACCOUNTS_WRITEandBANK_ACCOUNTS_READCUSTOMERS_WRITEPAYMENTS_WRITEandPAYMENTS_READ
- Setup: Complete the ACH Bank Transfer Payments setup
- Bank Support: Customer banks must be supported by Plaid (Square's banking partner)
This section walks through linking and storing a customer's bank account to their profile for future use.

Charging a buyer on their linked and stored bank account involves the generation and use of these token types:
| Token | Definition |
|---|---|
BNON | A token returned by Web Payments SDK for a Plaid-authorized bank account. |
BACT | A token returned by Square for a Plaid-authorized bank account stored with Square. |
BAUTH | A token returned by the Web Payments SDK for a buyer-authorized charge on a linked bank account. Use this token with the CreatePayment method. |
First, create a Square Customer object to associate with the bank account:
Create customer
The ach.tokenize with AchStoreOptions.intent set to 'STORE' presents the customer with Plaid's secure authentication interface. Set up an event listener to capture the BNON token after completion of the tokenize call:
Use the Web Payments SDK to initialize ACH and launch the Plaid authentication flow:
// Initialize ACH payment method const ach = await payments.ach({ transactionId: '415111211611', // Your unique transaction ID }); ach.addEventListener('ontokenization', async function(event) { const { tokenResult, error } = event.detail; if (error) { throw new Error(`Tokenization failed: ${error}`); } if (tokenResult.status === 'OK') { // Send token to your server await fetch('/create-bank-on-file', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: tokenResult.token }) }); } }); // Launch Plaid authentication flow try { await ach.tokenize({ accountHolderName: 'Lauren Noble', intent: 'STORE', // Always use STORE for saving accounts }); } catch (e) { console.error(e); }
From your server, create the bank account record using the BNON token and the following parameters:
- source_id - The BNON token sent to your server after the call to ach.tokenize.
- customer_id - The unique ID for the customer generated during customer creation.
Note
Testing with Real Tokens Required
The source_id parameter must be a valid token generated by the Web Payments SDK's ach.tokenize() method. You cannot use placeholder values, example tokens from documentation, or manually created strings for testing.
If you attempt to use an invalid or made-up source_id, you'll receive this error:
{ "errors": [ { "code": "NOT_FOUND", "detail": "Bank nonce not found", "category": "INVALID_REQUEST_ERROR" } ] }
To test the CreateBankAccount endpoint, you must first:
- Implement the Web Payments SDK on your frontend
- Complete the Plaid bank linking flow
- Call
ach.tokenize()to generate a valid token - Use that token immediately in your
CreateBankAccountrequest
Bank tokens are single-use and expire quickly, so you'll need to generate a fresh token for each test.
curl https://connect.squareupsandbox.com/v2/bank-accounts \ -X POST \ -H 'Authorization: Bearer {ACCESS_TOKEN}' \ -H 'Content-Type: application/json' \ -d '{ "idempotency_key": "{UNIQUE_KEY}", "source_id": "bnon:Ja85BvcwFYPiDZJV4H", "customer_id": "WC1GYWRIT7STE3GU4ZLQ3X76EF" }'
Process a single charge to a customer's stored bank account with these steps. These steps assume a bank account has already been linked and stored to the customer's account.
Did you know?
The Payments API automatically rejects attempts to charge seller bank accounts (those without the "bact:" prefix). This prevents accidentally charging business accounts instead of customer accounts.
Retrieve the customer's stored bank accounts using the ListBankAccounts endpoint. Important: You must include the customer_id as a query parameter to filter results to only that customer's accounts. Without this parameter, the endpoint returns all bank accounts associated with the seller's Square account, including the seller's own bank accounts.
List bank accounts
Present the available bank accounts in your UI to let the customer select which account to charge. Display enough information for the customer to identify their account while maintaining security:
Recommended display fields:
holder_name- Account holder's namebank_name- Financial institution nameaccount_type- CHECKING or SAVINGSaccount_number_suffix- Last 3-4 digits (e.g., "...000")
Important: Never display full account or routing numbers. The account_number_suffix provides enough information for identification.
Example UI pattern:
bankAccounts.forEach(account => { // Display: "Citizens Bank - Checking (...000) - Lauren Noble" const displayText = `${account.bank_name} - ${account.account_type} (...${account.account_number_suffix}) - ${account.holder_name}`; // Verify account is ready for payments if (account.status === 'VERIFIED' && account.debitable) { // Add to payment method selection UI addPaymentOption(displayText, account.id); } });
Capture the selected account's id to use in the next step for payment authorization.
Use the ach.tokenize method to generate a BAUTH token for the one-time charge authorization with the following parameters:
- bankAccountId - The stored bank account token (
BACT) that was returned when your application stored the bank account with Square. - intent - The purpose of the authorization. In this case, to
CHARGEthe bank account. - amount - The full price of the purchase, including any taxes and fees.
- currency - The currency of the bank account to authorize.
// Initialize ACH const ach = await payments.ach({ transactionId: '415111211611', }); // Authorize one-time charge try { await ach.tokenize({ intent: 'CHARGE', // Use CHARGE for one-time payments amount: '5.00', // Amount as string with dollars and cents currency: 'USD', bankAccountId: "bact:J71CYCQ789KnZnXi5HC", }); } catch (e) { console.error(e); }
Capture the BAUTH token using the same event listener pattern shown earlier.
On your application server, create the payment with the Payments API CreatePayment endpoint using the BAUTH token:
Create payment
Set up automated recurring charges for the same amount at regular intervals.

When you use intent: RECURRING_CHARGE with the Web Payments SDK, Square does not automatically charge the customer’s bank account on the specified frequency. The RECURRING_CHARGE operation only creates a reusable BAUTH token that’s authorized for recurring payments according to the schedule you defined. Your application is fully responsible for implementing the scheduling logic and calling the Payments API CreatePayment endpoint with this token whenever a payment is due.
Think of it as getting permission to charge on a schedule, not setting up automatic charges. You’ll need to build your own scheduling system (using cron jobs, scheduled tasks, or a third-party scheduling service) to trigger payments at the appropriate times. The frequency parameters you provide during tokenization are for authorization purposes only—they tell the customer what schedule they’re agreeing to, but don’t create any automatic payment processing on Square’s side.
Note
These steps assume a bank account has already been linked and stored to the customer's account.
Follow the same process as one-time payments to retrieve and display the customer's bank accounts. Consider adding UI elements to capture subscription preferences like frequency and start date.
Configure the recurring payment schedule when generating the BAUTH token:
// Helper function to create daily recurring frequency configuration function createDailyRecurringFrequency(days) { return { days: days // Charge every [x] days }; } // Example: Charge every 3 days const dailyFrequency = createDailyRecurringFrequency(3); // Use with ach.tokenize() await ach.tokenize({ intent: 'RECURRING_CHARGE', bankAccountId: 'bact:J71CYCQ789KnZnXi5HC', amount: '9.99', currency: 'USD', frequency: dailyFrequency, startDate: '2024-12-21' });
Use the BAUTH token to charge the account according to the authorized schedule. The payment process is identical to one-time payments, but you can reuse the BAUTH token for each scheduled charge without re-authorization.
Note
You don't need to charge immediately after obtaining the BAUTH token. Store it securely and use it when each scheduled payment is due.
For subscriptions with changing amounts (like usage-based billing), you'll need to add the AchVariableRecurringOptions parameter to the tokenize call.
Your client app uses the Web Payments SDK to let a buyer authorize a bank transfer and the payment:
// CLIENT-SIDE CODE (Browser) // For variable amounts (like utility bills), authorize recurring charges with changing amounts async function authorizeVariableRecurringCharges(bankAccountId) { try { // Initialize ACH const ach = await payments.ach({ transactionId: generateTransactionId() // Your unique transaction ID }); // Get authorization for recurring charges with variable amounts await ach.tokenize({ intent: 'RECURRING_CHARGE', bankAccountId: bankAccountId, variableAmount: true, // Indicates amount will vary each billing cycle frequency: { monthly: { occurrence: 1, // Every month days: { daysOfMonth: [15], // Bill on the 15th endOfMonth: false } } }, startDate: '2024-12-21' }); // Set up event listener to capture token ach.addEventListener('ontokenization', async function(event) { const { tokenResult, error } = event.detail; if (error) { console.error('Tokenization failed:', error); return; } if (tokenResult.status === 'OK') { // Save the BAUTH token for recurring use const response = await fetch('/api/save-recurring-authorization', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: tokenResult.token, // BAUTH token (reusable) customerId: customerId, billType: 'electric', schedule: 'monthly-15th' }) }); if (response.ok) { showSuccessMessage('Recurring payment authorization saved'); } } }); } catch (e) { console.error('Authorization failed:', e); showErrorMessage('Unable to authorize recurring payments'); } } // This only needs to be done once during customer setup async function setupVariableRecurringBilling() { const bankAccountId = 'bact:J71CYCQ789KnZnXi5HC'; await authorizeVariableRecurringCharges(bankAccountId); }
Note
The code examples in this section demonstrate a sample implementation pattern for managing recurring payments. Your actual implementation will depend on your application's architecture, database design, and business requirements. Consider these examples as a reference rather than a required approach.
Your application server code should perform two functions:
- Securely store an authorization for each customer's recurring bill
- Charge a customer on a pre-determined schedule
These examples show a typical business logic pattern which you might choose to adapt.
This example creates an endpoint that accepts a customer ID, an auth token and additional parameters needed to set up regular billing.
// SERVER-SIDE CODE (Node.js/Express example) // Save the recurring authorization token when customer sets up variable billing app.post('/api/save-recurring-authorization', async (req, res) => { const { token, customerId, billType, schedule } = req.body; try { // Parse the schedule string from client (e.g., 'monthly-15th') const scheduleDetails = parseSchedule(schedule); // Store the reusable BAUTH token in your database // This token will be used for all future scheduled charges await db.recurringAuthorizations.create({ customerId: customerId, bauthToken: token, // This token is reusable for variable amounts billType: billType, schedule: schedule, status: 'ACTIVE', createdAt: new Date(), lastUsed: null, // Store parsed schedule details for your cron job/scheduler scheduleDetails: { frequency: scheduleDetails.frequency, dayOfMonth: scheduleDetails.dayOfMonth, daysOfWeek: scheduleDetails.daysOfWeek, nextBillingDate: calculateNextBillingDate(scheduleDetails) } }); // Optional: Create a subscription record for tracking await db.subscriptions.create({ customerId: customerId, type: billType, status: 'ACTIVE', variableAmount: true, authorizationId: token, startDate: new Date() }); res.json({ success: true, message: 'Recurring payment authorization saved successfully', nextBillingDate: calculateNextBillingDate(scheduleDetails) }); } catch (error) { console.error('Failed to save authorization:', error); res.status(500).json({ success: false, error: 'Failed to save payment authorization' }); } }); // Helper function to parse schedule string function parseSchedule(schedule) { // Examples: 'monthly-15th', 'monthly-1st', 'monthly-last', 'weekly-monday' const parts = schedule.toLowerCase().split('-'); const frequency = parts[0]; if (frequency === 'monthly') { if (parts[1] === 'last') { return { frequency: 'monthly', endOfMonth: true }; } // Extract day number from strings like '15th', '1st', '23rd' const dayOfMonth = parseInt(parts[1].replace(/[^\d]/g, '')); return { frequency: 'monthly', dayOfMonth: dayOfMonth }; } else if (frequency === 'weekly') { return { frequency: 'weekly', daysOfWeek: [parts[1].toUpperCase()] }; } else if (frequency === 'daily') { const days = parseInt(parts[1]) || 1; return { frequency: 'daily', interval: days }; } throw new Error(`Invalid schedule format: ${schedule}`); } // Helper function to calculate next billing date based on schedule function calculateNextBillingDate(scheduleDetails) { const today = new Date(); if (scheduleDetails.frequency === 'monthly') { if (scheduleDetails.endOfMonth) { // Last day of next month const nextMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0); return nextMonth; } else { // Specific day of month const nextDate = new Date(today.getFullYear(), today.getMonth(), scheduleDetails.dayOfMonth); if (nextDate <= today) { nextDate.setMonth(nextDate.getMonth() + 1); } return nextDate; } } else if (scheduleDetails.frequency === 'weekly') { // Calculate next occurrence of specified day const daysOfWeek = ['SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY']; const targetDay = daysOfWeek.indexOf(scheduleDetails.daysOfWeek[0]); const todayDay = today.getDay(); const daysUntilTarget = (targetDay - todayDay + 7) % 7 || 7; const nextDate = new Date(today); nextDate.setDate(today.getDate() + daysUntilTarget); return nextDate; } else if (scheduleDetails.frequency === 'daily') { const nextDate = new Date(today); nextDate.setDate(today.getDate() + scheduleDetails.interval); return nextDate; } return null; } // Helper function to retrieve saved authorization async function getCustomerRecurringAuth(customerId) { const auth = await db.recurringAuthorizations.findOne({ where: { customerId: customerId, status: 'ACTIVE' } }); if (!auth) { throw new Error('No active recurring authorization found'); } return auth; }
Your application backend takes the payment amount and the bank charge authorization token and then calls CreatePayment to process the payment:
// SERVER-SIDE CODE (Node.js/Express example) // This endpoint is called by your scheduler (cron job) when payments are due // No client interaction needed - this runs automatically on schedule app.post('/api/process-scheduled-variable-payment', async (req, res) => { const { customerId } = req.body; try { // Get the saved BAUTH token and schedule details for this customer const authorization = await getCustomerRecurringAuth(customerId); // Calculate this month's bill amount (your business logic) const currentMonthAmount = await calculateMonthlyBill(customerId); // Create payment with this month's specific amount using the saved token const payment = await client.payments.create({ sourceId: authorization.bauthToken, // Reusable token from variable recurring auth idempotencyKey: generateIdempotencyKey(), amountMoney: { amount: Math.round(currentMonthAmount * 100), // This month's amount in cents currency: 'USD' }, autocomplete: true, locationId:"L88917AVBK2S5" customerId: customerId, note: `${authorization.billType} bill for ${new Date().toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}`, statementDescriptorName: authorization.billType.toUpperCase() }); // Update the authorization record with last used timestamp await db.recurringAuthorizations.update( { lastUsed: new Date() }, { where: { customerId: customerId } } ); // Calculate and store next billing date const nextBillingDate = calculateNextBillingDate(authorization.scheduleDetails); await db.recurringAuthorizations.update( { 'scheduleDetails.nextBillingDate': nextBillingDate }, { where: { customerId: customerId } } ); // Log the payment for record keeping await db.paymentHistory.create({ customerId: customerId, paymentId: payment.result.payment.id, amount: currentMonthAmount, billType: authorization.billType, status: payment.result.payment.status, processedAt: new Date() }); res.json({ success: true, paymentId: payment.result.payment.id, amount: currentMonthAmount, nextBillingDate: nextBillingDate }); } catch (error) { console.error('Payment failed:', error); // Log failed payment attempt await db.failedPayments.create({ customerId: customerId, error: error.message, attemptedAt: new Date() }); res.status(400).json({ success: false, error: error.message }); } }); // Scheduled job that runs daily to process due payments // This would be called by your cron job or task scheduler async function processDuePayments() { console.log('Processing scheduled payments for', new Date().toDateString()); try { // Find all customers with payments due today const duePayments = await db.recurringAuthorizations.findAll({ where: { status: 'ACTIVE', 'scheduleDetails.nextBillingDate': { [Op.lte]: new Date() // Due today or overdue } } }); console.log(`Found ${duePayments.length} payments to process`); // Process each due payment for (const authorization of duePayments) { try { await processScheduledVariablePayment(authorization.customerId); console.log(`Successfully processed payment for customer ${authorization.customerId}`); } catch (error) { console.error(`Failed to process payment for customer ${authorization.customerId}:`, error); // Continue processing other payments even if one fails } } console.log('Scheduled payment processing complete'); } catch (error) { console.error('Error in payment processing job:', error); } } // Helper function to calculate monthly bill (your business logic) async function calculateMonthlyBill(customerId) { // Example: Fetch usage data and calculate bill const usage = await db.usageData.findOne({ where: { customerId: customerId, billingPeriod: getCurrentBillingPeriod() } }); // Your pricing logic here const baseRate = 29.99; const usageRate = 0.12; // per unit const totalAmount = baseRate + (usage.unitsUsed * usageRate); return totalAmount; } // Example cron job setup (using node-cron) // This runs every day at 2 AM to process any due payments cron.schedule('0 2 * * *', async () => { console.log('Starting daily payment processing job'); await processDuePayments(); });
While you cannot delete stored bank accounts, you can disable them to prevent future charges:
Important
Disabling is permanent for that specific bank account record. To charge the account again, the customer must complete the full onboarding flow as if linking a new account.
curl https://connect.squareupsandbox.com/v2/bank-accounts/bact:J71CYCQ789KnZnXi5HC/disable \ -X POST \ -H 'Authorization: Bearer {ACCESS-TOKEN}' \ -H 'Content-Type: application/json'
Subscribe to these webhook events to track bank account lifecycle changes:
bank_account.created- Triggered when a customer links a new bank accountbank_account.verified- Triggered when account verification completes (account ready for payments)bank_account.disabled- Triggered when an account is disabled
These events help you update your UI, notify customers, and handle account status changes automatically. See Webhooks API for setup instructions.
- Verify account status is
VERIFIEDbefore attempting charges - Ensure sufficient funds in customer's account
- Check that BAUTH token hasn't expired (tokens have limited validity)
- Check the
fingerprintfield of existing accounts before creating new ones - Use
ListBankAccountsto find existing accounts for the customer
- Verify you're using the correct
customer_idwhen listing accounts - Ensure the bank account ID starts with "bact:" prefix
- Check that the account hasn't been disabled
- Implement webhook notifications for payment status updates
- Set up error handling for failed payments
- Configure testing scenarios in the Sandbox environment