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, merely 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.

    Once both steps have been taken for a bank account, it can mostly be used just like any other payment method, 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 will 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 via Plaid or collection via Stripe.js with delayed-verification using microdeposits. There may be additional costs when using Plaid, depending on the size of your business. Make sure to 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.

    To get started, you first need to set up a Plaid account and connect it to your Stripe account.

    Integrating Plaid

    Once your Plaid account is set up and connected to your Stripe account, add Plaid Link to your site.

    <button id='linkButton'>Open Plaid Link</button>
    <script src="https://cdn.plaid.com/link/v2/stable/link-initialize.js"></script>
    <script>
    var linkHandler = Plaid.create({
      env: 'sandbox',
      clientName: 'Stripe/Plaid Test',
      key: '[Plaid key]',
      product: ['auth'],
      selectAccount: true,
      onSuccess: function(public_token, metadata) {
        // Send the public_token and account ID to your app server.
        console.log('public_token: ' + public_token);
        console.log('account ID: ' + metadata.account_id);
      },
      onExit: function(err, metadata) {
        // The user exited the Link flow.
        if (err != null) {
          // The user encountered a Plaid API error prior to exiting.
        }
      },
    });
    
    // Trigger the Link UI
    document.getElementById('linkButton').onclick = function() {
      linkHandler.open();
    };
    </script>
    

    The above code uses a custom Plaid Link integration. You’ll need to replace the clientName with something meaningful to your business and the key value with your public Plaid key. An env value of sandbox lets you test with simulated data and development lets you test with live users; use production when it’s time to go live.

    Once your server has the public_token, you will need to make two calls to the Plaid server to get the Stripe bank account token along with the Plaid access_token that can be used 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}" \
      -d secret="{PLAID_SECRET}" \
      -d 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}" \
      -d secret="{PLAID_SECRET}" \
      -d access_token="{PLAID_ACCESS_TOKEN}" \
      -d 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;
      });
    });
    

    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, you’ll use Plaid’s Production environment:

    curl https://production.plaid.com/item/public_token/exchange \
      -H Content-Type="application/json" \
      -d client_id="{PLAID_CLIENT_ID}" \
      -d secret="{PLAID_SECRET}" \
      -d public_token="{PLAID_LINK_PUBLIC_TOKEN}"
    
    curl https://production.plaid.com/processor/stripe/bank_account_token/create \
      -H Content-Type="application/json" \
      -d client_id="{PLAID_CLIENT_ID}" \
      -d secret="{PLAID_SECRET}" \
      -d access_token="{PLAID_ACCESS_TOKEN}" \
      -d account_id="{PLAID_ACCOUNT_ID}"
    
    # Using Plaid's Ruby bindings (https://github.com/plaid/plaid-ruby)
    client = Plaid::Client.new(env: :production,
                      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}',
                    'production')
    
    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://production.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://production.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}")
      .productionBaseUrl() // Use the Production API URL
      .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.production);
    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;
      });
    });
    

    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_j6s1gd8xzAVfBBOabKdo",
      "request_id": "[Unique request ID]"
    }

    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 do not wish 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. Once you have that, attach it to a Stripe customer in your account.

    curl https://api.stripe.com/v1/customers \
      -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \
      -d source=btok_4XNshPRgmDRCVi \
      -d description="Example customer"
    
    # Set your secret key: remember to change this 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({
      source: token_id,
      description: 'Example customer',
    })
    
    # Set your secret key: remember to change this 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(
        source=token_id,
        description="Example customer"
    )
    
    // Set your secret key: remember to change this 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([
      "source" => $token_id,
      "description" => "Example customer"
    ]);
    
    // Set your secret key: remember to change this 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
    Map<String, Object> customerParams = new HashMap<String, Object>();
    customerParams.put("source", tokenID);
    customerParams.put("description", "Example customer");
    
    Customer customer = Customer.create(customerParams);
    
    // Set your secret key: remember to change this 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
    stripe.customers.create({
      source: tokenID,
      description: "Example customer"
    }, function(err, customer) {
    
    });
    

    After adding the bank account to a customer, it needs to be verified. When using Stripe without Plaid, verification is done via two small deposits into the bank account that Stripe will automatically send. These deposits will take 1-2 business days to appear on the customer’s online statement. The statement description for these deposits will be AMTS: and then the values of the two microdeposits that were sent. Your customer will need to relay the value of the two deposits to you.

    When accepting these values, be sure to note that there is a limit of 10 failed verification attempts. Once this limit has been crossed, the bank account will be unable to be verified. Careful messaging about what these microdeposits are and how they are used can help your end customers avoid this issue. Once you have these values, 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 change this 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 change this 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 change this 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 change this 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
    Customer customer = Customer.retrieve("cus_AFGbOSiITuJVDs");
    ExternalAccount source = customer.getSources().retrieve("ba_17SHwa2eZvKYlo2CUx7nphbZ");
    
    // verify the account
    Map params = new HashMap<String, Object>();
    ArrayList amounts = new ArrayList();
    amounts.add(32);
    amounts.add(45);
    params.put("amounts", amounts);
    source.verify(params);
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    const stripe = require('stripe')('sk_test_4eC39HqLyjWDarjtT1zdp7dc');
    
    var data = {amounts: [32,45]}
    stripe.customers.verifySource(
      'cus_AFGbOSiITuJVDs',
      'ba_17SHwa2eZvKYlo2CUx7nphbZ',
      {
        amounts: [32, 45],
      },
      function(err, bankAccount) {
        // asynchronously called
      }
    );
    

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

    Payment authorization

    Before creating an ACH charge, it is important that you’ve received 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, simply 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 change this 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
    })
    
    # Set your secret key: remember to change this 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
    )
    
    // Set your secret key: remember to change this 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
    ]);
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.apiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc";
    
    Map<String, Object> params = new HashMap<String, Object>();
    params.put("amount", 1500);
    params.put("currency", "usd");
    params.put("customer", customerId); // Previously stored, then retrieved
    
    Charge charge = Charge.create(params);
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    const stripe = require('stripe')('sk_test_4eC39HqLyjWDarjtT1zdp7dc');
    
    stripe.charges.create({
      amount: 1500,
      currency: 'usd',
      customer: customerId, // Previously stored, then retrieved
    }).then(function(charge) {
      // asynchronously called
    });
    

    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 will have the initial status of pending.
    • A pending balance transaction will immediately be created reflecting the payment amount, less our fee.
    • Payments created after 21:00 UTC are currently processed on the next business day.
    • During the following 4 business days, the payment will either transition to succeeded or failed depending on the customer’s bank.
    • Successful ACH payments will be 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 the credit card payments. If a customer disputes a charge and the customer’s bank accepts the request to return the funds, Stripe immediately removes the funds for the charge from your Stripe account. Unlike credit card disputes, you are not able to contest ACH reversals. You will need to directly reach out to your customer to resolve the situation.

    The ACH network allows 60 days for consumers to contest a debit on their account. Business accounts only have 2 business days, but since there is no way to be sure that the end account is a business account 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

    Like other payment methods, ACH charges can be refunded using the Refund endpoint. However, the timing and risks associated with ACH refunds are different than card refunds. To learn more about those differences, please refer to our support guide.

    If a refund for an ACH charge fails, you will receive a charge.refund.updated notification. This means that we have been unable to process the refund, and 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 will not receive charge.succeeded or charge.failed notification until up to 5 business days later.
    • You will receive a charge.succeeded notification once 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 could not 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 change this 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 change this 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 change this 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 change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.apiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc";
    
    Map<String, Object> params = new HashMap<String, Object>();
    params.put("amount", 1500);
    params.put("currency", "usd");
    params.put("customer", customerId); // Previously stored, then retrieved
    Map<String, Object> transferDataParams = new HashMap<String, Object>();
    transferDataParams.put("amount", 850);
    transferDataParams.put("destination", "{{CONNECTED_STRIPE_ACCOUNT_ID}}");
    params.put("transfer_data", transferDataParams);
    
    Charge charge = Charge.create(params);
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    const stripe = require('stripe')('sk_test_4eC39HqLyjWDarjtT1zdp7dc');
    
    stripe.charges.create({
      amount: 1500,
      currency: 'usd',
      customer: customerId, // Previously stored, then retrieved
      transfer_data: {
        amount: 850,
        destination: '{{CONNECTED_STRIPE_ACCOUNT_ID}}',
      },
    }).then(function(charge) {
      // asynchronously called
    });
    

    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.

    On this page