Payments
Bank debits
ACH Direct Debit

ACH Guide

Stripe supports accepting ACH payments—direct from bank accounts—alongside credit cards. ACH is currently supported only for Stripe businesses based in the U.S. We'd love to hear about your use case, though!

With Stripe, you can accept ACH payments in nearly the same way as you accept credit card payments, by providing a verified bank account as the source argument for a charge request. However, accepting bank accounts requires a slightly different initial workflow than accepting credit cards:

  1. Bank accounts must first be verified.
  2. Bank accounts must be authorized for your use by the customer.

After taking both steps for a bank account, your customer can use it like other payment methods, including for recurring charges and Connect applications. The two key differences between using bank accounts and credit cards are:

  • ACH payments take up to 5 business days to receive acknowledgment of their success or failure. Because of this, ACH payments take up to 7 business days to be reflected in your available Stripe balance.
  • You can only accept funds in USD and only from U.S. bank accounts. In addition, your account must have a U.S./USD bank account to accept ACH payments.

Collecting and verifying bank accounts

Before you can create an ACH charge, you must first collect and verify your customer’s bank account and routing number. In order to properly identify the bank account, you also need to collect the name of the person or business who owns the account, and if the account is owned by an individual or a company. Stripe provides two methods for doing so: instant collection and verification using Plaid or collection via Stripe.js with delayed-verification using microdeposits. You may incur additional costs when using Plaid, depending on the size of your business. Take this into account when making your decision.

As charging a bank account requires both verification of the account and customer authorization to use it, the best practice is to store the bank account on a Customer object in Stripe for easy reuse.

Using Plaid

Plaid provides the quickest way to collect and verify your customer’s banking information. Using the Stripe + Plaid integration, you’re able to instantly receive a verified bank account, allowing for immediate charging. This is done by using Plaid Link, receiving the Stripe bank account token directly from Plaid.

Step 1: Set up your Plaid account

If you do not have a Plaid account, create one. Your account will be automatically enabled for integration access. To verify that your Plaid account is enabled for the Stripe integration, go to the Integrations section of the account dashboard. Make sure your Stripe account is connected there.

Step 2: Fetch a Link token

A link_token is a one-time use token that is used to initialize Plaid Link. You can create a link_token and configure it for your specific Link flow by calling the Create Link Token endpoint from your server.

curl https://sandbox.plaid.com/link/token/create \ -H "Content-Type: application/json" \ -d "{\"client_id\": \"{{PLAID_CLIENT_ID}}\",\"secret\": \"{{PLAID_SECRET}}\",\"client_name\": \"My App\",\"user\": {\"client_user_id\": \"Stripe test\"},\"products\": [\"auth\"],\"country_codes\": [\"US\"],\"language\": \"en\", \"webhook\": \"https://webhook.sample.com/\"}"
# Using Plaid's Ruby bindings (https://github.com/plaid/plaid-ruby) client = Plaid::Client.new( env: 'sandbox', client_id: '{{PLAID_CLIENT_ID}}', secret: '{{PLAID_SECRET}}' ) client_user_id = 'Stripe test' # Create the link_token with all of your configurations link_token_response = client.link_token.create( user: { client_user_id: client_user_id }, client_name: 'My App', products: ['auth'], country_codes: ['US'], language: 'en', webhook: 'https://sample.webhook.com', ) # Pass the result to your client-side app to initialize Link
# Using Plaid's Python bindings (https://github.com/plaid/plaid-python) client = Client( '{{PLAID_CLIENT_ID}}', '{{PLAID_SECRET}}', 'sandbox' ) client_user_id = 'Stripe test' # Create a link_token with all of your configurations response = client.LinkToken.create({ 'user': { 'client_user_id': client_user_id, }, 'products': ["auth"], 'client_name': "My App", 'country_codes': ['US'], 'language': 'en', 'webhook': 'https://sample.webhook.com', }) # Pass the result to your client-side app to initialize Link print('link_token' + response['link_token'])
// Using Plaid's Java bindings (https://github.com/plaid/plaid-java) // Use builder to create a client PlaidClient plaidClient = PlaidClient.newBuilder() .clientIdAndSecret("{{PLAID_CLIENT_ID}}", "{{PLAID_SECRET}}") .sandboxBaseUrl() // Use the Sandbox. Can also be `developmentBaseUrl()` or `productionBaseUrl()` .build(); String clientUserId = "Stripe test"; // Create a link token for your specific link flow Response<LinkTokenCreateResponse> response = plaidClient .service() .linkTokenCreate( new LinkTokenCreateRequest( user, "My App", Collections.singletonList("auth") ) .withCountryCodes(Collections.singletonList("US")) .withLanguage("en") .withWebhook("https://sample.webhook.com") ).execute(); // Pass the result to your client-side app to initialize Link return response.body();
// Using Plaid's Node.js bindings (https://github.com/plaid/plaid-node) var plaid = require('plaid'); var plaidClient = new plaid.Client( '{{PLAID_CLIENT_ID}}', '{{PLAID_SECRET}}', plaid.environments.sandbox ); const clientUserId = 'Stripe test'; client.createLinkToken({ user: { client_user_id: clientUserId, }, client_name: 'My App', products: ['auth'], country_codes: ['US'], language: 'en', webhook: 'https://sample.webhook.com', }, function(error, linkTokenResponse) { // Pass the result to your client-side app to initialize Link response.json({ link_token: linkTokenResponse.link_token }); });

