Skip to main content
You can use PayPal’s JavaScript SDK v6 to accept Automated Clearing House (ACH) Direct Debit payments with fully integrated instant bank account verification.

Prerequisites

  1. Ensure you have a PayPal business account approved for Expanded Checkout.
  2. Ensure to set up developer, sandbox, and production environment accounts.
  3. Ensure to complete the account provisioning process to enable ACH payments.

Load PayPal JavaScript SDK

PayPal’s JavaScript SDK provides the necessary pre-built tools to render the Pay with Bank (ACH) button on your webpage and handle payment authorization. Include the JavaScript SDK as a <script> tag in the HTML file that renders your webpage.
<script src="https://www.paypal.com/web-sdk/v6/core"></script>

Load and use JavaScript SDK v6 along with v5

If you have an existing JavaScript SDK v5 integration and want to integrate JavaScript SDK v6 on the same page, add the data-namespace attribute to your existing v5 <script> tag.
<script
    src="https://www.paypal.com/sdk/js?client-id=test"
    data-namespace="paypal_v5">
</script>
This ensures that:
  • You access the v5 SDK via window.paypal_v5 instead of window.paypal.
  • JavaScript SDK v6 can attach to window.paypal without naming collisions.
The following example shows how to use both SDKs on the same page:
<!-- JavaScript SDK v5 -->
<script
    src="https://www.paypal.com/sdk/js?client-id=test"
    data-namespace="paypal_v5">
</script>

<!-- JavaScript SDK v6 -->
<script
    src="https://www.paypal.com/web-sdk/v6/core">
</script>

<script>
    // Access v5 SDK via the namespace "window.paypal_v5"
    window.paypal_v5.Buttons().render('#v5-button-container');

    // Access v6 SDK via default namespace "window.paypal"
    window.paypal.createInstance({ clientToken })
</script>

Create browser-safe client token

The browser-safe token is a client-side access token that authorizes an app to use the JavaScript SDK resources.
A browser-safe token is not the same as a server-side access token. The server-side access token helps PayPal authenticate an app when the app accesses PayPal REST API resources.
To get the browser-safe client token from your server-side code, make a POST call to the /v1/oauth2/token endpoint and include the following:
  • Encoded app credentials Client ID : Secret in Base64 format, in the Authorization header.
  • Data parameters:
ParameterAction
grant_typeSet the parameter value to client_credentials to specify that the app is requesting to exchange client ID and secret for an access token.
response_typeSet the parameter value to client_token to request a client-side access token.
Response: Contains the browser-safe client token in the access_token response parameter.
  • Merchants
  • Partners
// Get OAuth token using client credentials
async function getClientToken() {
  try {
    const response = await fetch("/your-server/api/paypal/browser-safe-client-token");
    const data = await response.json();

    if (!response.ok) {
      throw new Error(data.error || "Failed to fetch client token");
    }

    return data.access_token;
  } catch (error) {
    console.error("Client token error:", error);
    throw error;
  }
}

Initialize JavaScript SDK and create SDK instance

In your client-side code, use the window.paypal.createInstance() method with the following options to create a PayPal SDK instance:
OptionAction
clientTokenPass the browser-safe client token.
componentsSet to ["bank-ach-payments"] to load and render ACH components.
partnerAttributionId [Partner-specific attribute]Set to your BN code to identify as a PayPal partner. See BN code for more information.
Response: Contains an SDK instance object with the createBankAchOneTimePaymentSession() method for creating ACH payment sessions and the findEligibleMethods() method for checking payment method availability.
  • Merchants
  • Partners
const clientToken = await getClientToken();

const sdkInstance = await window.paypal.createInstance({
  clientToken,
  components: ["bank-ach-payments"],
});
window.sdkInstance = sdkInstance;

Verify eligibility

In your client-side code, use the following methods to determine if you are eligible to offer ACH as a payment method:
  • findEligibleMethods(): Returns all the eligible payment methods.
  • isEligible(): Indicates if ACH is an eligible payment method.
PayPal performs a risk assessment on each transaction. The eligibility to render the Pay with Bank - ACH button depends on the risk profile.
  • Merchants
  • Partners
Call findEligibleMethods() with an input options object containing the following parameters:
ParameterAction
currencyCodeSet to the three-letter currency code. Example: USD.
paymentFlowSet to ONE_TIME_PAYMENT. JavaScript SDK retrieves all payment methods through which the merchant is eligible to accept one-time payments.
amountSet to the total order amount that includes item cost, applicable taxes, shipping costs, handling cost, insurance cost, and discounts if any. Example: 100.00.
Response: Contains an object with the isEligible() method. Use the method to verify if the eligible payment methods include ACH.
const options = {
  currencyCode: "USD",
  paymentFlow: "ONE_TIME_PAYMENT",
  amount: "100.00"
};

