Skip to content

ADR 002: Huntington for California

Status: Accepted
Context: ACH payment processing for surrogacy escrow disbursements


Context

SeedTrust processes ACH disbursements to surrogates, agencies, and vendors. The original payment path uses NACHA file generation via the carta-ach library, with file delivery to M&T Bank via SFTP. This is a batch-file approach: the admin generates a .txt NACHA file and uploads it to M&T’s SFTP server, where M&T processes the ACH transactions.

The NACHA/M&T path has several operational characteristics:

  • Asynchronous and batch-oriented — files are generated manually, uploaded, and processed by the bank overnight
  • No real-time transfer status — SeedTrust does not get per-transaction feedback during processing
  • SFTP delivery — requires credentials and manual (or automated) upload step
  • Managed in Flask — the NACHA workflow is part of the legacy Flask admin UI

For Intended Parents based in California, the team chose to integrate with Huntington Bank’s direct API instead. This provides:

  • Real-time transfer status per transaction
  • API-based account provisioning — Outside Instruments (external bank accounts) are registered via API, not pre-uploaded
  • CIP (Customer Identification Program) verification at account creation time
  • Dual-approval enforcement at the API layer
  • SSE streaming for live batch progress in the admin UI

Decision

Route all cases where the Intended Parent’s state is California (CA) through the Huntington Bank API. All other cases continue to use NACHA/M&T.

How routing is determined:

When an IP submits their ACH form, the system calls should_use_huntington(state, country):

HUNTINGTON_STATES = ["CA", "CALIFORNIA"]
def should_use_huntington(state, country):
return is_us_country(country) and state.upper() in HUNTINGTON_STATES

If this returns True, case.bank_account_type is set to BankAccountEnum.HUNTINGTON. Otherwise it defaults to BankAccountEnum.M_T.

The routing is set at ACH form submission time and is stored on the case. It is not re-evaluated dynamically per transaction.


Consequences

Positive:

  • Real-time batch visibility for CA cases — admins can watch transactions process and see failures immediately
  • API-driven provisioning eliminates the SFTP delivery step for these cases
  • Dual-approval enforced programmatically (two distinct admin approvers required before a batch can run)
  • Failed transactions can be retried individually without regenerating the entire batch

Negative:

  • Two parallel payment paths with different code, different admin UIs, and different failure modes. Developers must understand both
  • Huntington batches require the Huey worker. If the worker is down, Huntington transactions queue up indefinitely. NACHA batches are unaffected (Flask generates them synchronously)
  • Outside Instrument provisioning adds latency. Each new payee requires an API call to register their bank account before the first transfer can proceed
  • Huntington has rate limits and retryable errors (429, request_inprogress) that must be handled — the NACHA path does not have these concerns
  • State-based routing is static. If an IP moves from California to another state, the case bank_account_type does not automatically change. A manual update would be required

Current State

The Huntington integration is live in FastAPI (seedtrustapi/src/seedtrust/modules/banking/huntington/). The NACHA/M&T path remains in Flask. Both are actively used in production.

The auto-healing task (auto_heal_huntington_batches_periodic) runs every minute to reconcile stuck batches and surface failures.

See also: ACH & Banking, ACH Batch Generation Runbook