Step 3: Integrate with Plaid Link

Integrating with Link is easy. All it takes is a few lines of client-side JavaScript and a small server-side handler to exchange the Link public_token for a Plaid access_token and a Stripe bank account token.

<button id="link-button">Link Account</button> <script src="https://cdn.plaid.com/link/v2/stable/link-initialize.js"></script> <script type="text/javascript"> (async function() { const configs = { // Pass the link_token generated in step 2. token: '{{LINK_TOKEN}}', onLoad: function() { // The Link module finished loading. }, onSuccess: function(public_token, metadata) { // The onSuccess function is called when the user has // successfully authenticated and selected an account to // use. // // When called, you will send the public_token // and the selected account ID, metadata.accounts, // to your backend app server. // // sendDataToBackendServer({ // public_token: public_token, // account_id: metadata.accounts[0].id // }); console.log('Public Token: ' + public_token); switch (metadata.accounts.length) { case 0: // Select Account is disabled: https://dashboard.plaid.com/link/account-select break; case 1: console.log('Customer-selected account ID: ' + metadata.accounts[0].id); break; default: // Multiple Accounts is enabled: https://dashboard.plaid.com/link/account-select } }, onExit: async function(err, metadata) { // The user exited the Link flow. if (err != null) { // The user encountered a Plaid API error // prior to exiting. } // metadata contains information about the institution // that the user selected and the most recent // API request IDs. // Storing this information can be helpful for support. }, }; var linkHandler = Plaid.create(configs); document.getElementById('link-button').onclick = function() { linkHandler.open(); }; })(); </script>

Step 4: Write server-side handler

The Link module handles the entire onboarding flow securely and quickly, but does not actually retrieve account data for a user. Instead, the Link module returns a public_token and an accounts array, which is a property on the metadata object, via the onSuccess callback.

The accounts array will contain information about bank accounts associated with the credentials entered by the user, and may contain multiple accounts if the user has more than one bank account at the institution. In order to avoid any confusion about which account your user wishes to use with Stripe, it is recommended to set Select Account to “Enabled for one account” in the Plaid developer dashboard. When this setting is selected, the accounts array will always contain exactly one element.

When your server has the public_token and account_id, you must make two calls to the Plaid server to get the Stripe bank account token along with the Plaid access_token to use for other Plaid API requests.

curl https://sandbox.plaid.com/item/public_token/exchange \ -H "Content-Type: application/json" \ -d "{\"client_id\": \"{{PLAID_CLIENT_ID}}\", \"secret\": \"{{PLAID_SECRET}}\", \"public_token\": \"{{PLAID_LINK_PUBLIC_TOKEN}}\"}" curl https://sandbox.plaid.com/processor/stripe/bank_account_token/create \ -H "Content-Type: application/json" \ -d "{\"client_id\": \"{{PLAID_CLIENT_ID}}\", \"secret\": \"{{PLAID_SECRET}}\", \"access_token\": \"{{PLAID_ACCESS_TOKEN}}\", \"account_id\": \"{{PLAID_ACCOUNT_ID}}\"}"
# Using Plaid's Ruby bindings (https://github.com/plaid/plaid-ruby) client = Plaid::Client.new( env: :sandbox, client_id: ENV['PLAID_CLIENT_ID'], secret: ENV['PLAID_SECRET'], public_key: ENV['PLAID_PUBLIC_KEY'] ) exchange_token_response = client.item.public_token.exchange('{{PLAID_LINK_PUBLIC_TOKEN}}') access_token = exchange_token_response['access_token'] stripe_response = client.processor.stripe.bank_account_token.create(access_token, '{{ACCOUNT_ID}}') bank_account_token = stripe_response['stripe_bank_account_token']
# Using Plaid's Python bindings (https://github.com/plaid/plaid-python) client = Client( '{{PLAID_CLIENT_ID}}', '{{PLAID_SECRET}}', '{{PLAID_PUBLIC_KEY}}', 'sandbox' ) exchange_token_response = client.Item.public_token.exchange('{{PLAID_LINK_PUBLIC_TOKEN}}') access_token = exchange_token_response['access_token'] stripe_response = client.Processor.stripeBankAccountTokenCreate(access_token, '{{ACCOUNT_ID}}') bank_account_token = stripe_response['stripe_bank_account_token']
$headers[] = 'Content-Type: application/json'; $params = [ 'client_id' => '{{PLAID_CLIENT_ID}}', 'secret' => '{{PLAID_SECRET}}', 'public_token' => '{{PLAID_LINK_PUBLIC_TOKEN}}', ]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, "https://sandbox.plaid.com/item/public_token/exchange"); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30); curl_setopt($ch, CURLOPT_TIMEOUT, 80); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); if(!$result = curl_exec($ch)) { trigger_error(curl_error($ch)); } curl_close($ch); $jsonParsed = json_decode($result); $btok_params = [ 'client_id' => '{{PLAID_CLIENT_ID}}', 'secret' => '{{PLAID_SECRET}}', 'access_token' => $jsonParsed->access_token, 'account_id' => '{{ACCOUNT_ID}}' ]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, "https://sandbox.plaid.com/processor/stripe/bank_account_token/create"); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($btok_params)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30); curl_setopt($ch, CURLOPT_TIMEOUT, 80); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); if(!$result = curl_exec($ch)) { trigger_error(curl_error($ch)); } curl_close($ch); $btok_parsed = json_decode($result);
// Using Plaid's Java bindings (https://github.com/plaid/plaid-java) // Use builder to create a client PlaidClient plaidClient = PlaidClient.newBuilder() .clientIdAndSecret("{{PLAID_CLIENT_ID}}", "{{PLAID_SECRET}}") .publicKey("{{PLAID_PUBLIC_KEY}}") .sandboxBaseUrl() // Use the Sandbox. Can also be `developmentBaseUrl()` or `productionBaseUrl()` .build(); // Required request parameters are always Request object constructor arguments Response<ItemPublicTokenExchangeResponse> exchangeResponse = plaidClient.service() .itemPublicTokenExchange(new ItemPublicTokenExchangeRequest("{{PLAID_LINK_PUBLIC_TOKEN}}")).execute(); if (exchangeResponse.isSuccessful()) { String accessToken = exchangeResponse.body().getAccessToken(); Response<ItemStripeTokenCreateResponse> stripeResponse = plaidClient.service().itemStripeTokenCreate(new ItemStripeTokenCreateRequest(accessToken, "{{ACCOUNT_ID}}")).execute(); if (stripeResponse.isSuccessful()) { String bankAccountToken = stripeResponse.body().getStripeBankAccountToken(); } }
// Using Plaid's Node.js bindings (https://github.com/plaid/plaid-node) var plaid = require('plaid'); var plaidClient = new plaid.Client( '{{PLAID_CLIENT_ID}}', '{{PLAID_SECRET}}', '{{PLAID_PUBLIC_KEY}}', plaid.environments.sandbox ); plaidClient.exchangePublicToken('{{PLAID_LINK_PUBLIC_TOKEN}}', function(err, res) { var accessToken = res.access_token; // Generate a bank account token plaidClient.createStripeToken(accessToken, '{{ACCOUNT_ID}}', function(err, res) { var bankAccountToken = res.stripe_bank_account_token; }); });
// Using Plaid's Go bindings (https://github.com/plaid/plaid-go) clientOptions := plaid.ClientOptions{ "{{PLAID_CLIENT_ID}}", "{{PLAID_SECRET}}", "{{PLAID_PUBLIC_KEY}}", plaid.Sandbox, &http.Client{}, } client, _ := plaid.NewClient(clientOptions) exchangeResp, _ := client.ExchangePublicToken("{{PLAID_LINK_PUBLIC_TOKEN}}") stripeTokenResp, _ := client.CreateStripeToken( exchangeResp.AccessToken, "{{ACCOUNT_ID}}", ) stripeBankAccountToken := stripeTokenResp.StripeBankAccountToken
// Plaid does not yet offer a .NET library. // See the following section to manually collect and verify bank accounts.

