Bank Accounts & Prenotes

Bank account records hold a user’s KMS-encrypted account and routing numbers for ACH submission. Prenotes are zero-dollar ACH entries that verify an account before a live debit.

Account Records

Records are BANKINFO# items in the site-payments single table (schema), one per user and Plaid account_id. The API creates a record the first time a flow needs the account:

  1. Fetch account number, routing number, and the official account mask from the Transactions Service (Plaid Auth).

  2. Compute account_routing_hash, a salted SHA-256 of account + routing (site/salt secret).

  3. KMS-encrypt the account and routing numbers with alias/site-payments-bank-account.

  4. Store the record; later reads decrypt inside the Lambda at submission time.

A record with no mask attribute has its mask backfilled from the Transactions Service accounts endpoint on the next read. A failed backfill leaves the record unchanged, and a later read retries.

Source: pkg/bankaccount/bankaccount.go, pkg/bankaccount/store/

Account Mask

GET /{user_id}/bank-accounts/{account_id} and every payment record’s masked_account_number use the official account mask (last 2–4 characters), falling back to the last 4 digits of the auth account number when no mask is stored.

For some institutions the auth account number is a tokenized account number (TAN) whose digits differ from the account the user knows. Match on mask, not on the auth number’s last 4.

Removal

Path Effect

DELETE /{user_id}/bank-accounts/{account_id}

Deletes the BANKINFO# item; 202

Return-code engine invalidate-bank action

Deletes the item and writes BLOCKED — ACH dispatch

GET /check-bank-account returns the user IDs holding a given account_routing_hash (Fraud Detection).

Prenotes

Endpoint Submission

POST /prenotes/jpm

JPM ACH debit for 0.00

POST /prenotes/usio

Usio ACH prenote against the named usio_account credential — Merchant accounts

Both endpoints load the user’s bank account record, submit, and on provider success save a payment record: payment_status = ACHSENT, amount = 0, payment_provider = jpm or usio, event_type = PRENOTE_SUBMITTED (published immediately — Events). A provider failure returns 500 and writes nothing; a missing bank account returns 404.

Resolution follows the normal ACH paths:

  • Completion — check-ach-worker after the 3-business-day gate, PRENOTE_COMPLETED — Check ACH.

  • Return — JPM webhook or the Usio ACH-returns sync, payment_status = FAILED, PRENOTE_RETURNED.

PRENOTE_RETURNED is outside the blocklist-handler’s Kinesis filter: a prenote return records the code and takes no bank or user action.

Some banks return R01 (insufficient funds) on a zero-dollar prenote. It does not mean the account is invalid for a live debit.

Batch submission

site-payments-prenoter consumes SQS site-payments-prenotes and calls the prenote endpoints through the signing Payments client, one request per message. The only producer is pkg/prenotes/batch-script, a manual CSV-driven run.

Source: pkg/api/prenotes.go, pkg/prenotes/prenoter.go