An ACH record is one fixed-format line in an ACH file. Each record is ASCII, 94 characters long, and stores fields at defined positions, so both field order and record order matter. section 2.3 section 2.4.2
Identify records by their type and place them within the ACH hierarchy:
- Type 1 file header: appears once per file. section 2.3
- Type 5 batch header: starts each batch. section 2.3
- Type 6 entry detail: represents entries within a batch. section 2.3
- Type 7 addenda: supplies zero or more additional records for an entry. section 2.3
- Type 8 batch control and Type 9 file trailer: close batches and the complete file. section 2.3
Parsing means slicing each line at its prescribed positions and mapping the fields to usable values. A practical parser can begin with separate functions for file headers, batch headers, entry details, and addenda, then add validation for malformed input. section 2.4.2 section 2.6.1
Rejecting a short record
A parser should detect any line that is shorter than the required 94 bytes, retain or process it according to the chosen policy, and accumulate an error that unit tests can verify. Truncated trailing spaces during file transfer are one practical cause. section 2.6.1
Figure 2.2 ACH file layout
Use the ACH file layout to see how file, batch, entry, addenda, and trailer records nest. section 2.3
Listing 2.5 Starting point for our ACH parser
def parse_file_header(line):
return {
'record_type': 'File Header',
'priority_code': line[1:3],
'immediate_destination': line[3:13],
'immediate_origin': line[13:23],
# ... additional fields
}
def parse_batch_header(line):
return {
'record_type': 'Batch Header',
'service_class_code': line[1:4],
'company_name': line[4:20],
# ... additional fields
}
def parse_entry_detail(line):
return {
'record_type': 'Entry Detail',
'transaction_code': line[1:3],
'receiving_dfi_id': line[3:11],
'account_number': line[12:29],
'amount': line[29:39],
# ... additional fields
}
def parse_addenda(line):
return {
'record_type': 'Addenda',
# ... additional fields based on the specific addenda type
}
def parse_batch_control(line):
return {
'record_type': 'Batch Control',
# ... additional fields
}
def parse_file_control(line):
return {
'record_type': 'File Control',
# ... additional fields
}
def parse_ach(filename):
records = []
parser_functions = {
'1': parse_file_header,
'5': parse_batch_header,
'6': parse_entry_detail,
'7': parse_addenda,
'8': parse_batch_control,
'9': parse_file_control
}
with open(filename, 'r') as file:
lines = file.readlines()
for line in lines:
record_type = line[0]
parser = parser_functions.get(record_type)
if parser:
records.append(parser(line))
else:
print(f"Unknown record type: {record_type} in line: {line}")
continue
return records
# Test the function with an ACH file
parsed_records = parse_ach('path_to_ach_file.txt')
for record in parsed_records:
print(record)
Use the starter parser listing to inspect position-based extraction for several ACH record types. section 2.4.2