The response will contain a verified Stripe bank account token ID. You can attach this token to a Stripe Customer object, or create a charge directly on it.

{ "stripe_bank_account_token": "btok_clbYT3vKslibBeSxUXg7", "request_id": "[Unique request ID]" }

Step 5: Get ready for production

Plaid uses different API hosts for test and production requests. The above request uses Plaid’s Sandbox environment, which uses simulated data. To test with live users, use Plaid’s Development environment. Plaid’s Development environment supports up to 100 live objects, which you won’t be billed for. When it’s time to go live, use Plaid’s Production environment.

Manually collecting and verifying bank accounts

Plaid supports instant verification for many of the most popular banks. However, if your customer’s bank is not supported or you don’t want to integrate with Plaid, collect and verify the customer’s bank using Stripe alone.

First, use Stripe.js to securely collect your customer’s bank account information, receiving a representative token in return. When you have that, attach it to a Stripe customer in your account.

curl https://api.stripe.com/v1/customers \ -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \ -d description="Example customer" \ -d source=btok_4XNshPRgmDRCVi
# Set your secret key. Remember to switch to your live secret key in production! # See your keys here: https://dashboard.stripe.com/account/apikeys Stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc' # Get the bank token submitted by the form token_id = params[:stripeToken] # Create a Customer customer = Stripe::Customer.create({ description: 'Example customer', source: token_id, })
# Set your secret key. Remember to switch to your live secret key in production! # See your keys here: https://dashboard.stripe.com/account/apikeys stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc' # Get the bank token submitted by the form token_id = request.POST['stripeToken'] # Create a Customer customer = stripe.Customer.create( description='Example customer', source=token_id, )
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys \Stripe\Stripe::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); \Stripe\Customer::create([ 'description' => 'Example customer', 'source' => $token_id, ]);
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys Stripe.apiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; // Get the bank token submitted by the form String tokenID = request.getParameter("stripeToken"); // Create a Customer CustomerCreateParams params = CustomerCreateParams.builder() .setDescription("Example customer") .setSource(tokenID) .build(); Customer customer = Customer.create(params);
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys const stripe = require('stripe')('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); // Get the bank token submitted by the form var tokenID = request.body.stripeToken; // Create a Customer const customer = await stripe.customers.create({ description: 'Example customer', source: tokenID, });
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys stripe.Key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc" // Get the bank token submitted by the form params := &stripe.CustomerParams{ Description: stripe.String("Example customer"), Source: stripe.String(tokenID), // Get the bank token submitted by the form } customer, _ := customer.New(params)
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys StripeConfiguration.ApiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; var options = new CustomerCreateOptions { Description = "Example customer", Source = "btok_4XNshPRgmDRCVi", // Get the bank token submitted by the form }; var service = new CustomerService(); var customer = service.Create(options);

Customer bank accounts require verification. When using Stripe without Plaid, Stripe automatically sends two small deposits for this purpose. These deposits take 1-2 business days to appear on the customer’s online statement. The statement has a description that includes AMTS followed by the two microdeposit amounts. Your customer must relay these amounts to you.

When accepting these amounts, be aware that the limit is 10 failed verification attempts. If this limit is exceeded, the bank account can’t be verified. Clear messaging about what these microdeposits are and how you use them can help your customers avoid verification issues. As soon as you have these amounts, you can verify the bank account.

curl https://api.stripe.com/v1/customers/cus_AFGbOSiITuJVDs/sources/ba_17SHwa2eZvKYlo2CUx7nphbZ/verify \ -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \ -d "amounts[]"=32 \ -d "amounts[]"=45
# Set your secret key. Remember to switch to your live secret key in production! # See your keys here: https://dashboard.stripe.com/account/apikeys Stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc' # get the existing bank account bank_account = Stripe::Customer.retrieve_source( 'cus_AFGbOSiITuJVDs', 'ba_17SHwa2eZvKYlo2CUx7nphbZ' ) # verify the account bank_account.verify({amounts: [32, 45]})
# Set your secret key. Remember to switch to your live secret key in production! # See your keys here: https://dashboard.stripe.com/account/apikeys stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc' # get the existing bank account bank_account = stripe.Customer.retrieve_source( 'cus_AFGbOSiITuJVDs', 'ba_17SHwa2eZvKYlo2CUx7nphbZ' ) # verify the account bank_account.verify(amounts=[32, 45])
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys \Stripe\Stripe::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); // get the existing bank account $bank_account = \Stripe\Customer::retrieveSource( 'cus_AFGbOSiITuJVDs', 'ba_17SHwa2eZvKYlo2CUx7nphbZ' ); // verify the account $bank_account->verify(['amounts' => [32, 45]]);
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys Stripe.apiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; // get the existing bank account CustomerRetrieveParams customerParams = CustomerRetrieveParams.builder() .addExpand("sources") .build(); Customer customer = Customer.retrieve( "cus_AFGbOSiITuJVDs", customerParams, null ); BankAccount bankAccount = (BankAccount) customer .getSources() .retrieve("ba_17SHwa2eZvKYlo2CUx7nphbZ"); // verify the account BankAccountVerifyParams params = BankAccountVerifyParams.builder() .addAmount(32L) .addAmount(45L) .build(); bankAccount.verify(params);
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys const stripe = require('stripe')('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); const bankAccount = stripe.customers.verifySource( 'cus_AFGbOSiITuJVDs', 'ba_17SHwa2eZvKYlo2CUx7nphbZ', { amounts: [32, 45], } );
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys stripe.Key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc" params := &stripe.SourceVerifyParams{ Amounts: [2]int64{32, 45}, Customer: stripe.String("cus_AFGbOSiITuJVDs"), } ba, err := paymentsource.Verify("ba_17SHwa2eZvKYlo2CUx7nphbZ", params)
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys StripeConfiguration.ApiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; var options = new BankAccountVerifyOptions { Amounts = new List<long>{32, 45}, }; var service = new BankAccountService(); service.Verify( "cus_AFGbOSiITuJVDs", "ba_17SHwa2eZvKYlo2CUx7nphbZ", options );

When the bank account is verified, you can make charges against it.

Payment authorization

Before creating an ACH charge, get authorization from your customer to debit their account. Doing so ensures compliance with the ACH network and helps protect you from disputes, additional fees, and reversed payments. See our support page for more information on authorization requirements.

Creating an ACH charge

To create a charge on a verified bank account, use the stored Customer object the same way you would when using a card.

curl https://api.stripe.com/v1/charges \ -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \ -d amount=1500 \ -d currency=usd \ -d customer=cus_AFGbOSiITuJVDs
# Set your secret key. Remember to switch to your live secret key in production! # See your keys here: https://dashboard.stripe.com/account/apikeys Stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc' charge = Stripe::Charge.create({ amount: 1500, currency: 'usd', customer: 'cus_AFGbOSiITuJVDs', })
# Set your secret key. Remember to switch to your live secret key in production! # See your keys here: https://dashboard.stripe.com/account/apikeys stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc' charge = stripe.Charge.create( amount=1500, currency='usd', customer='cus_AFGbOSiITuJVDs', )
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys \Stripe\Stripe::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); $charge = \Stripe\Charge::create([ 'amount' => 1500, 'currency' => 'usd', 'customer' => 'cus_AFGbOSiITuJVDs', ]);
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys Stripe.apiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; ChargeCreateParams params = ChargeCreateParams.builder() .setAmount(1500L) .setCurrency("usd") .setCustomer("cus_AFGbOSiITuJVDs") .build(); Charge charge = Charge.create(params);
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys const stripe = require('stripe')('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); const charge = await stripe.charges.create({ amount: 1500, currency: 'usd', customer: 'cus_AFGbOSiITuJVDs', });
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys stripe.Key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc" chargeParams := &stripe.ChargeParams{ Amount: stripe.Int64(1500), Currency: stripe.String(string(stripe.CurrencyUSD)), Customer: stripe.String("cus_AFGbOSiITuJVDs"), } ch, err := charge.New(chargeParams)
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys StripeConfiguration.ApiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; var options = new ChargeCreateOptions { Amount = 1500, Currency = "usd", Customer = "cus_AFGbOSiITuJVDs", }; var service = new ChargeService(); var charge = service.Create(options);

Attempting to charge an unverified bank account results in an error with the message “The customer’s bank account must be verified in order to create an ACH payment.”

If the customer has multiple stored sources (of any type), specify which bank account to use by passing its ID in as the source parameter.

Testing ACH

You can mimic successful and failed ACH charges using the following bank routing and account numbers:

  • Routing number: 110000000
  • Account number:
    • 000123456789 (success)
    • 000111111116 (failure upon use)
    • 000111111113(account closed)
    • 000222222227 (NSF/insufficient funds)
    • 000333333335 (debit not authorized)
    • 000444444440 (invalid currency)

To mimic successful and failed bank account verifications, use these meaningful amounts:

  • [32, 45] (success)
  • [any other number combinations] (failure)

ACH payments workflow

ACH payments take up to 5 business days to receive acknowledgment of their success or failure:

  • When created, ACH charges have the initial status of pending.
  • A pending balance transaction is immediately created reflecting the payment amount, less our fee.
  • Payments created on or after 22:00 UTC are currently processed on the next business day.
  • During the following 4 business days, the payment transitions to either succeeded or failed depending on the customer’s bank.
  • Successful ACH payments are reflected in your Stripe available balance after 7 business days, at which point the funds are available for automatic or manual transfer to your bank account.
  • Failed ACH payments will reverse the pending balance transaction created.
  • Your customer will see the payment reflected on their bank statement 1-2 days after creating the charge. (Your customer will know the payment succeeded before the bank notifies Stripe.)

Failures can happen for a number of reasons, such as insufficient funds, a bad account number, or the customer disabled debits from their bank account.

ACH disputes

Disputes on ACH payments are fundamentally different than those on credit card payments. If a customer’s bank accepts the request to return the funds for a disputed charge, Stripe immediately removes the funds from your Stripe account. Unlike credit card disputes, you can’t contest ACH reversals. You must contact your customer to resolve the situation. The ACH network allows 60 days for consumers to contest a debit. Business accounts only have 2 business days, but because there is no way to be sure if the account is a business or personal account, you should never rely on this as a way to reduce risk.

Risk of double-crediting with ACH refunds and disputes

If you proactively issue your customer a refund while the customer’s bank also initiates the dispute process, your customer may receive two credits for the same transaction.

When issuing a refund for an ACH payment, you must notify your customer immediately that you are issuing the refund and that it may take 2-5 business days for the funds to appear in their bank account.

ACH refunds

You can refund ACH charges using the Refund endpoint, but the timing and risks associated with ACH refunds are different from card refunds. If a refund for an ACH charge fails, you’ll receive a charge.refund.updated notification, which means that we haven’t been able to process the refund. You must return the funds to your customer outside of Stripe. This is rare—normally occurring when an account is frozen between the original charge and the refund request.

ACH-specific webhook notifications

When using ACH, you will receive many of the standard charge webhook notifications, with a couple of notable differences:

  • After creating the charge, you will receive a charge.pending notification. You won’t receive charge.succeeded or charge.failed notification until up to 5 business days later.
  • You will receive a charge.succeeded notification after the charge has transitioned to succeeded and the funds are available in your balance.
  • You will receive a charge.failed notification if the ACH transfer fails for any reason. The charge’s failure_code and failure_message will be set, and the funds will be reversed from your Stripe pending balance at this point.
  • You will receive a customer.source.updated notification when the bank account is properly verified. The bank account’s status will be set to verified.
  • If the bank account couldn’t be verified because either of the two small deposits failed, you will receive a customer.source.updated notification. The bank account’s status will be set to verification_failed.

Connect support

With Connect, your platform can earn money while processing charges. You can either:

curl https://api.stripe.com/v1/charges \ -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \ -d amount=1500 \ -d currency=usd \ -d customer=cus_AFGbOSiITuJVDs \ -d "transfer_data[amount]"=850 \ -d "transfer_data[destination]"="{{CONNECTED_STRIPE_ACCOUNT_ID}}"
# Set your secret key. Remember to switch to your live secret key in production! # See your keys here: https://dashboard.stripe.com/account/apikeys Stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc' charge = Stripe::Charge.create({ amount: 1500, currency: 'usd', customer: customer_id, # Previously stored, then retrieved transfer_data: { amount: 850, destination: '{{CONNECTED_STRIPE_ACCOUNT_ID}}', }, })
# Set your secret key. Remember to switch to your live secret key in production! # See your keys here: https://dashboard.stripe.com/account/apikeys stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc' charge = stripe.Charge.create( amount=1500, currency='usd', customer=customer_id, # Previously stored, then retrieved transfer_data={ 'amount': 850, 'destination': '{{CONNECTED_STRIPE_ACCOUNT_ID}}', }, )
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys \Stripe\Stripe::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); $charge = \Stripe\Charge::create([ 'amount' => 1500, 'currency' => 'usd', 'customer' => $customer_id, // Previously stored, then retrieved 'transfer_data' => [ 'amount' => 850, 'destination' => '{{CONNECTED_STRIPE_ACCOUNT_ID}}', ], ]);
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys Stripe.apiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; ChargeCreateParams params = ChargeCreateParams.builder() .setAmount(1500L) .setCurrency("usd") .setCustomer(customerId) // Previously stored, then retrieved .setTransferData( ChargeCreateParams.TransferData.builder() .setAmount(850L) .setDestination("{{CONNECTED_STRIPE_ACCOUNT_ID}}") .build()) .build(); Charge charge = Charge.create(params);
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys const stripe = require('stripe')('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); const charge = await stripe.charges.create({ amount: 1500, currency: 'usd', customer: customerId, // Previously stored, then retrieved transfer_data: { amount: 850, destination: '{{CONNECTED_STRIPE_ACCOUNT_ID}}', }, });
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys stripe.Key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc" params := &stripe.ChargeParams{ Amount: stripe.Int64(1500), Currency: stripe.String(string(stripe.CurrencyUSD)), Customer: stripe.String(string(customer_id)), TransferData: &stripe.ChargeTransferDataParams{ Amount: stripe.Int64(850), Destination: stripe.String("{{CONNECTED_STRIPE_ACCOUNT_ID}}"), }, } charge, _ := charge.New(params)
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys StripeConfiguration.ApiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; var options = new ChargeCreateOptions { Amount = 1500, Currency = "usd", Customer = customer_id, // previously stored then retrieved TransferData = new ChargeTransferDataOptions { Amount = 850, Destination = "{{CONNECTED_STRIPE_ACCOUNT_ID}}", }, }; var service = new ChargeService(); var charge = service.Create(options);

Services Agreement

Use of the live mode API is subject to the Stripe Services Agreement. Let us know if you have any questions on that agreement.