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_STATESIf 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_typedoes 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