Your guide to the Moov ACH API

The Moov ACH project includes an API with several operations for ACH files, including bidirectional conversion between plaintext and JSON formats. While the project’s primary API documentation contains plenty of technical details and examples, the purpose of this guide is to serve as a launching point for new users. We’ll run through a few basic examples of creating, fetching, modifying, and validating files.

Background

ACH is an electronic payment system managed by the National Automated Clearing House Association (Nacha). The ACH network sees over 25 billion transactions annually and supports several payment types, from direct deposits to bill payments. If you’d like to learn more about ACH, check out our short intro to ACH payments.

ACH files follow a standard format defined by Nacha. Each ACH file contains batches with a standard entry class (SEC) code to identify transaction type and shell structure composed of entry detail and addenda records. Our ACH API supports all SEC codes listed in the official Nacha operating rules. Please refer to our ACH file structure documentation to understand how the assembly of these files works.

Moov ACH does not persist (save) any data about the files, batches, or entry details created. Storage only occurs in memory of the process, and the API service will not have data saved when restarted.

Initial setup

The easiest way to get started with the ACH API is to install Docker on your system and run the latest ACH image in your terminal with:

docker run -p 8080:8080 -p 9090:9090 moov/ach:latest

This step automatically pulls the latest image and runs the API on port 8080 with Prometheus metrics on port 9090. To ensure the service is active, you can ping it with curl localhost:8080/ping. If it’s up and running, you’ll receive an HTTP 200 status with PONG as the response body. Otherwise, you’ll see something like Failed to connect to localhost port 8080: Connection refused.

Listing files

curl localhost:8080/files will access a list of ACH files stored in memory. Opening a browser window and setting the URL to localhost:8080/files lets you view the file list quickly. Assuming you haven’t uploaded any files yet, it will return {"files":[],"error":null}.

Each file is stored in JSON format and assigned a unique file ID if not already provided within its contents. Since these files can be large, I recommend using an in-browser JSON formatting tool or jq to view them.

Creating files

With the /create endpoint, you can create ACH files on the server. You can choose to upload plaintext or JSON representations using this same endpoint. We have many sample files in the test/testdata directory of our project and refer to these examples throughout this article. Ensure your current directory is test/testdata before running the following commands.

# Upload a plaintext representation
curl -X POST --data-binary "@ppd-debit.ach" localhost:8080/files/create

# Upload a JSON file
curl -X POST -H "content-type: application/json" -d "@ppd-valid.json" localhost:8080/files/create

In either case, you’ll receive a response with the newly created file’s unique ID and any parsing errors that may have occurred, as validation is performed automatically upon submission. Errors will not block file creation—an invalid file will be stored.

Fetching files

Once a file is on the server, you can fetch its contents by supplying its file ID. Use the cURL -o flag to save the files locally.

# Retrieve the JSON representation
curl localhost:8080/files/<YOUR-UNIQUE-FILE-ID>

# Retrieve the raw ACH file format
curl localhost:8080/files/<YOUR-UNIQUE-FILE-ID>/contents

Validating files

We offer a /validate endpoint since invalid files can make their way onto the service during the initial file creation process. A file is “invalid” if it’s not Nacha compliant. To validate a file without any custom validation options, simply use:

curl localhost:8080/files/<YOUR-UNIQUE-FILE-ID>/validate

Certain API operations on valid files can turn them invalid as well. For instance, if you deleted all the batches in a file using the /batches endpoint, validation would report, {"error":"invalid ACH file: BatchCount calculated 0 is out-of-balance with file control 1"}.

Deleting files

Deleting files stored in memory is as simple as sending a curl DELETE request. For example:

curl -X DELETE localhost:8080/files/<YOUR-UNIQUE-FILE-ID>

Fetching, appending, and deleting batches

A few operations for batches are also available. You can assign each batch an optional ID during the initial file or batch creation. Use this ID to fetch or delete the individual batch. Unlike file IDs, generating the batch ID does not happen by the service. In addition, batch IDs only need to be unique within the scope of the file they’re in.

# Fetch all batches in a file
curl localhost:8080/files/<YOUR-UNIQUE-FILE-ID>/batches

