Prompt

How do I set up ACH payments API workflows for recurring invoicing and return handling?

Technology / Vertical SaaS — Fintech2 observationsLast seen Oct 18, 2026

Latest observation

Oct 18, 2026OpenAI APIWeb search: off

To set up ACH payments API workflows for recurring invoicing and return handling, think in terms of two parallel flows:

  1. Forward flow: create invoice → collect bank account → authorize debit → schedule/charge → mark paid
  2. Exception flow: if the ACH debit returns, detect it → classify reason → notify customer → retry or fail invoice → reconcile ledger

Here’s a practical blueprint.


1) Core ACH workflow for recurring invoices

A. Collect and store customer authorization

Before you can debit ACH, you generally need:

  • customer bank account details
  • explicit authorization for recurring debits
  • signed mandate/authorization language
  • proof of authorization stored in your system

Typical setup:

  • Customer enters routing + account number via your hosted bank-link form or API.
  • You verify the bank account if required.
  • You store a tokenized payment method reference, not raw bank data.
  • You store mandate metadata:
    • customer name
    • authorization timestamp
    • authorization text/version
    • recurring billing terms
    • IP/device metadata if applicable

B. Create the customer and payment method objects

In your billing system, keep entities like:

  • customer
  • bank_account_payment_method
  • mandate
  • invoice
  • payment_attempt
  • ach_return
  • retry_policy

C. Generate invoices on schedule

For recurring billing:

  • create invoice from subscription schedule
  • calculate amount, taxes, discounts, and due date
  • decide whether to auto-debit on invoice finalization or on due date

Common patterns:

  • Auto-charge immediately on invoice creation
  • Net terms: invoice is open first, then debit on due date
  • Pre-notification: send advance notice before debit, if required

D. Initiate ACH debit

Your API call should generally:

  • reference the invoice
  • reference the stored bank payment method
  • include amount, currency, descriptor, and effective date
  • mark it as “pending” since ACH is asynchronous

Example logical flow:

  1. POST /invoices
  2. POST /payments/ach_debits with invoice_id
  3. Payment enters pending_submission
  4. Once cleared, mark invoice paid
  5. If returned, mark payment returned and invoice open or past_due

E. Handle asynchronous settlement

ACH is not instant. You need webhooks or event polling for:

  • submitted
  • accepted
  • settled
  • returned
  • reversed
  • canceled

Do not mark the invoice paid until settlement confirmation unless your business is willing to absorb return risk.


2) Recommended recurring billing workflow

A strong recurring ACH workflow usually looks like this:

Step 1: Subscription renews

  • Scheduler creates invoice for period.
  • Invoice status: draft

Step 2: Finalize invoice

  • Freeze amounts.
  • Send invoice or reminder.
  • If authorization allows, create ACH debit automatically.

Step 3: Submit debit

  • Create ACH payment attempt.
  • Status: pending

Step 4: Receive payment outcome

  • If successful: invoice paid
  • If failure/return: invoice open, past_due, or collection_pending

Step 5: Retry or escalate

  • Retry based on reason code and policy.
  • Notify customer.
  • Optionally suspend service after X days or failed attempts.

3) Return handling workflow

ACH returns are a normal part of bank debit processing. You need a robust return handler.

A. Listen for return events

Your payment provider will usually send webhooks like:

  • ach.return.created
  • payment.returned
  • payment.failed
  • chargeback.created or equivalent

Store:

  • return code
  • return date
  • original payment ID
  • bank trace number if available
  • settlement status

B. Classify the return reason

Common ACH return categories:

  • R01: Insufficient funds
  • R02: Account closed
  • R03: No account/unable to locate
  • R04: Invalid account number
  • R05: Unauthorized debit to consumer account
  • R06: Returned per ODFI request
  • R07: Authorization revoked
  • R08: Payment stopped
  • R09: Uncollected funds
  • R10: Customer advises not authorized
  • R20: Non-transaction account
  • R29: Corporate customer advises not authorized
  • R31: Permissible return entry

Your response should depend on code:

  • NSF / temporary issues: retry after delay
  • Invalid/closed account: require new bank details
  • Unauthorized/revoked: stop debits immediately and investigate
  • Stop payment: contact customer and pause recurrence

C. Update invoice and ledger

When a return occurs:

  • reverse the payment application against the invoice
  • reopen invoice balance
  • post accounting entries for returned item fees if applicable
  • adjust revenue recognition if you had already recognized it

D. Notify customer and internal teams

Send:

  • customer email/SMS about failed debit
  • support ticket if unauthorized or suspicious
  • finance notification if return fee or multiple returns

E. Retry policy

For ACH returns, do not blindly retry. Common best practice:

  • retry only for NSF-related returns
  • wait several business days
  • limit number of retries
  • change retry timing to align with payday patterns if allowed

Avoid retries for:

  • revoked authorization
  • closed/invalid accounts
  • stop payment
  • unauthorized returns

4) Event-driven architecture

A clean implementation is event-driven:

Event types

  • invoice.created
  • invoice.finalized
  • ach.debit.submitted
  • ach.debit.settled
  • ach.debit.returned
  • customer.mandate.revoked

