---
title: Add a bank account
summary: Normalizes and submits an ACH bank account to US Bank so it can later be used for payments, handling duplicate-account responses as a non-exceptional outcome.
status: draft
owner: SAIF
last_reviewed: 2026-07-30
library: SAIF.Business.EPay.USBank
reference_type: package
operation_id: OP-EPAY-02
trigger: Caller invokes USBankAPIs.AddBankAccount(inBankAccount, inUSBankUser, inIsPolicyVoluntary)
---

# Add a bank account

Add-a-bank-account submits a single ACH account (checking or savings, business or consumer) to US Bank so it can be referenced by a US Bank user ID in later payment or recurring-authorization calls. There is no local database persistence in this operation — the account itself lives only at US Bank, keyed by the account key returned in the response.

| Field | Value |
| --- | --- |
| **Operation ID** | OP-EPAY-02 |
| **Trigger** | Caller invokes `USBankAPIs.AddBankAccount(inBankAccount, inUSBankUser, inIsPolicyVoluntary)` |
| **Inputs** | `IBankAccount` (encrypted account number, encrypted routing number, nickname, business/consumer flag, checking/savings flag); `IUSBankUser` (US Bank user sequence number); policy voluntary/assigned-risk flag |
| **Outputs** | `AddBankAccountResults` carrying a success flag and, on success, the US Bank-assigned account key |
| **Primary components** | `USBankAPIs.AddBankAccount`, `USBankAPIs.GetFilledUSBankBankAccountInfo`, `USBankAPIs.GetFilledAddBankAccountRequest` |
| **Entry point** | `Business\SAIF.Business.EPay.USBank\USBankAPIs.vb` |

## Steps

1. **Classify the account category.** `GetFilledUSBankBankAccountInfo` sets `accountCategory` to Business or Consumer based on `inBankAccount.IsBusiness`/`IsConsumer` ([BR-012](../business-rules.md#br-012-bank-account-payloads-require-category-and-type)).
   - If neither flag is true: throw `ApplicationException("Bank account must be business or consumer")`.
2. **Decrypt and normalize the account number.** `SAIF.Security.clsSecureEncrypt.DecryptString` decrypts the stored account number, and spaces are stripped from the result.
3. **Classify the account type.** `accountType` is set to Checking or Savings based on `inBankAccount.IsChecking`/`IsSavings` ([BR-012](../business-rules.md#br-012-bank-account-payloads-require-category-and-type)).
   - If neither flag is true: throw `ApplicationException("Bank account must be checking or savings")`.
4. **Decrypt the routing number.** The bank routing number is decrypted the same way as the account number.
5. **Normalize the nickname.** The nickname has invalid characters stripped (`RemoveInvalidUSBankAccountNickNameCharacters`) and is truncated to 20 characters ([BR-011](../business-rules.md#br-011-outbound-text-must-use-us-bank-safe-characters-and-lengths)).
6. **Build the add-account request.** `GetFilledAddBankAccountRequest` wraps the normalized bank account, resolves the biller credential from the voluntary/assigned-risk flag, and sets the request's `userID` to the US Bank user's sequence number as a string.
7. **Call the SOAP client.** `AddBankAccount` is invoked on the `epayWebServiceService` client obtained through the SAIF-certificate-authenticated `GetWebService()` helper.
   - On a transport-level failure (exception): wrap and re-throw as an `ApplicationException`.
8. **Interpret the response code.** If the response code is non-zero:
   - If the code is `75` or `5025` (duplicate account): return `AddBankAccountResults(False, String.Empty)` — a handled, non-exceptional outcome ([BR-013](../business-rules.md#br-013-duplicate-bank-accounts-return-a-handled-result)).
   - Otherwise: concatenate all response messages and throw an `ApplicationException`.
9. **Return the success result.** If the response code is zero, return `AddBankAccountResults(True, AddBAResponse.accountKey)`.

## Process flow

The following diagram visualizes this operation's control flow.

```mermaid
flowchart TD
	Start([AddBankAccount bankAccount, usBankUser, isVoluntary]) --> Category{Business or consumer?}
	Category -->|Neither| ErrCat([Throw: must be business or consumer])
	Category -->|One set| Decrypt[Decrypt account number and routing number]
	Decrypt --> Type{Checking or savings?}
	Type -->|Neither| ErrType([Throw: must be checking or savings])
	Type -->|One set| Nick[Normalize and shorten nickname]
	Nick --> Call[Call AddBankAccount SOAP operation]
	Call --> Code{Response code}
	Code -->|0| Success([Return Success = True, AccountKey])
	Code -->|75 or 5025| Dup([Return Success = False,<br>empty AccountKey])
	Code -->|Other non-zero| Fail([Throw ApplicationException<br>with response message])
```

## Failure and edge-case behavior

- Missing or ambiguous account category/type throws before any network call is made, so malformed metadata never reaches US Bank ([BR-012](../business-rules.md#br-012-bank-account-payloads-require-category-and-type)).
- Duplicate-account response codes (`75`, `5025`) are the only response codes translated into a handled failure result; every other non-zero code is treated as an exceptional failure ([BR-013](../business-rules.md#br-013-duplicate-bank-accounts-return-a-handled-result)).
- The account number and routing number must already be encrypted with the SAIF encryption utility before this operation runs; decryption failure surfaces as whatever exception `clsSecureEncrypt.DecryptString` raises, uncaught by this method.*
- There is no local persistence of the returned account key by this operation; callers are responsible for storing it if it needs to be reused.*

> **Note:** Statements marked with `*` were inferred from code structure, naming
> conventions, or contextual evidence where direct confirmation was not available
> in source artifacts.

## Related documentation

- [Operations and processes](../operations-processes.md)
- [Business rules](../business-rules.md)
- [SQL catalog](../sql/index.md)
- [Database tables](../database-tables.md)

## Sources

- `Business\SAIF.Business.EPay.USBank\USBankAPIs.vb`
