# Accept an ACH Direct Debit payment
Build a custom payment form or use Stripe Checkout to accept payments with ACH Direct Debit.
# React Native
> #### Use the Accounts v2 API to represent customers
>
> The Accounts v2 API is generally available for Connect users, and in public preview for other Stripe users. If you’re part of the Accounts v2 preview, you need to specify a [preview version](https://docs.stripe.com/api-v2-overview.md#sdk-and-api-versioning) in your code.
>
> To join the Accounts v2 preview, go to [Account previews and features](https://dashboard.stripe.com/settings/previews) in your Dashboard and enable **Reusable payment methods for Global Payouts**.
>
> For most use cases, we recommend [modelling your customers as customer-configured Account objects](https://docs.stripe.com/accounts-v2/use-accounts-as-customers.md) instead of using [Customer](https://docs.stripe.com/api/customers.md) objects.
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](https://docs.stripe.com/payments/payment-intents.md) to track and handle all the states of the payment until the payment completes.
> ACH Direct Debit is a [delayed notification](https://docs.stripe.com/payments/payment-methods.md#payment-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 Stripe [Server-side] [Client-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:
#### Ruby
```bash
# Available as a gem
sudo gem install stripe
```
```ruby
# If you use bundler, you can add this line to your Gemfile
gem 'stripe'
```
### Client-side
The [React Native SDK](https://github.com/stripe/stripe-react-native) is open source and fully documented. Internally, it uses the [native iOS](https://github.com/stripe/stripe-ios) and [Android](https://github.com/stripe/stripe-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):
#### yarn
```bash
yarn add @stripe/stripe-react-native
```
#### npm
```bash
npm install @stripe/stripe-react-native
```
Next, install some other necessary dependencies:
- For iOS, go 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.
> We recommend following the [official TypeScript guide](https://reactnative.dev/docs/typescript#adding-typescript-to-an-existing-project) to add TypeScript support.
### Stripe initialisation
To initialise Stripe in your React Native app, either wrap your payment screen with the `StripeProvider` component, or use the `initStripe` initialisation method. Only the API [publishable key](https://docs.stripe.com/keys.md#obtain-api-keys) in `publishableKey` is required. The following example shows how to initialise Stripe using the `StripeProvider` component.
```jsx
import { useState, useEffect } from 'react';
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 (
{/* Your app code here */}
);
}
```
> Use your API [test keys](https://docs.stripe.com/keys.md#obtain-api-keys) while you test and develop, and your [live mode](https://docs.stripe.com/keys.md#test-live-modes) keys when you publish your app.
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](https://dashboard.stripe.com/financial-connections/application) with [Stripe Financial Connections](https://docs.stripe.com/financial-connections.md).
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 reuse their bank account details across Stripe businesses.
## Create or retrieve a customer [Recommended] [Server-side]
> #### Use the Accounts v2 API to represent customers
>
> The Accounts v2 API is generally available for Connect users, and in public preview for other Stripe users. If you’re part of the Accounts v2 preview, you need to specify a [preview version](https://docs.stripe.com/api-v2-overview.md#sdk-and-api-versioning) in your code.
>
> To join the Accounts v2 preview, go to [Account previews and features](https://dashboard.stripe.com/settings/previews) in your Dashboard and enable **Reusable payment methods for Global Payouts**.
>
> For most use cases, we recommend [modelling your customers as customer-configured Account objects](https://docs.stripe.com/accounts-v2/use-accounts-as-customers.md) instead of using [Customer](https://docs.stripe.com/api/customers.md) objects.
#### Accounts v2
Create a customer-configured [Account](https://docs.stripe.com/api/v2/core/accounts/object.md#v2_account_object-configuration-customer) object when your user creates an account with your business, or retrieve an existing `Account` associated with this user. Associating the ID of the `Account` 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 `Account` to enable Financial Connections’ [return user optimisation](https://docs.stripe.com/financial-connections/fundamentals.md#return-user-optimization).
```curl
curl -X POST https://api.stripe.com/v2/core/accounts \
-H "Authorization: Bearer <>" \
-H "Stripe-Version: 2026-06-24.preview" \
--json '{
"contact_email": "{{CUSTOMER_EMAIL}}"
}'
```
#### Customers v1
Create a *Customer* (Customer objects represent customers of your business. They let you reuse payment methods and give you the ability to track multiple payments) 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 optimisation](https://docs.stripe.com/financial-connections/fundamentals.md#return-user-optimization).
```curl
curl https://api.stripe.com/v1/customers \
-u "<>:" \
-d email={{CUSTOMER_EMAIL}}
```
## Create a PaymentIntent [Server-side]
A [PaymentIntent](https://docs.stripe.com/api/payment_intents.md) 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. Specify the [id](https://docs.stripe.com/api/customers/object.md#customer_object-id) of the Customer.
If you want to reuse the payment method in the future, provide the [setup_future_usage](https://docs.stripe.com/api/payment_intents/create.md#create_payment_intent-setup_future_usage) parameter with the value of `off_session`.
For more information on Financial Connections fees, see [pricing details](https://stripe.com/financial-connections#pricing).
By default, collecting bank account payment information uses [Financial Connections](https://docs.stripe.com/financial-connections.md) to instantly verify your customer’s account, with a fallback option of manual account number entry and microdeposit verification. See the [Financial Connections docs](https://docs.stripe.com/financial-connections/ach-direct-debit-payments.md) to learn how to configure Financial Connections and access additional account data to optimise your ACH integration. For example, you can use Financial Connections to check an account’s balance before initiating the ACH payment.
> To expand access to additional data after a customer authenticates their account, they must re-link their account with expanded permissions.
#### Accounts v2
```curl
curl https://api.stripe.com/v1/payment_intents \
-u "<>:" \
-d amount=1099 \
-d currency=usd \
-d "automatic_payment_methods[enabled]=true" \
-d setup_future_usage=off_session \
-d "customer_account={{CUSTOMERACCOUNT_ID}}"
```
#### Customers v1
```curl
curl https://api.stripe.com/v1/payment_intents \
-u "<>:" \
-d amount=1099 \
-d currency=usd \
-d "automatic_payment_methods[enabled]=true" \
-d setup_future_usage=off_session \
-d "customer={{CUSTOMER_ID}}"
```
### Client-side
A PaymentIntent includes a *client secret* (The client secret is a unique key returned from Stripe as part of a PaymentIntent. This key lets the client access important fields from the PaymentIntent (status, amount, currency) while hiding sensitive ones (metadata, customer)). 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.
```javascript
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: '',
product: '',
}),
});
const {clientSecret} = await response.json();
return clientSecret;
};
return ...;
}
```
## Collect payment method details [Client-side]
Rather than sending the entire PaymentIntent object to the client, use its *client secret* (The client secret is a unique key returned from Stripe as part of a PaymentIntent. This key lets the client access important fields from the PaymentIntent (status, amount, currency) while hiding sensitive ones (metadata, customer)) 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](https://docs.stripe.com/js/payment_intents/collect_bank_account_for_payment) to collect bank account details, create a *PaymentMethod* (PaymentMethods represent your customer's payment instruments, used with the Payment Intents or Setup Intents APIs), 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.
```javascript
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',
paymentMethodData: {
billingDetails: {
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 (
setName(value.nativeEvent.text)}
/>
);
}
```
This loads a modal UI that handles bank account details collection and verification. When it completes, the *PaymentMethod* (PaymentMethods represent your customer's payment instruments, used with the Payment Intents or Setup Intents APIs) is automatically attached to the PaymentIntent.
## Optional: Customise the bank account collector [Client-side]
### Dark mode
By default, the bank account collector automatically switches between light and dark mode compatible colours based on device settings. If your app doesn’t support dark or light mode, you can set `style` to `alwaysLight` or `alwaysDark`, respectively.
```javascript
const {session, error} = await collectBankAccountForPayment(
clientSecret,
{
style: 'alwaysLight',
},
);
```
To achieve an always light or always dark appearance in your Android app, make use of Android’s `AppCompatDelegate`. Our UI automatically respects this setting.
#### Kotlin
```kotlin
// force dark
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
// force light
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
```
## 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. Many payment method types *require* a return URL. If you don’t provide one, we can’t present payment methods that require a return URL to your users, even if you’ve enabled them.
To provide a return URL:
1. [Register](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app#Register-your-URL-scheme) a custom URL. Universal links aren’t supported.
2. [Configure](https://reactnative.dev/docs/linking) your custom URL.
3. Set up your root component to forward the URL to the Stripe SDK as shown below.
> If you’re using Expo, [set your scheme](https://docs.expo.io/guides/linking/#in-a-standalone-app) in the `app.json` file.
```jsx
import { useEffect, useCallback } from 'react';
import { Linking, View } 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 (
);
}
```
For more information on native URL schemes, refer to the [Android](https://developer.android.com/training/app-links/deep-linking) and [iOS](https://developer.apple.com/documentation/xcode/allowing_apps_and_websites_to_link_to_your_content/defining_a_custom_url_scheme_for_your_app) docs.
## Optional: Access data on a Financial Connections bank account [Server-side]
You can only access Financial Connections data if you request additional [data permissions](https://docs.stripe.com/financial-connections/fundamentals.md#data-permissions) when you create your PaymentIntent .
After your customer successfully completes the [Stripe Financial Connections authentication flow](https://docs.stripe.com/financial-connections/fundamentals.md#authentication-flow), the `us_bank_account` PaymentMethod returned includes a [financial_connections_account](https://docs.stripe.com/api/payment_methods/object.md#payment_method_object-us_bank_account-financial_connections_account) ID that points to a [Financial Connections Account](https://docs.stripe.com/api/financial_connections/accounts.md). Use this ID to access account data.
> Bank accounts that your customers link through manual entry and microdeposits don’t have a `financial_connections_account` ID on the Payment Method.
To determine the Financial Connections account ID, retrieve the PaymentIntent and expand the `payment_method` attribute:
```curl
curl -G https://api.stripe.com/v1/payment_intents/{{PAYMENTINTENT_ID}} \
-u "<>:" \
-d "expand[]=payment_method"
```
```json
{
"id": "{{PAYMENT_INTENT_ID}}",
"object": "payment_intent",
// ...
"payment_method": {
"id": "{{PAYMENT_METHOD_ID}}",
// ...
"type": "us_bank_account"
"us_bank_account": {
"account_holder_type": "individual",
"account_type": "checking",
"bank_name": "TEST BANK","financial_connections_account": "{{FINANCIAL_CONNECTIONS_ACCOUNT_ID}}",
"fingerprint": "q9qchffggRjlX2tb",
"last4": "6789",
"routing_number": "110000000"
}
}
// ...
}
```
If you opted to receive `balances` permissions, we recommend checking a balance at this stage to verify sufficient funds before confirming a payment.
Learn more about using additional account data to optimise your ACH integration with [Financial Connections](https://docs.stripe.com/financial-connections/ach-direct-debit-payments.md#optimize).
## Collect mandate acknowledgement and submit the payment [Client-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 authorisation from your customer before you can initiate payment by displaying mandate terms for them to accept. For more information on mandates, see [Mandates](https://docs.stripe.com/payments/ach-direct-debit.md#mandates).
When the customer accepts the mandate terms, you must confirm the PaymentIntent. Use `confirmPayment` to confirm the intent.
```javascript
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 (
setName(value.nativeEvent.text)}
/>
);
}
```
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](https://docs.stripe.com/payments/ach-direct-debit/accept-a-payment.md?platform=react-native#react-native-verify-with-microdeposits) |
| `Processing` | The bank account was instantly verified or verification isn’t necessary. | Step 7: [Confirm the PaymentIntent succeeded](https://docs.stripe.com/payments/ach-direct-debit/accept-a-payment.md?platform=react-native#confirm-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](https://docs.stripe.com/payments/ach-direct-debit.md#mandate-and-microdeposit-emails) if you prefer.
## Verify bank account with microdeposits [Client-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.
```javascript
nextAction: {
type: 'verifyWithMicrodeposits';
redirectUrl: "https://payments.stripe.com/…",
microdepositType: "descriptor_code";
arrivalDate: "1647586800";
}
```
If you supplied a [billing email](https://docs.stripe.com/api/payment_methods/object.md#payment_method_object-billing_details-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.
> 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:
1. Verify the deposits directly in your app by calling `verifyMicrodepositsForPayment` after collecting either the descriptor code or amounts.
```javascript
import { verifyMicrodepositsForPayment } from '@stripe/stripe-react-native';
export default function MyPaymentScreen() {
const [verificationText, setVerificationText] = useState('');
return (
setVerificationText(value.nativeEvent.text)} // Validate and store your user's verification input
/>