PlutoPay Docs
API Reference Dashboard ↗

#ACH bank payments

Accept payments directly from a US bank account (ACH Direct Debit). ACH is fundamentally different from cards:

  • Asynchronous — settles over ~4 business days, not seconds.
  • Verified up front — the customer links + verifies their bank via Financial Connections (instant).
  • Cheaper — Stripe charges ~0.8% (capped at $5) vs card's 2.9%; PlutoPay's 3% platform fee still applies.
  • Late returns — a payment can settle, then be returned days later. Stripe raises those as disputes.

#The two-step flow

The customer links their bank client-side (so bank credentials + the mandate never touch your server), then you create the payment.

1. Client (browser/app)  →  Stripe.js collectBankAccountForPayment()
                             ├─ Financial Connections: customer picks their bank
                             ├─ instant verification
                             └─ NACHA mandate acceptance
                             → a verified us_bank_account payment_method

2. Your server           →  POST /v1/transactions  (payment_method_type=ach)
                             → PaymentIntent in `processing`
                             → settles to `succeeded` (~4 business days) by webhook

Bank linking + mandate collection happen in the browser with Stripe.js, using a client_secret from the ACH payment you create in step 2. This keeps bank credentials and the mandate on Stripe's hosted UI — never on your server.

const { paymentIntent, error } = await stripe.collectBankAccountForPayment({
  clientSecret,
  params: {
    payment_method_type: 'us_bank_account',
    payment_method_data: {
      billing_details: { name: 'Jane Doe', email: 'jane@example.com' },
    },
  },
  expand: ['payment_method'],
});
// The customer picks their bank, verifies instantly, and accepts the ACH
// mandate — all in Stripe's UI. Then confirm:
await stripe.confirmUsBankAccountPayment(clientSecret);

#Step 2 — Create the ACH payment

Server-side, create the payment with payment_method_type: "ach". PlutoPay configures the PaymentIntent for us_bank_account + Financial Connections instant verification and returns a client_secret for step 1.

Field Type Required Description
amount integer Cents. Minimum 50.
payment_method_type string ach.
currency string Defaults to your account currency (usd).
customer_id uuid Recommended — associates the bank for reuse.
payment_method string A pre-verified us_bank_account PM (server-side confirm flow).
mandate object { ip_address, user_agent } for a server-side mandate (only with payment_method).
curl https://plutopayus.com/api/v1/transactions \
  -H "Authorization: Bearer sk_test_your_key_here" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ach_order_2001" \
  -d '{
    "amount": 12000,
    "currency": "usd",
    "payment_method_type": "ach",
    "description": "Invoice #2001",
    "customer_id": "019c8044-2ab5-7396-b5af-216ef14dc582"
  }'

#Response 201

{
  "data": {
    "id": "019f48f7-f24a-72e3-8a0a-f8400fa13667",
    "reference": "txn_HZRiT0thzZ9AJ0qCGVgS",
    "type": "payment",
    "status": "processing",
    "amount": 12000,
    "currency": "usd",
    "payment_method_type": "ach",
    "is_test": true,
    "created_at": "2026-07-05T14:12:03+00:00"
  },
  "client_secret": "pi_3T2..._secret_9Yx..."
}

The payment stays processing until it settles.

#Mandate (NACHA authorization)

US regulation (NACHA) requires the customer to authorize the debit. In the recommended client-side flow, Stripe.js collects the mandate during bank linking. If you confirm server-side (passing a verified payment_method), supply a mandate object (ip_address + user_agent of the customer's acceptance) and show authorization text such as:

By clicking [accept], you authorize [MERCHANT] to debit the bank account specified above for any amount owed for charges arising from your use of [MERCHANT]'s services … until this authorization is revoked. You may amend or cancel this authorization at any time with 30 days notice.

#Async lifecycle

State PlutoPay status Webhook Counts in Gross?
Created processing payment.created No
Submitted, awaiting bank processing payment.processing No
Settled succeeded payment.succeeded Yes
Early failure failed payment.failed No
Late return succeeded + dispute dispute.created Yes (deducted from Net)

While processing, an ACH charge is excluded from Gross Volume — it only counts once it settles to succeeded.

#Late returns are disputes

An ACH payment can settle (succeeded), then be returned by the bank days later (up to 60 days for personal accounts). Stripe raises these as disputes, not failures.

  • Webhook: dispute.created (then dispute.closed with the outcome).
  • PlutoPay records the disputed amount and deducts it from Net Volume, while the charge stays in Gross (it did succeed) — mirroring how refunds work.
  • Won → the amount returns to Net. Lost → the deduction stays; a non-refundable ~$15 dispute fee applies.

Handle dispute.created / dispute.closed in your webhook receiver to keep your books in sync.

#Fees & refunds

Component Rate
Stripe ACH processing ~0.8%, capped at $5
PlutoPay platform fee 3% of the charge (same as card)

Refund a settled ACH payment exactly like a card (POST /v1/transactions/{transaction}/refunds). ACH refunds carry reverse_transfer + refund_application_fee automatically, so the refund cost lands on the correct account and the platform fee is returned.

#Testing

Use test-mode keys and Stripe's ACH test payment methods:

Test payment method Behavior
pm_usBankAccount_success Settles to succeeded.
pm_usBankAccount_processing Stays in processing indefinitely.
pm_usBankAccount_insufficientFunds Early failure → failed.
pm_usBankAccount_accountClosed Early failure → failed.
pm_usBankAccount_dispute Succeeds, then a late return → dispute.created.