// Query for available payment methods that support one-time payments
const paymentMethods = await sdkInstance.findEligibleMethods(options);

// Check if ACH is one of the available payment methods
const isAchEligible = paymentMethods.isEligible("ach");

Render Pay with Bank button

After confirming eligibility, render the Pay with Bank - ACH button on your webpage:
  1. Define the container for the ACH button: In the HTML file corresponding to the webpage where you want to render the button, include a container element.
<section class="payment-method-container" id="bank-ach-section">
  <h2>Pay with Bank</h2>
  <!-- BankAchButton web component launches the popup flow -->
  <bank-ach-button id="paypal-bank-ach-button" hidden></bank-ach-button>
</section>
  1. Create payment session: Use sdkInstance.createBankAchOneTimePaymentSession() to create a payment session and register the onApprove(), onCancel(), onComplete(), and onError() event handlers.
  2. Attach the onClick event handler and display the button: Use addEventListener() to attach the onClick event handler that triggers the onClick function when customers select the Pay with Bank - ACH button.
// isAchEligible returns true if ACH payments are eligible for your account.

if (isAchEligible) {
  // Step 2: Create payment session
  const bankAchSession = sdkInstance.createBankAchOneTimePaymentSession({
    onApprove,
    onCancel,
    onComplete,
    onError,
  });

  // Step 3: Attach the onClick event handler and display the button
  const bankButton = document.querySelector("#paypal-bank-ach-button");
  bankButton.addEventListener("click", onClick);
  bankButton.removeAttribute("hidden");
} else {
  console.log("ACH is not eligible");
}

Create order and start authentication flow

Create an onClick event-handler function that:
  • Starts the authentication flow.
  • Creates an order and passes the order ID to JavaScript SDK.
  • Merchants
  • Partners
In your client-side code:
  1. Include an event-handler function that is triggered when the user clicks the Pay with Bank - ACH button. The function calls the createOrder() function, receives the order ID from the server-side code, passes it to the JavaScript SDK, and starts the authentication (account verification) flow.
JavaScript SDK uses the order ID to confirm the payment source as bank.ach_debit to PayPal servers.
  1. Include the createOrder() function that constructs a payload, calls the server-side code to create an order, and passes the payload. Include the following parameters in the payload:
ParameterAction
intentSet to CAPTURE to capture payment immediately after approval.
purchase_units[].amount.currency_codeSet to the three-letter currency code. Example: USD.
purchase_units[].amount.valueSet to the total order amount that includes item cost, applicable taxes, shipping costs, handling cost, insurance cost, and discounts if any. Example: 100.00.

If there is a mismatch between purchase_units[].amount.value and the amount specified in the findEligibleMethods() method, PayPal considers purchase_units[].amount.value as the total order amount.
In your server-side code, include the code to:
  1. Use a valid full-scope Bearer access token, make a POST call to the /v2/checkout/orders endpoint, and pass the payload received from client-side.
  2. Receive the response to the Create Order call. The response contains the order ID.
  3. Pass the order ID to the client-side code.
async function onClick() {
  try {
    console.log("Starting bank account authentication...");
    const startOptions = {
          presentationMode: "popup",
    };

    const checkoutOptionsPromise = createOrder().then((id) => ({
          orderId: id,
    }));

    // Start the pre-configured authentication flow
    await bankAchSession.start(startOptions, checkoutOptionsPromise);
  } catch (error) {
    console.error("Error starting ACH flow:", error);
  }
}

async function createOrder() {
  console.log("Creating order...");

  const orderPayload = {
    intent: "CAPTURE",
    purchase_units: [
      {
        amount: {
          currency_code: "USD",
          value: "100.00",
        },
      },
    ],
  };

  try {
    const response = await fetch(
      "/your-server/api/paypal/order",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(orderPayload),
      },
    );

    const data = await response.json();

    if (!response.ok) {
      throw new Error(data.message || "Failed to create order");
    }

    console.log(`Order created: ${data.id}`);
    return data.id;
  } catch (error) {
    console.error("Error creating order:", error);
    throw error;
  }
}

Handle events

Event handlers manage the different outcomes when your customers attempt to make ACH payments. In your code, create event-handler functions that handle customer approval for payment, payment cancellation, error scenarios, and payment capture completion.

a. Handle approval and capture payment