# Fetch an individual batch
curl localhost:8080/files/<YOUR-UNIQUE-FILE-ID>/batches/<BATCH-ID>

# Append a new batch
curl -X POST -d "@new-batch.json" localhost:8080/files/<YOUR-UNIQUE-FILE-ID>/batches

# Delete a batch
curl -X DELETE localhost:8080/files/<YOUR-UNIQUE-FILE-ID>/batches/<BATCH-ID>

Flattening batches

It’s possible to consolidate batches in a single file using the /flatten endpoint. If you have multiple batches with the same batch header contents (e.g., SEC code), flattening will combine all of their entry records into a single batch. This operation produces a new file rather than modifies the original one. If a file cannot be consolidated, /flatten will just duplicate it. Please refer to FlattenBatches for additional details on how this operation behaves.

To flatten batches, use:

curl -X POST localhost:8080/files/<YOUR-UNIQUE-FILE-ID>/flatten

File before flattening:

{"id":"unflattened","fileHeader":{"id":"file-01","immediateDestination":"231380104","immediateOrigin":"121042882","fileCreationDate":"181008","fileCreationTime":"","fileIDModifier":"A","immediateDestinationName":"Citadel","immediateOriginName":"Wells Fargo"},"batches":[{"batchHeader":{"id":"batch-01","serviceClassCode":200,"companyName":"Wells Fargo","companyIdentification":"121042882","standardEntryClassCode":"PPD","companyEntryDescription":"Trans. Des","effectiveEntryDate":"181009","originatorStatusCode":1,"ODFIIdentification":"12104288","batchNumber":1},"entryDetails":[{"id":"entry-01","transactionCode":22,"RDFIIdentification":"23138010","checkDigit":"4","DFIAccountNumber":"81967038518      ","amount":100000,"identificationNumber":"#83738AB#      ","individualName":"Steven Tander         ","discretionaryData":"  ","traceNumber":"121042880000001","category":"Forward"}],"batchControl":{"id":"","serviceClassCode":200,"entryAddendaCount":1,"entryHash":23138010,"totalDebit":0,"totalCredit":100000,"companyIdentification":"121042882","ODFIIdentification":"12104288","batchNumber":1},"advBatchControl":{"id":"","serviceClassCode":280,"entryAddendaCount":0,"entryHash":1,"totalDebit":0,"totalCredit":0,"achOperatorData":"","ODFIIdentification":"","batchNumber":1},"offset":null},{"batchHeader":{"id":"batch-01","serviceClassCode":200,"companyName":"Wells Fargo","companyIdentification":"121042882","standardEntryClassCode":"PPD","companyEntryDescription":"Trans. Des","effectiveEntryDate":"181009","originatorStatusCode":1,"ODFIIdentification":"12104288","batchNumber":2},"entryDetails":[{"id":"entry-02","transactionCode":22,"RDFIIdentification":"23138010","checkDigit":"4","DFIAccountNumber":"81967038518      ","amount":100000,"identificationNumber":"#83738AB#      ","individualName":"John Tander         ","discretionaryData":"  ","traceNumber":"121042880000002","category":"Forward"}],"batchControl":{"id":"","serviceClassCode":200,"entryAddendaCount":1,"entryHash":23138010,"totalDebit":0,"totalCredit":100000,"companyIdentification":"121042882","ODFIIdentification":"12104288","batchNumber":2},"advBatchControl":{"id":"","serviceClassCode":280,"entryAddendaCount":0,"entryHash":1,"totalDebit":0,"totalCredit":0,"achOperatorData":"","ODFIIdentification":"","batchNumber":1},"offset":null}],"IATBatches":null,"fileControl":{"id":"unflattened","batchCount":2,"blockCount":1,"entryAddendaCount":2,"entryHash":46276020,"totalDebit":0,"totalCredit":200000},"fileADVControl":{"id":"","batchCount":0,"entryAddendaCount":0,"entryHash":0,"totalDebit":0,"totalCredit":0},"NotificationOfChange":null,"ReturnEntries":null}

File after flattening:

