Accept an ACH Direct Debit payment
Build a custom payment form or use Stripe Checkout to accept payments with ACH Direct Debit.
Accepting ACH Direct Debit payments in your app consists of:
- Creating an object to track a payment
- Collecting payment method information
- Submitting the payment to Stripe for processing
- Verifying your customer’s bank account
Stripe uses a Payment Intent to track and handle all the states of the payment until the payment completes.
Note
ACH Direct Debit is a delayed notification payment method, which means that funds aren’t immediately available after payment. A payment typically takes 4 business days to arrive in your account.
Set up StripeServer-sideClient-side
Server-side 
This integration requires endpoints on your server that talk to the Stripe API. Use our official libraries for access to the Stripe API from your server:
Client-side 
The React Native SDK is open source and fully documented. Internally, it uses the native iOS and Android SDKs. To install Stripe’s React Native SDK, run one of the following commands in your project’s directory (depending on which package manager you use):
Next, install some other necessary dependencies:
- For iOS, navigate to the ios directory and run
pod install
to ensure that you also install the required native dependencies. - For Android, there are no more dependencies to install.
Stripe initialization
To initialize Stripe in your React Native app, either wrap your payment screen with the StripeProvider
component, or use the initStripe
initialization method. Only the API publishable key in publishableKey
is required. The following example shows how to initialize Stripe using the StripeProvider
component.
import { StripeProvider } from '@stripe/stripe-react-native'; function App() { const [publishableKey, setPublishableKey] = useState(''); const fetchPublishableKey = async () => { const key = await fetchKey(); // fetch key from your server here setPublishableKey(key); }; useEffect(() => { fetchPublishableKey(); }, []); return ( <StripeProvider publishableKey={publishableKey} merchantIdentifier="merchant.identifier" // required for Apple Pay urlScheme="your-url-scheme" // required for 3D Secure and bank redirects > // Your app code here </StripeProvider> ); }
With Stripe, you can instantly verify a customer’s bank account. If you want to retrieve additional data on an account, sign up for data access with Stripe Financial Connections.
Stripe Financial Connections lets your customers securely share their financial data by linking their financial accounts to your business. Use Financial Connections to access customer-permissioned financial data such as tokenized account and routing numbers, balance data, ownership details, and transaction data.
Access to this data helps you perform actions like check balances before initiating a payment to reduce the chance of a failed payment because of insufficient funds.
Financial Connections enables your users to connect their accounts in fewer steps with Link, allowing them to save and quickly reuse their bank account details across Stripe merchants.
Create or retrieve a customerRecommendedServer-side
Create a Customer object when your user creates an account with your business, or retrieve an existing Customer associated with this user. Associating the ID of the Customer object with your own internal representation of a customer enables you to retrieve and use the stored payment method details later. Include an email address on the Customer to enable Financial Connections’ return user optimization.
Create a PaymentIntentServer-side
A PaymentIntent is an object that represents your intent to collect payment from a customer and tracks the lifecycle of the payment process through each stage.
Server-side 
First, create a PaymentIntent on your server and specify the amount to collect and usd
as the currency. If you already have an integration using the Payment Intents API, add us_
to the list of payment method types for your PaymentIntent. Specify the id of the Customer.
If you want to reuse the payment method in the future, provide the setup_future_usage parameter with the value of off_
.
By default, collecting bank account payment information uses Financial Connections to instantly verify your customer’s account, with a fallback option of manual account number entry and microdeposit verification. See the Financial Connections docs to learn how to configure Financial Connections and access additional account data to optimize your ACH integration. For example, you can use Financial Connections to check an account’s balance before initiating the ACH payment.
Note
To expand access to additional data after a customer authenticates their account, they must re-link their account with expanded permissions.
Client-side 
A PaymentIntent includes a client secret. You can use the client secret in your React Native app to securely complete the payment process instead of passing back the entire PaymentIntent object. In your app, request a PaymentIntent from your server and store its client secret.
function PaymentScreen() { // ... const fetchIntentClientSecret = async () => { const response = await fetch(`${API_URL}/create-intent`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ // This is an example request body, the parameters you pass are up to you customer: '<CUSTOMER_ID>', product: '<PRODUCT_ID>', }), }); const {clientSecret} = await response.json(); return clientSecret; }; return <View>...</View>; }
Collect payment method detailsClient-side
Rather than sending the entire PaymentIntent object to the client, use its client secret from the previous step. This is different from your API keys that authenticate Stripe API requests.
Handle the client secret carefully because it can complete the charge. Don’t log it, embed it in URLs, or expose it to anyone but the customer
Use collectBankAccountForPayment to collect bank account details, create a PaymentMethod, and attach that PaymentMethod to the PaymentIntent. You must include the account holder’s name in the billingDetails
parameter to create an ACH Direct Debit PaymentMethod.
import {collectBankAccountForPayment} from '@stripe/stripe-react-native'; export default function MyPaymentScreen() { const [name, setName] = useState(''); const handleCollectBankAccountPress = async () => { // Fetch the intent client secret from the backend. // See `fetchIntentClientSecret()`'s implementation above. const {clientSecret} = await fetchIntentClientSecret(); const {paymentIntent, error} = await collectBankAccountForPayment( clientSecret, { paymentMethodType: 'USBankAccount', payment_method_data: { billing_details: { name: "John Doe", }, }, }, ); if (error) { Alert.alert(`Error code: ${error.code}`, error.message); } else if (paymentIntent) { Alert.alert('Payment status:', paymentIntent.status); if (paymentIntent.status === PaymentIntents.Status.RequiresConfirmation) { // The next step is to call `confirmPayment` } else if ( paymentIntent.status === PaymentIntents.Status.RequiresAction ) { // The next step is to call `verifyMicrodepositsForPayment` } } }; return ( <PaymentScreen> <TextInput placeholder="Name" onChange={(value) => setName(value.nativeEvent.text)} /> <Button onPress={handleCollectBankAccountPress} title="Collect bank account" /> </PaymentScreen> ); }
This loads a modal UI that handles bank account details collection and verification. When it completes, the PaymentMethod is automatically attached to the PaymentIntent.
Set up a return URL (iOS only)Client-side
When a customer exits your app, for example to authenticate in Safari or their banking app, provide a way for them to automatically return to your app afterward. Many payment method types require a return URL, so if you fail to provide it, we can’t present those payment methods to your user, even if you’ve enabled them.
To provide a return URL:
- Register a custom URL. Universal links aren’t supported.
- Configure your custom URL.
- Set up your root component to forward the URL to the Stripe SDK as shown below.
Note
If you’re using Expo, set your scheme in the app.
file.
import React, { useEffect, useCallback } from 'react'; import { Linking } from 'react-native'; import { useStripe } from '@stripe/stripe-react-native'; export default function MyApp() { const { handleURLCallback } = useStripe(); const handleDeepLink = useCallback( async (url: string | null) => { if (url) { const stripeHandled = await handleURLCallback(url); if (stripeHandled) { // This was a Stripe URL - you can return or add extra handling here as you see fit } else { // This was NOT a Stripe URL – handle as you normally would } } }, [handleURLCallback] ); useEffect(() => { const getUrlAsync = async () => { const initialUrl = await Linking.getInitialURL(); handleDeepLink(initialUrl); }; getUrlAsync(); const deepLinkListener = Linking.addEventListener( 'url', (event: { url: string }) => { handleDeepLink(event.url); } ); return () => deepLinkListener.remove(); }, [handleDeepLink]); return ( <View> <AwesomeAppComponent /> </View> ); }
For more information on native URL schemes, refer to the Android and iOS docs.
Collect mandate acknowledgement and submit the paymentClient-side
Before you can initiate the payment, you must obtain authorization from your customer by displaying mandate terms for them to accept.
To comply with Nacha rules, you must obtain authorization from your customer before you can initiate payment by displaying mandate terms for them to accept. For more information on mandates, see Mandates.
When the customer accepts the mandate terms, you must confirm the PaymentIntent. Use confirmPayment
to confirm the intent.
import {confirmPayment} from '@stripe/stripe-react-native'; export default function MyPaymentScreen() { const [name, setName] = useState(''); const handleCollectBankAccountPress = async () => { // See above }; const handlePayPress = async () => { // use the same clientSecret as earlier, see above const {error, paymentIntent} = await confirmPayment(clientSecret, { paymentMethodType: 'USBankAccount', }); if (error) { Alert.alert(`Error code: ${error.code}`, error.message); } else if (paymentIntent) { if (paymentIntent.status === PaymentIntents.Status.Processing) { // The debit has been successfully submitted and is now processing } else if ( paymentIntent.status === PaymentIntents.Status.RequiresAction && paymentIntent?.nextAction?.type === 'verifyWithMicrodeposits' ) { // The payment must be verified with `verifyMicrodepositsForPayment` } else { Alert.alert('Payment status:', paymentIntent.status); } } }; return ( <PaymentScreen> <TextInput placeholder="Name" onChange={(value) => setName(value.nativeEvent.text)} /> <Button onPress={handleCollectBankAccountPress} title="Collect bank account" /> <Button onPress={handlePayPress} title="Pay" /> </PaymentScreen> ); }
If successful, Stripe returns a PaymentIntent object, with one of the following possible statuses:
Status | Description | Next Steps |
---|---|---|
RequiresAction | Bank account verification requires further action. | Step 6: Verifying bank accounts with microdeposits |
Processing | The bank account was instantly verified or verification isn’t necessary. | Step 7: Confirm the PaymentIntent succeeded |
After successfully confirming the PaymentIntent, an email confirmation of the mandate and collected bank account details must be sent to your customer. Stripe handles these by default, but you can choose to send custom notifications if you prefer.
Verify bank account with microdepositsClient-side
Not all customers can verify their bank account instantly. In these cases, Stripe sends a microdeposit to the bank account. This deposit might take up to 1-2 business days to appear on the customer’s online statement. This deposit takes one of two shapes:
- Descriptor code. Stripe sends a single, 0.01 USD microdeposit to the customer’s bank account with a unique, 6-digit
descriptorCode
that starts with SM. Your customer uses this string to verify their bank account. - Amount. Stripe sends two, non-unique microdeposits to the customer’s bank account, with a statement descriptor that reads
ACCTVERIFY
. Your customer uses the deposit amounts to verify their bank account.
If the result of the confirmPayment
method call in the previous step is a PaymentIntent with a requiresAction
status, the PaymentIntent contains a nextAction
field that contains some useful information for completing the verification.
nextAction: { type: 'verifyWithMicrodeposits'; redirectUrl: "https://payments.stripe.com/…", microdepositType: "descriptor_code"; arrivalDate: "1647586800"; }
If you supplied a billing email, Stripe uses this email to notify your customer when we expect the deposits to arrive. The email includes a link to a Stripe-hosted verification page where they can confirm the amounts of the deposits and complete verification.
Warning
Verification attempts have a limit of ten failures for descriptor-based microdeposits and three for amount-based ones. If you exceed this limit, we can no longer verify the bank account. In addition, microdeposit verifications have a timeout of 10 days. If you can’t verify microdeposits in that time, the PaymentIntent reverts to requiring new payment method details. Clear messaging about what these microdeposits are and how you use them can help your customers avoid verification issues.
When you know the payment is in the requiresAction
state and the nextAction
is of type verifyWithMicrodeposits
, you can complete verification of a bank account in two ways:
- Verify the deposits directly in your app by calling
verifyMicrodepositsForPayment
after collecting either the descriptor code or amounts.
import { verifyMicrodepositsForPayment } from '@stripe/stripe-react-native'; export default function MyPaymentScreen() { const [verificationText, setVerificationText] = useState(''); return ( <TextInput placeholder="Descriptor code or comma-separated amounts" onChange={(value) => setVerificationText(value.nativeEvent.text)} // Validate and store your user's verification input /> <Button title="Verify microdeposit" onPress={async () => { const { paymentIntent, error } = await verifyMicrodepositsForPayment(secret, { // Provide either the descriptorCode OR amounts, not both descriptorCode: verificationText, amounts: verificationText, }); if (error) { Alert.alert(`Error code: ${error.code}`, error.message); } else if (paymentIntent) { Alert.alert('Payment status:', paymentIntent.status); } }} /> ); }
- Use the Stripe-hosted verification page that we automatically provide for you. To do this, use the
nextAction[redirectUrl]
URL in thenextAction
object (see above) to direct your customer to complete the verification process.
const {error, paymentIntent} = await confirmPayment(clientSecret, { paymentMethodType: 'USBankAccount', }); if (error) { Alert.alert(`Error code: ${error.code}`, error.message); } else if (paymentIntent) { if ( paymentIntent.status === PaymentIntents.Status.RequiresAction && paymentIntent?.nextAction?.type === 'verifyWithMicrodeposits' ) { // Open the Stripe-hosted verification page Linking.openURL(paymentIntent.nextAction.redirectUrl); } }
When the bank account is successfully verified, Stripe returns the PaymentIntent object with a status
of Processing
, and sends a payment_intent.processing webhook event.
Verification can fail for several reasons. The failure might happen synchronously as a direct error response, or asynchronously through a payment_intent.payment_failed webhook event (shown in the following examples).
Error Code | Synchronous or Asynchronous | Message | Status change |
---|---|---|---|
payment_ | Synchronously, or asynchronously through webhook event | Microdeposits failed. Please check the account, institution and transit numbers provided | status is requires_ , and last_ is set. |
payment_ | Synchronously | The amounts provided don’t match the amounts that we sent to the bank account. You have {attempts_remaining} verification attempts remaining. | Unchanged |
payment_ | Synchronously, or asynchronously through webhook event | Exceeded the number of allowed verification attempts | status is requires_ , and last_ is set. |
payment_ | Asynchronously through a webhook event | Microdeposit timeout. The customer hasn’t verified their bank account within the required 10 day period. | status is requires_ , and last_ is set. |
Confirm the PaymentIntent succeededServer-side
ACH Direct Debit is a delayed notification payment method. This means that it can take up to four business days to receive notification of the success or failure of a payment after you initiate a debit from your customer’s account.
The PaymentIntent you create initially has a status of processing
. After the payment has succeeded, the PaymentIntent status is updated from processing
to succeeded
.
We recommend using webhooks to confirm the charge has succeeded and to notify the customer that the payment is complete. You can also view events on the Stripe Dashboard.
Test your integration
Learn how to test scenarios with instant verifications using Financial Connections.
Send transaction emails in a sandbox
After you collect the bank account details and accept a mandate, send the mandate confirmation and microdeposit verification emails in a sandbox. To do this, provide an email in the payment_
field in the form of {any-prefix}+test_
when you collect the payment method details.
Common mistake
You need to activate your Stripe account before you can trigger these emails while testing.
Test account numbers
Stripe provides several test account numbers and corresponding tokens you can use to make sure your integration for manually-entered bank accounts is ready for production.
Account number | Token | Routing number | Behavior |
---|---|---|---|
000123456789 | pm_ | 110000000 | The payment succeeds. |
000111111113 | pm_ | 110000000 | The payment fails because the account is closed. |
000111111116 | pm_ | 110000000 | The payment fails because no account is found. |
000222222227 | pm_ | 110000000 | The payment fails due to insufficient funds. |
000333333335 | pm_ | 110000000 | The payment fails because debits aren’t authorized. |
000444444440 | pm_ | 110000000 | The payment fails due to invalid currency. |
000666666661 | pm_ | 110000000 | The payment fails to send microdeposits. |
000555555559 | pm_ | 110000000 | The payment triggers a dispute. |
000000000009 | pm_ | 110000000 | The payment stays in processing indefinitely. Useful for testing PaymentIntent cancellation. |
000777777771 | pm_ | 110000000 | The payment fails due to payment amount causing the account to exceed its weekly payment volume limit. |
Before test transactions can complete, you need to verify all test accounts that automatically succeed or fail the payment. To do so, use the test microdeposit amounts or descriptor codes below.
Test microdeposit amounts and descriptor codes
To mimic different scenarios, use these microdeposit amounts or 0.01 descriptor code values.
Microdeposit values | 0.01 descriptor code values | Scenario |
---|---|---|
32 and 45 | SM11AA | Simulates verifying the account. |
10 and 11 | SM33CC | Simulates exceeding the number of allowed verification attempts. |
40 and 41 | SM44DD | Simulates a microdeposit timeout. |
Test settlement behavior
Test transactions settle instantly and are added to your available test balance. This behavior differs from livemode, where transactions can take multiple days to settle in your available balance.