Skip to content

Runbook: ACH Batch Generation

This runbook covers the end-to-end process for generating and submitting ACH payment batches. There are two separate paths: NACHA/M&T (used for non-California cases, handled in Flask) and Huntington API (used for California IPs, handled in FastAPI). They do not share code or UI.


Prerequisites

Before generating any batch:

  1. ACH forms must be approved. Every payee (surrogate, agency, IP) must have an approved master_ach_form record. Unapproved forms will silently exclude transactions from the batch.

  2. Transactions must be in PAID status with nacha_sent = False and payable_batch_id = NULL.

  3. Required admin permissions:

ActionPermission
View transaction listVIEW_NACHA
Generate NACHA batchVIEW_NACHA
Download NACHA fileDOWNLOAD_NACHA_HISTORY
Upload to M&T SFTPUPLOAD_NACHA_SFTP
View/approve ACH formsVIEW_IP_ACH_FORMS, VIEW_SURRO_ACH_FORMS, VIEW_AGENCY_ACH_FORMS

Step 1: Approve Pending ACH Forms

Before processing transactions, check for unapproved or updated ACH forms.

Location: Flask admin → ACH Forms (separate sections for IPs, Surrogates, Agencies)

Route: /admin/ach/ip/, /admin/ach/surrogate/, /admin/ach/agency/

Forms needing attention have:

  • form_approved == NULL — never approved
  • form_updates != NULL — user submitted a banking change

To approve:

  1. Open the ACH form detail page
  2. Review the banking fields (routing number, account number, account type, name)
  3. Verify the routing number is valid (the UI shows validation status; routing validation can be bypassed via the routing-number-verification-disabled PostHog feature flag if needed)
  4. Click Approve

What happens on approval:

  • Form updates are merged into the main master_ach_form record
  • All banking fields are re-encrypted with ACH_KEY
  • form_approved timestamp and admin_id are recorded
  • form_updates field is cleared; a backup copy is saved to form_backups
  • Admin users with COMPANY_BANKING_ALERT permission receive an email notification

Code: seedtrust_flask/seedtrust/views/admin/ach.pyach_form_detail()


Path A: NACHA / M&T (Non-California Cases)

Step 2A: Select Transactions for the Batch

Location: Flask admin → NACHA Dashboard (/nacha/list)

The dashboard lists eligible transactions:

  • Status: PAID
  • nacha_sent = False
  • payable_batch_id = NULL
  • Type: DISBURSEMENT or SCHEDULED PAYMENT
  • Amount: > 0

Select transactions using the checkboxes. You can batch multiple transactions in a single NACHA file.

Step 3A: Generate the NACHA File

Click Generate NACHA Batch. The system:

  1. Creates a NachaBatch record with a random 20-character ID
  2. Reads company banking settings from the Company profile (routing number, company TIN, bank name)
  3. For each selected transaction:
    • Validates the payee’s routing number (checksum algorithm)
    • Retrieves the decrypted account number and account type from the approved ACH form
    • Determines the ACH transaction code (22 for credit/checking, 32 for credit/savings, etc.)
    • Sanitizes payee name to ASCII alphanumeric
  4. Sets the effective entry date to the next US business day (skips weekends and US bank holidays)
  5. Generates the NACHA file using the carta-ach library with standard entry class PPD
  6. Uploads the .txt file to S3 (CRLF line endings, as required by NACHA spec)
  7. Marks all included transactions: nacha_sent = True, nacha_batch_id = <id>
  8. Returns the batch ID and S3 file path

The field ID modifier (A–Z, then 0–9) cycles with each batch to ensure unique NACHA file headers. The current modifier is displayed and increments automatically.

Step 4A: Deliver the File to M&T Bank

You have two options — download or SFTP upload. They are mutually exclusive: downloading marks the batch as closed for SFTP.

Option 1: SFTP Upload (automated delivery)

Click Upload to Bank SFTP on the batch detail page.

The system connects to M&T’s SFTP server using credentials from environment variables (BANK_SFTP_HOST, BANK_SFTP_USERNAME, BANK_SFTP_PASSWORD, BANK_SFTP_DIRECTORY) and uploads the file. After upload:

  • nacha_batch.sftp_upload_status is set to SUCCESS or FAILED
  • The remote path and file size are logged in the audit trail

If the upload fails, the error message is stored in nacha_batch.sftp_error_message. You can retry via the Retry button — the batch is not locked until it is downloaded.

Option 2: Manual Download

Click Download NACHA File. The file is retrieved from S3 via CloudFront and saved to your machine. The batch is then marked as CLOSED via DOWNLOAD — the SFTP upload button is disabled. You are responsible for transmitting the file to M&T manually.

SFTP Upload Status Values:

StatusMeaning
NOT_UPLOADEDBatch created, no delivery attempted yet
PENDINGUpload in progress
SUCCESSFile successfully sent to M&T SFTP
FAILEDUpload failed — check error message and retry
CLOSED via DOWNLOADBatch was downloaded (SFTP path closed)

Code: seedtrust_flask/seedtrust/views/admin/banking.py, seedtrust_flask/seedtrust/utils/sftp_utils.py