{"id":"flattened","fileHeader":{"id":"file-02","immediateDestination":"231380104","immediateOrigin":"121042882","fileCreationDate":"210510","fileCreationTime":"1759","fileIDModifier":"A","immediateDestinationName":"Citadel","immediateOriginName":"Wells Fargo"},"batches":[{"batchHeader":{"id":"batch-01","serviceClassCode":200,"companyName":"Wells Fargo","companyIdentification":"121042882","standardEntryClassCode":"PPD","companyEntryDescription":"Trans. Des","effectiveEntryDate":"181009","originatorStatusCode":1,"ODFIIdentification":"12104288","batchNumber":1},"entryDetails":[{"id":"entry-01","transactionCode":22,"RDFIIdentification":"23138010","checkDigit":"4","DFIAccountNumber":"81967038518      ","amount":100000,"identificationNumber":"#83738AB#      ","individualName":"Steven Tander         ","discretionaryData":"  ","traceNumber":"121042880000001","category":"Forward"},{"id":"entry-02","transactionCode":22,"RDFIIdentification":"23138010","checkDigit":"4","DFIAccountNumber":"81967038518      ","amount":100000,"identificationNumber":"#83738AB#      ","individualName":"John Tander         ","discretionaryData":"  ","traceNumber":"121042880000002","category":"Forward"}],"batchControl":{"id":"","serviceClassCode":200,"entryAddendaCount":2,"entryHash":46276020,"totalDebit":0,"totalCredit":200000,"companyIdentification":"121042882","ODFIIdentification":"12104288","batchNumber":1},"offset":null}],"IATBatches":null,"fileControl":{"id":"flattened","batchCount":1,"blockCount":1,"entryAddendaCount":2,"entryHash":46276020,"totalDebit":0,"totalCredit":200000},"fileADVControl":{"id":"","batchCount":0,"entryAddendaCount":0,"entryHash":0,"totalDebit":0,"totalCredit":0},"NotificationOfChange":null,"ReturnEntries":null}

Segmenting files

With /segment, you can split a single file into two, separating debit entries from credit entries. This operation produces new files and doesn’t modify the source file. To segment an existing file on the service, use:

curl -X POST localhost:8080/files/<YOUR-UNIQUE-FILE-ID>/segment

If you attempt to segment a “Mixed Debits and Credits” file that doesn’t have both debits and credits, a single “Debits Only” or “Credits Only” file will be created. Alternatively, if you attempt to segment anything other than a “Mixed Debits and Credits” file, it’ll be duplicated. Please refer to SegmentFile for additional details on how this operation behaves.

File before segmenting (mixed debits and credits):

{"id":"unsegmented","fileHeader":{"id":"file-01","immediateDestination":"231380104","immediateOrigin":"121042882","fileCreationDate":"181008","fileCreationTime":"","fileIDModifier":"A","immediateDestinationName":"Citadel","immediateOriginName":"Wells Fargo"},"batches":[{"batchHeader":{"id":"batch-01","serviceClassCode":200,"companyName":"Wells Fargo","companyIdentification":"121042882","standardEntryClassCode":"PPD","companyEntryDescription":"Trans. Des","effectiveEntryDate":"181008","originatorStatusCode":1,"ODFIIdentification":"12104288","batchNumber":1},"entryDetails":[{"id":"credit","transactionCode":22,"RDFIIdentification":"23138010","checkDigit":"4","DFIAccountNumber":"81967038518      ","amount":100000,"identificationNumber":"#83738AB#      ","individualName":"Steven Tander         ","discretionaryData":"  ","traceNumber":"121042880000001","category":"Forward"},{"id":"debit","transactionCode":27,"RDFIIdentification":"23138010","checkDigit":"4","DFIAccountNumber":"91987638987      ","amount":100000,"identificationNumber":"#83738AB#      ","individualName":"Jason Bogo            ","discretionaryData":"  ","traceNumber":"121042880000002","category":"Forward"}],"batchControl":{"id":"","serviceClassCode":200,"entryAddendaCount":2,"entryHash":46276020,"totalDebit":100000,"totalCredit":100000,"companyIdentification":"121042882","ODFIIdentification":"12104288","batchNumber":1},"advBatchControl":{"id":"","serviceClassCode":280,"entryAddendaCount":0,"entryHash":1,"totalDebit":0,"totalCredit":0,"achOperatorData":"","ODFIIdentification":"","batchNumber":1},"offset":null}],"IATBatches":null,"fileControl":{"id":"unsegmented","batchCount":1,"blockCount":1,"entryAddendaCount":2,"entryHash":46276020,"totalDebit":100000,"totalCredit":100000},"fileADVControl":{"id":"","batchCount":0,"entryAddendaCount":0,"entryHash":0,"totalDebit":0,"totalCredit":0},"NotificationOfChange":null,"ReturnEntries":null}