In your client-side code:
  1. Include the onApprove() event-handler function that receives the order ID after the customer successfully verifies their bank account, calls the captureOrder() function, and passes the order ID.
  2. Include the captureOrder() function that:
    • Receives the order ID from the onApprove() event.
    • Calls the server-side code to capture the payment.
    • Receives the captured order details from the server-side code.
    • Displays the transaction results to the customer.
  • Merchants
  • Partners
In your server-side code, include the code to:
  1. Use a valid full-scope Bearer access token and make a POST call to the /v2/checkout/orders/{order_id}/capture endpoint with the order ID received from client-side as the path parameter.
  2. Receive the captured order details from the PayPal server and pass them to the client-side code.
async function onApprove(data) {
  try {
    console.log("Processing ACH payment capture...");
    console.log(`Capturing order ${data.orderId}...`);

    // Capture the order
    const orderData = await captureOrder({
      orderId: data.orderId,
    });

    console.log("✅ Payment captured successfully");
    console.log("Capture result:", orderData);

    // Display transaction details to the customer
    console.log(`Transaction ID: ${orderData.purchase_units[].payments.captures[].id}`);
    console.log(`Status: ${orderData.status}`);
  } catch (error) {
    console.error("❌ Error capturing payment:", error.message);
  }
}

async function captureOrder({ orderId }) {
  console.log(`Capturing order ${orderId}`);

  try {
    const response = await fetch(
      `/your-server/api/paypal/order/${orderId}`,
      {
        method: "PUT",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({}),
      },
    );

    const data = await response.json();

    if (!response.ok) {
      throw new Error(data.message || "Failed to capture order");
    }

    return data;
  } catch (error) {
    console.error("Error capturing order:", error);
    throw error;
  }
}

b. Handle cancellation

In your app code, include the onCancel() event-handler function that handles payment cancellation. Payment cancellation can occur when the customer cancels account verification or payment processing. JavaScript SDK passes the cancellation details including the order ID to the function’s data parameter.
function onCancel(data) {
  console.log("ACH Payment Cancelled!", data);
  let message = "Customer cancelled the bank account payment process";
  if (data && data.orderId) {
    message += ` for order ${data.orderId}`;
  }
  console.log(message);
}

c. Handle errors

In your app code, include the onError(data) event-handler function to process errors that occur during bank account verification or payment process. JavaScript SDK passes error message and error details to the function’s data parameter.
function onError(data) {
  console.log("ACH Payment Error:", data);
  console.error(`Error occurred during bank account payment: ${data.message || data || 'Unknown error'}`);
}

d. [Optional] Handle completion

In your app code, you can include the onComplete(data) event-handler function to perform necessary clean-up operations.
function onComplete(data) {
  console.log("ACH Payment Complete:", data);
  console.log("Bank account payment flow completed");
}

Test and go live

Test the end-to-end integration flow
  1. Load your integration page and verify ACH eligibility checking works correctly.
  2. Click the Pay with Bank button to start the bank account authentication process.
  3. Verify the order is created successfully when the authentication flow begins as documented in the Create order and start authentication flow section.
  4. Complete the authentication flow and verify the onApprove callback receives the order ID as documented in the Handle approval and capture payment section. When the bank selection popup appears, use the following sandbox test credentials:
    • Bank selection: Select Demo Bank from the list of available banks.
    • Username: pyplopenbankingubsb.site16441.2credential
    • Password: site16441.2
  5. Confirm your server successfully captures the payment using the order ID as documented in the Handle approval and capture payment section.
  6. Verify the authentication flow starts correctly with order creation as documented in the Create order and start authentication flow section.
Test error handling
  1. Cancel the authentication process and verify the onCancel callback executes with the order ID as documented in the Handle cancellation section.
  2. Test error scenarios and verify the onError callback handles failures appropriately as documented in the Handle errors section.
  3. Confirm the onComplete callback executes when the flow finishes as documented in the Handle completion section.
  4. Test idempotent retry logic for 5xx errors during order capture as documented in the Handle approval and capture payment section.
Go live
  1. Switch from sandbox to production API endpoints:
    • Change SDK URL from https://www.sandbox.paypal.com/web-sdk/v6/core to https://www.paypal.com/web-sdk/v6/core.
    • Update API base URL from https://api-m.sandbox.paypal.com to https://api-m.paypal.com.
  2. Replace sandbox credentials with production credentials:
    • Use your production client ID and secret.
    • Ensure your production PayPal business account is approved for Expanded Checkout and ACH payments.
  3. Deploy and test your production integration to confirm everything works correctly.