A month ago, I left my job at Braintree on the bank payments team. After spending over a couple of years building and maintaining the system, I believe I have a decent understanding of how the 'Automated Clearing House' or ACH system works in the United States. This post aims to share some details about how it works from an engineer's perspective, and it is not specific to Braintree.
What is ACH?
The term "Automated Clearing House" didn't make much sense to me the first time I heard it. What do cleaning houses have anything to do with bank payments? It seemed like the result you'd get if you put the phrase "vacuum cleaner" through Google Translate in another language and then translated it back.
Turns out, since the 19th century, before electronic systems were common, banks would physically meet at a centralized location, where representatives from each bank would exchange and tally up checks to 'clear' them. These locations came to be called 'clearing houses.' Here's a simplified view of how it works: let's say Bank A's customers wrote checks worth $12,000, and Bank B's customers wrote checks worth $15,000. Bank B would pay $3,000 to Bank A to 'clear' their debts. In an oversimplified view, ACH automates this manual process.
Today, ACH is used to facilitate money movement for various purposes, including payroll, rent payments, mortgages, B2B payments, and more. It is particularly suitable for high-value transactions that would be costly to process on a credit card due to the high interchange fees. In fact, according to a report by NACHA, ACH facilitated a volume of $61.9 trillion in 2020.
There are 5 major players involved in the ACH process (In reality, like any complex and old system, there are multiple intermediaries and downstream systems involved but they are not the focus of this post).
- Federal Reserve
- Merchant's Bank
- Customer's Bank
Let's take on the role of the merchant with the goal of enabling customers to make payments via ACH in exchange for our products. In centralized systems like ACH, a bank is required to initiate transactions on our behalf. To facilitate ACH payments from customers, we need to partner with a bank and open a business account. This bank is referred to as the 'Originating Depository Financial Institution' or ODFI. The customer's bank in this scenario is known as the 'Receiving Depository Financial Institution' or RDFI.
How does it work?
Step-by-step, the process works as follows:
Customer → Merchant
As you'll later see, the customers hold a significant amount of power in the ACH system so it is important to verify the account before accepting a payment. That is step zero. There are various methods available to verify a customer's bank account, each with its own advantages and disadvantages.
Utilizing a third party service that maintains extensive databases: This option allows for instantaneous checks, but it may not be supported by all banks, and there is a possibility of outdated data.
Integrating with services like Plaid that offer APIs to connect with the customer's financial institution: This option shares similar limitations as the one mentioned above, and it can also be relatively expensive.
Micro-Transfers serve as the most reliable form of verification. Essentially, we credit the customer's account with two random deposits of less than $1 and wait for the ACH network to work its magic. The customer can then confirm the deposited amounts, providing evidence that the account indeed exists. As you'll note, this option requires a much larger lift as we have to build an additional UI and the necessary logic to handle the deposits. It can also be slow, expensive, and prone to abuse. Read this crazy story of a person siphoning $50,000 from E-Trade and Schwab for an example.
After verifying the account or deciding to take the risk, we move on to the actual payment. The following are the bank details we'll need:
account_number: I learned the hard way that account numbers don't necessarily have to consist of only numbers. Seriously! Apart from the 17-character limit, banks have the freedom to issue account numbers with any combination of characters. This adds an extra layer of security as it makes it harder to guess account numbers. They can even be a single digit!
routing_number: This number represents the bank's ID. Larger banks may have multiple routing numbers, while smaller ones typically have a single routing number. Routing numbers are always 9 digits long, and each digit serves a specific purpose, containing information that identifies the bank. The 9th digit acts as a checksum calculation of the first 8 digits, providing an additional measure to validate the routing number. Routing numbers change a lot more frequently than you'd imagine and it usually happens when banks aquire other banks or merge with them.
ach_mandate: This refers to a checkbox right above the pay button and is equivalent to "I accept the terms and conditions" agreement. It is actually required as per NACHA rules. The customer's bank allows them to dispute or reverse any charge, no questions asked. Even after 60 days, they can initiate a dispute. The customer's bank may reach out to you, and you will need to provide this mandate as proof that you were authorized to charge the customer. It is essential to timestamp and store this mandate. Stripe recommends the following language for the mandate:
Other details required for the payment are:
Before the client sends the account details to the backend, it is crucial to encrypt the account number as it is a sensitive piece of information. On the backend, the account number should never be stored or made available as plain text; it should always be encrypted. The decryption of the account number only happens when building the NACHA file, which is the next step in the process.
Merchant → ODFI
When the customer initiates the payment, perform necessary validations and store the transaction details in the database with a relevant status (
initiated, for example). Typically, the ODFI sets a cut-off time for the day. Prior to this cut-off time, a scheduled job can retrieve all transactions with an
initiated status and create a NACHA/ACH file. The NACHA file is a specific file format mandated by the federal reserve.
Once the ACH file is generated, it needs to be uploaded to the ODFI's SFTP server, ideally close to the bank's cut-off time. At this point, the status of the transactions can be transitioned from
ODFI → Federal Reserve
The exact details of the next few steps in the process are somewhat obscure to me since I haven't directly interacted with these systems. After receiving the NACHA file, the ODFI takes on the responsibility of parsing through it. They conduct their own validations and risk review on the entire file. Interestingly, the ODFI assumes that the transactions will go through even before sending them to the federal reserve. This is why transactions may appear instantly on the originator side but take days to reflect on the receiver side. The ODFI generates or combines files for all their customers and proceeds to transfer them to the federal reserve.
Some banks still rely on antiquated and archaic systems, with a few of them still running on COBOL! As a merchant, we have no direct visibility into this process.
Federal Reserve → RDFI
The Federal Reserve sorts through all the ACH files they receive from various sources. They process the transactions in batches, ensuring compliance with NACHA rules and formatting requirements. The transactions are then sorted based on their destination, typically determined by the routing number associated with each transaction. Once the destination file is complete, it is delivered to the relevant RDFIs
RDFI → Federal Reserve
ACH operates on a "no news is good news" model, meaning that no response is provided for successful transactions. The originator of the transaction must assume that the transaction went through unless they receive specific information indicating otherwise within a certain timeframe.
Once the RDFI processes the ACH file, they handle each transaction accordingly. For deposits, they credit the respective accounts, while for charges, they verify if the customer's account holds sufficient funds to cover the transaction. If there are enough funds, the amount is deducted from the account. However, if there are insufficient funds or other issues, it results in an error or what is known as a 'return' in ACH lingo. Various return codes exist to signify different scenarios, such as non-existent account, closed account, or account unable to be debited, etc.
Within a few business days after receiving the transaction, the RDFI is required to respond in case of failures. They provide return codes to indicate the reasons for the failures, such as insufficient funds, account closure, etc. New return codes are introduced by NACHA on a regular basis.
|R01||Insufficient Funds: The available and/or cash reserve balance is not sufficient to cover the dollar value of the debit entry.|
|R02||Account Closed: A previously active account has been closed by action of the customer or the RDFI.|
|R04||Invalid Account Number: The account number structure is not valid.|
|R07||Authorization Revoked by Customer: The Receiver has revoked authorization.|
|R08||Payment Stopped: The Receiver has requested the stop payment of a specific ACH debit entry.|
|R09||Uncollected Funds: The RDFI has sufficient book or ledger balance to satisfy the dollar value of the transaction, but the dollar value of transactions in the process of collection could make this balance insufficient.|
|R20||Non-Transaction Account: Policies and regulations restrict transaction activity in this type of account.|
The RDFI compiles all the failed transactions for the day into a single file known as the 'ACH Return File.' Note that the customer, has up to 60 days since the day of transaction creation to 'dispute' the charge - no questions asked. For instance, a transaction created on May 1st could potentially receive a return as late as June 29th.
In addition to the failed transactions, the return file also includes transactions that were successfully processed but require some minor corrections for future reference. These are referred to as 'Notification of Change' or NOCs. These notifications highlight issues such as incorrect or outdated routing numbers, incorrect account types, misspellings of account names, and other related concerns.
|C02||Incorrect Transit/Routing Number: The Routing Number is no longer valid for the Receiver’s account.|
|C03||Incorrect Routing Number & DFI Account Number: Both Routing Number and the Receiver's account number are incorrect.|
|C04||Incorrect Individual Name/Receiving Company Name: The individual or company name is incorrect.|
|C05||Incorrect Transaction Code: The Transaction Code is incorrect and should be changed.|
Every business day, the RDFI generates the return file containing the failures that have occurred since the last file was generated. This return file is then sent to the Federal Reserve.
Federal Reserve → ODFI → Merchant
The Federal Reserve processes the return file received from the RDFI. They parse the file and filter it, separating the transactions based on the respective banks involved. These filtered files are then sent to the ODFI.
The ODFI further updates their ledger to reflect the returned transactions and compiles a return file specifically for the merchant.
Merchant → Customer
The final piece of the puzzle involves the merchant receiving the return file from the ODFI and processing the transactions accordingly. The return file doesn't necessarily have a set time of arrival due to multiple parties involved in the process. Typically, a job is scheduled to run at a regular frequency, such as every hour or 30 minutes, to check if a new return file has been uploaded to the SFTP server. If no file is found, the job can be terminated until the next run. It is possible that no file is uploaded for the day or that multiple files are uploaded, so the job needs to account for these scenarios.
For each transaction listed in the return file, we, the merchant, look it up in the database and update the transaction's state from
returned. Additional information, such as return codes, should also be stored to aid in reconciling the issue with the customer. After processing the returns, any NOCs present in the return file need to be addressed by updating the bank account details in the database accordingly.
As mentioned earlier, ACH operates on a 'no news is good news' model. If a certain amount of time has passed since the transaction was sent to the ODFI without any return, it is assumed to be settled. For example, let's say the transaction was created on day 'X'.
The settlement schedule can vary based on our risk appetite. For faster settlement, if no return is received by day
X+1, the transaction can be considered 'settled'. However, in most cases, 1 day is not sufficient, so normal settlement timelines are typically
X+3 business days. It's important to account for weekends and bank holidays, as ACH processing does not occur on those days. Any transactions that do not receive a return by their expected settlement date can be transitioned to the
settled state, indicating that the payment has been successfully processed.
Since returns can occur up to 60 calendar days later, there may be instances where a transaction initially marked as
settled needs to be moved to the
returned state. This requires reconciling the situation with the customer, as we would have likely sent the product assuming that the payment had gone through. This highlights why verification of bank accounts is important.
The ACH system handles the volume it does because it's widely recognized as incredibly reliable. One of the main reasons for this is that the system rarely changes or introduces new features. This has its pros and cons. On one hand, it adds confidence to the system since it's working smoothly without any major problems. However, considering the multitude of complex money movement systems available, it's important for ACH to elevate its features. To accomplish this, we can build new systems and incorporate useful features on top of the existing framework.
When a customer makes a payment via ACH, the transaction sits in our database until it's time to include it in the NACHA file that will be uploaded to the ODFI. During this period, wouldn't it be great if our customers had the flexibility to change their minds and void the transaction? Allowing customers this freedom generally leads to improved conversion rates over time.
Quite simply, if we're before the cut-off time, the customer should be able to click a button to void their transaction. We can easily retrieve the transaction from the database and update its state from
voided. However, there's an interesting race condition to consider. When supporting a high volume of transactions, the job responsible for generating the NACHA file requires a certain amount of time to run. Consequently, we should prevent voids once the job starts running. Otherwise, we would find ourselves in a situation where we've informed the customer that their charge was voided, but we've still sent it to the bank. To address this issue, we can implement a few measures. For example, creating a separate 'void cutoff' that adds a buffer to the ODFI's cut-off time, allowing enough time for the job to complete. Additionally, we can restrict transitions from the
pending state to
voided to prevent voids during the job execution.
Returns can be quite costly for merchants due to fees paid to the ODFI, resulting in reduced revenue. However, there are provisions available to mitigate some of these costs in specific situations. For returns with codes 'R01' (insufficient funds) and 'R09' (uncollected funds), instead of transitioning the state from
returned, we can move them to a separate state called
reinitiated. To handle this, you can maintain a separate database table to store details of these
reinitiated transactions, as there are specific rules involved.
ACH allows for reinitiation up to two times, which means a total of three entries including the initial transaction. During return file processing, transactions with R01 and R09 codes can be transitioned to their correct states and moved to the separate database table. Additionally, when generating the NACHA file, the job can check for any transactions that need to be included for reinitiation. In many cases, credits may be pending in the customer's account, and on retrying, the payment might successfully go through.
Implementing this feature can be tricky as there is a risk of double charging the customer. It can be a complex flow to manage, but with proper care, it can help reduce costs and increase successful payments.
Refunds are a commonly understood concept where customers are able to return items or deny services to receive their funds back. The main difference is that the refund transaction will be a credit instead of a debit. However, in the context of the ACH system, there can be some peculiarities that need to be considered.
Let's take a scenario as an example:
- Day 1: Debit transaction initiated
- Day 3: Debit transaction settled
- Day 4: Order refunded. Credit transaction created.
- Day 6: Refund transaction settled
- Day 7: Original transaction returned
In this case, the customer has both disputed the original transaction, resulting in the withdrawal of their funds, and received a refund from us, effectively being double-paid. Such situations need to be carefully monitored and reconciled with the customer.
ACH, an aged system that carries a substantial portion of domestic money movement in the United States, holds immense significance for the economy. This guide aims to provide an overview of how the system operates and the considerations involved in developing applications that interact with it.
In recent times, there has been a wave of innovation with RTP (Real Time Payments), which is expected to bring ACH up to par with other "instant" forms of payments. While I haven't had the opportunity to work directly with these new systems yet, I look forward to the chance of exploring them in the future.