File after segmenting (credits only):

{"id":"segmented-credits-only","fileHeader":{"id":"file-02","immediateDestination":"231380104","immediateOrigin":"121042882","fileCreationDate":"210510","fileCreationTime":"1821","fileIDModifier":"A","immediateDestinationName":"Citadel","immediateOriginName":"Wells Fargo"},"batches":[{"batchHeader":{"id":"batch-02","serviceClassCode":220,"companyName":"Wells Fargo","companyIdentification":"121042882","standardEntryClassCode":"PPD","companyEntryDescription":"Trans. Des","effectiveEntryDate":"181008","originatorStatusCode":1,"ODFIIdentification":"12104288","batchNumber":1},"entryDetails":[{"id":"credit","transactionCode":22,"RDFIIdentification":"23138010","checkDigit":"4","DFIAccountNumber":"81967038518      ","amount":100000,"identificationNumber":"#83738AB#      ","individualName":"Steven Tander         ","discretionaryData":"  ","traceNumber":"121042880000001","category":"Forward"}],"batchControl":{"id":"","serviceClassCode":220,"entryAddendaCount":1,"entryHash":23138010,"totalDebit":0,"totalCredit":100000,"companyIdentification":"121042882","ODFIIdentification":"12104288","batchNumber":1},"offset":null}],"IATBatches":null,"fileControl":{"id":"segmented-credits-only","batchCount":1,"blockCount":1,"entryAddendaCount":1,"entryHash":23138010,"totalDebit":0,"totalCredit":100000},"fileADVControl":{"id":"","batchCount":0,"entryAddendaCount":0,"entryHash":0,"totalDebit":0,"totalCredit":0},"NotificationOfChange":null,"ReturnEntries":null}

File after segmenting (debits only):

{"id":"segmented-debits-only","fileHeader":{"id":"file-03","immediateDestination":"231380104","immediateOrigin":"121042882","fileCreationDate":"210510","fileCreationTime":"1821","fileIDModifier":"A","immediateDestinationName":"Citadel","immediateOriginName":"Wells Fargo"},"batches":[{"batchHeader":{"id":"batch-03","serviceClassCode":225,"companyName":"Wells Fargo","companyIdentification":"121042882","standardEntryClassCode":"PPD","companyEntryDescription":"Trans. Des","effectiveEntryDate":"181008","originatorStatusCode":1,"ODFIIdentification":"12104288","batchNumber":1},"entryDetails":[{"id":"debit","transactionCode":27,"RDFIIdentification":"23138010","checkDigit":"4","DFIAccountNumber":"91987638987      ","amount":100000,"identificationNumber":"#83738AB#      ","individualName":"Jason Bogo            ","discretionaryData":"  ","traceNumber":"121042880000002","category":"Forward"}],"batchControl":{"id":"","serviceClassCode":225,"entryAddendaCount":1,"entryHash":23138010,"totalDebit":100000,"totalCredit":0,"companyIdentification":"121042882","ODFIIdentification":"12104288","batchNumber":1},"offset":null}],"IATBatches":null,"fileControl":{"id":"segmented-debits-only","batchCount":1,"blockCount":1,"entryAddendaCount":1,"entryHash":23138010,"totalDebit":100000,"totalCredit":0},"fileADVControl":{"id":"","batchCount":0,"entryAddendaCount":0,"entryHash":0,"totalDebit":0,"totalCredit":0},"NotificationOfChange":null,"ReturnEntries":null}

Summary

We hope this walkthrough helps you get started with ACH and serves as a quick refresher for current users. The API is only a fraction of the entire project, so we encourage you to visit ACH on GitHub too! If you have any questions or suggestions for ACH, please contact the Moov team on Slack (#ach channel) or submit an issue.