Services

  • Billing service: invoices/subscriptions
  • Payments service: ACH debits and bank tokens
  • Webhook handler: idempotent processing of provider events
  • Ledger service: accounting entries
  • Notification service: customer emails/SMS
  • Risk/compliance service: returns, unauthorized patterns, AML/KYC rules

Key design rules

  • Use idempotency keys for debit creation
  • Store provider event IDs to avoid double-processing
  • Make webhook handlers retry-safe
  • Keep a full audit log for mandate and debit actions

5) Example workflow states

Invoice states

  • draft
  • open
  • pending_payment
  • paid
  • past_due
  • void
  • collection

ACH payment states

  • created
  • pending_submission
  • submitted
  • settled
  • returned
  • reversed
  • canceled

Mandate states

  • active
  • revoked
  • expired
  • superseded

6) Suggested API endpoints

If you’re designing your own system, endpoints might look like:

  • POST /customers
  • POST /customers/{id}/bank-accounts
  • POST /mandates
  • POST /subscriptions
  • POST /invoices
  • POST /invoices/{id}/finalize
  • POST /ach-payments
  • GET /ach-payments/{id}
  • POST /webhooks/ach
  • POST /returns/{return_id}/resolve
  • POST /payments/{id}/retry

7) Important compliance considerations

ACH is regulated. Make sure you handle:

  • customer authorization and retention
  • NACHA rules and return timelines
  • prenote/pre-notification rules if relevant
  • SEC code selection for consumer vs corporate debits
  • handling of unauthorized returns
  • data security for bank account info
  • audit trails and access controls

If you operate in the US, NACHA compliance is especially important.


8) Practical retry strategy

A simple policy:

  • R01 / R09: retry once after 3–5 business days
  • R02 / R03 / R04: do not retry; request updated account
  • R05 / R07 / R10 / R29: do not retry; mark as unauthorized
  • R08: do not retry until customer clears stop payment
  • Maximum retries per invoice: 1–2
  • Maximum retries per customer per month: configurable

9) Minimal implementation pattern

On invoice due date

  1. Create ACH debit
  2. Store payment_attempt
  3. Send to provider
  4. Wait for webhook

On webhook success

  1. Mark payment settled
  2. Mark invoice paid
  3. Send receipt

On webhook return

  1. Record ACH return code
  2. Reopen invoice balance
  3. Notify customer
  4. Decide retry/stop based on code

10) Example pseudocode

def charge_recurring_invoice(invoice_id):
    invoice = get_invoice(invoice_id)
    if invoice.status not in ["open", "due"]:
        return

    payment_method = get_default_ach_method(invoice.customer_id)
    if not payment_method or not payment_method.mandate_active:
        raise Exception("No active ACH mandate")

    payment = create_ach_payment(
        invoice_id=invoice.id,
        payment_method_id=payment_method.id,
        amount=invoice.amount_due,
        idempotency_key=f"invoice-{invoice.id}"
    )

    submit_to_provider(payment)

    update_invoice_status(invoice.id, "pending_payment")


def handle_ach_webhook(event):
    if already_processed(event.id):
        return

    if event.type == "ach.settled":
        mark_payment_settled(event.payment_id)
        mark_invoice_paid(event.invoice_id)

    elif event.type == "ach.returned":
        record_return(event.payment_id, event.return_code)
        reopen_invoice(event.invoice_id)
        notify_customer_of_return(event.invoice_id, event.return_code)
        maybe_retry(event.payment_id, event.return_code)

    store_event(event.id)

11) Best practices checklist

  • Collect and store explicit ACH authorization
  • Tokenize bank account data
  • Use idempotency keys
  • Treat ACH as asynchronous
  • Use webhooks for settlement/return events
  • Implement return-code-specific logic
  • Keep audit logs and mandate records
  • Build retry rules conservatively
  • Separate invoice, payment, and ledger states
  • Monitor return rates and unauthorized debits

If you want, I can also give you one of these:

  1. a sample database schema for ACH invoicing,
  2. a step-by-step Stripe/GoCardless-style workflow,
  3. or a Node.js / Python webhook handler example for ACH returns.

Observation history

How did Obsurfable measure this prompt?

Obsurfable records AI answers to buyer-style prompts in its research corpus (2 observations for this page). Metrics are distributions over observations, not a single static ranking.

Which AI systems does Obsurfable collect answers from?

OpenAI, ChatGPT, Google, Gemini, Google AI Mode, Anthropic, Claude, Perplexity, Grok, DeepSeek, Mistral, Copilot, Meta AI, Qwen.

How can brands improve their AEO?

Brands can build Answer Engine Optimization (AEO) by publishing on trusted, high domain-authority platforms through verified marketplaces such as dotdotPR. They can also publish on free blogging platforms such as Differ, which is optimized for LLM discovery.

Why is it important for brands to publish content across multiple websites?

Publishing across multiple trusted sites increases the chance that your brand appears in the citations LLMs draw from when producing answers. Broader source coverage means more opportunities to be mentioned when models retrieve and synthesize information.

Want this interpreted for your brand?

Explorer is the free public corpus. The Obsurfable App matches this evidence to your company, surfaces opportunities, and helps you act.