Path B: Huntington API (California Cases)

Huntington batches are handled entirely in FastAPI. The batch creation and processing happen via API endpoints in the admin dashboard.

When it applies: Any case where the Intended Parent’s state is California (CA). This is determined at ACH form submission time and stored as case.bank_account_type = HUNTINGTON.

Dual Approval Requirement

Every Huntington transaction requires two distinct admin approvers before it can be included in a batch:

  • transaction.first_approver_id — must be set (first admin approved)
  • transaction.second_approver_id — must NOT be set yet (second approval happens during batch processing)
  • The two approvers must be different admin users

Transactions missing first_approver_id or already having second_approver_id are excluded.

Step 2B: Initiate the Batch

Eligible transactions (status PENDING_TRANSFER, first_approver_id set, no second_approver_id) are collected and a HuntingtonBatch record is created with status PENDING. The transactions are linked via HuntingtonBatchTransaction and their status changes to IN_PROCESS. A PROCESS_HUNTINGTON_BATCH Huey job is enqueued.

The Huey worker must be running. Without it, the job never executes and transactions remain stuck in IN_PROCESS.

Step 3B: Background Batch Processing (Huey)

The worker processes each transaction in the batch:

  1. Validates the transaction is eligible (not on hold, not already completed, approvers correct)
  2. Retrieves ACH info from the payee’s approved ACH form (routing number, account number, account type, bank name)
  3. Gets or creates an Outside Instrument — Huntington requires a registered external account object per person, even if the bank account was already used. The instrument GUID is stored in the database for future reuse.
  4. Initiates an ACCOUNTTOBANK transfer via the Huntington API, providing the source account GUID, outside instrument GUID, amount, and a reference ID
  5. Records the transfer GUID in transaction.huntington_transfer_guid
  6. Sets second_approver_id to the processing admin — this is the second approval

On completion, the batch status becomes COMPLETED, PARTIAL (mixed results), or FAILED.

Step 4B: Monitoring the Batch

The batch detail page shows real-time progress via SSE (Server-Sent Events): processed_transaction_count vs total_transaction_count.

Check batch health in the JobRun table:

  • status = PENDING accumulating → Huey worker may be down
  • resolution = ERROR → Check Errors table and PostHog for the exception

Auto-Healing Stuck Batches

The auto_heal_huntington_batches_periodic Huey task runs every minute. It detects batches stuck in PROCESSING where all transactions have reached terminal states and finalizes them. It also marks individual transactions as FAILED if they have not updated in more than 5 minutes and are more than 15 minutes old (sets ledger status to ON_HOLD).

Code: seedtrustapi/src/seedtrust/modules/banking/huntington/service/batches.py


Retrying Failed Transactions (Huntington)

If a batch completes with PARTIAL or FAILED status, individual failed transactions can be retried:

Requirements for retry:

  • Transaction status must be FAILED
  • Transaction must NOT have a huntington_transfer_guid set (if it has one, the transfer was initiated — contact the bank before retrying)
  • Both approvers must re-approve (a new batch is created with the same dual-approval requirement)

A new HuntingtonBatch is created for the retry. The retry is logged with action type TRANSACTION_RETRY_BATCH.


Troubleshooting

NACHA batch: routing number validation failure

  • The carta-ach library validates routing numbers using a checksum algorithm
  • If a valid routing number is incorrectly rejected, the PostHog feature flag routing-number-verification-disabled can bypass validation temporarily
  • Verify the routing number with the payee before overriding

NACHA batch: SFTP connection failure

  • Check nacha_batch.sftp_error_message for the raw error
  • Verify SFTP credentials: BANK_SFTP_HOST, BANK_SFTP_PORT, BANK_SFTP_USERNAME, BANK_SFTP_PASSWORD
  • Verify the BANK_SFTP_DIRECTORY exists and the SFTP user has write permission
  • Retry is available if the batch has not been downloaded

Huntington: transactions stuck in IN_PROCESS

  • Verify the Huey worker is running (uv run st run worker)
  • Check the JobRun table for PROCESS_HUNTINGTON_BATCH entries with resolution = ERROR
  • The auto-healer will mark transactions as FAILED after 15 minutes of inactivity (ledger set to ON_HOLD)
  • For ON_HOLD transactions: investigate the Huntington error, resolve the underlying issue, then reset to retry

Huntington: “request_inprogress” error

  • This is a retryable Huntington API error indicating a duplicate request is already in flight
  • The worker has built-in retry logic (up to 2 retries with exponential backoff)
  • If it persists, check whether the transfer was actually created in the Huntington portal before re-submitting — a transfer GUID on the transaction record means the transfer was initiated

Notes

NACHA return file processing is manual. When M&T sends back a return or NOC file, there is no automated ingestion. An admin must receive the notification from M&T externally, then manually update the affected transactions to RETURN status in the admin UI.

Dual approval applies to both paths. When the dual-approval feature flag is on, both the NACHA/M&T path and the Huntington path require two separate admin approvals before a transaction can be included in a batch.

Huntington uses both polling and inbound webhooks for transfer status updates. The HuntingtonWebhookEvent table records processed webhook events.