Skip to the content.
How U.S. Payments Really Work Part 3
How U.S. Payments Really Work Part 3

NACHA File Demystified: Cracking the 94-Character Code

The 94-character lines that power $72 trillion in ACH transactions annually.

Suma Manjunath
Author: Suma Manjunath
Published on: August 15, 2025

NACHA Demystified Banner

Audience: Fintech engineers, payroll system builders, backend developers working with ACH
Reading Time: 12 minutes
Prerequisites: Basic understanding of bank accounts and ACH money flow
Why Now: $72 trillion moves through ACH each year. If you build fintech in the U.S., you will touch NACHA files. Getting it wrong means payroll failures, rejected batches, and angry CFOs.

TL;DR:

⚠️ Disclaimer: All scenarios, accounts, names, and data used in examples are not real. They are realistic scenarios provided only for educational and illustrative purposes.


Problem Definition

The challenge: Engineers building payroll or B2B payments eventually face NACHA rejections like “Invalid field length at position 47.” Debugging is painful because every character must align exactly.

Who faces this:

Cost of inaction: Rejected batches mean missed payrolls, delayed settlements, regulatory risk, and customer churn.

Why current tutorials fail: Most explanations are abstract (“fixed-width, 94 characters”) without complete, runnable examples. Developers are left guessing about padding, routing numbers, or totals.


Solution Implementation

The Mail Analogy

NACHA files are like mail delivery:

Here’s a Mermaid diagram of the structure:

flowchart TD
    A["File Header (1)"] --> B["Batch Header (5)"]
    B --> C1["Entry Detail (6)"]
    B --> C2["Entry Detail (6)"]
    C1 --> D["Addenda (7, optional)"]
    C2 --> D
    B --> E["Batch Control (8)"]
    A --> F["File Control (9)"]
    F --> G["Filler Lines (all 9s)"]

NACHA structure diagram fallback


Complete Ruby NACHA File Generator

require 'date'

class NachaFileBuilder
  def initialize(company_name:, company_id:, destination_bank:)
    @company_name = company_name
    @company_id = company_id
    @destination_bank = destination_bank
    @entries = []
    @batch_number = 1
  end

  def add_credit(account_number:, routing_number:, amount_cents:, name:, individual_id:)
    @entries << {
      transaction_code: '22',  # Credit to checking
      routing_number: routing_number,
      account_number: account_number.ljust(17),
      amount: sprintf('%010d', amount_cents),
      name: name.ljust(22)[0,22],
      individual_id: individual_id
    }
  end

  def generate_file
    lines = []
    lines << file_header_record
    lines << batch_header_record

    @entries.each_with_index do |entry, idx|
      lines << entry_detail_record(entry, idx + 1)
    end

    lines << batch_control_record
    lines << file_control_record

    # Pad to multiple of 10 records
    while lines.length % 10 != 0
      lines << "9" * 94
    end

    lines.join("\n")
  end

  private

  def file_header_record
    [
      '1', '01',
      @destination_bank.ljust(10),
      @company_id.rjust(10, '0'),
      Date.today.strftime('%y%m%d'),
      Time.now.strftime('%H%M'), 'A',
      '094', '10', '1',
      'BANK OF AMERICA'.ljust(23),
      @company_name.ljust(23),
      ' ' * 8
    ].join('').ljust(94)
  end

  def batch_header_record
    "5220#{@company_name.ljust(16)}#{@company_id.ljust(10)}PPDPAYROLL   #{Date.today.strftime('%y%m%d')}   10610000520000001".ljust(94)
  end

  def entry_detail_record(entry, trace_number)
    [
      '6', entry[:transaction_code],
      entry[:routing_number][0,8],
      entry[:account_number],
      entry[:amount],
      entry[:name],
      '  ',
      entry[:individual_id].ljust(15),
      sprintf('%07d', trace_number)
    ].join('').ljust(94)
  end

  def batch_control_record
    total_credit = @entries.sum { |e| e[:amount].to_i }
    count = @entries.length
    hash = @entries.sum { |e| e[:routing_number][0,8].to_i }.to_s[-10..]
    "8220000#{count.to_s.rjust(4,'0')}#{hash}0000000000#{sprintf('%010d', total_credit)}#{@company_id.ljust(10)}06100005#{@batch_number.to_s.rjust(7,'0')}".ljust(94)
  end

  def file_control_record
    "9000001#{@batch_number.to_s.rjust(6,'0')}000000#{@entries.length.to_s.rjust(6,'0')}#{'0'*22}".ljust(94)
  end
end

# --- Usage Example ---
builder = NachaFileBuilder.new(company_name: "ACME CORP", company_id: "123456789", destination_bank: "061000052")
builder.add_credit(account_number: "123456789", routing_number: "061000052", amount_cents: 12500, name: "JOHN DOE", individual_id: "EMP001")

puts builder.generate_file

ℹ️ Note: This produces a valid 10-line NACHA file padded with 9s.

Warning: If you misalign spacing or routing digits, the ODFI will reject the entire batch.

💡 Tip: Always validate your generated file with your ODFI’s sandbox tools before submitting to production.


Validation & Monitoring

Testing Steps

  1. Save output to sample.ach.
  2. Run through a NACHA validator (provided by your ODFI).
  3. Check:
    • Line count multiple of 10
    • Entry Hash = sum of first 8 digits of routing numbers
    • Debit and credit totals balance

Success Metrics

Common Failure Modes

Troubleshooting


Key Takeaways


Acronyms


References

  1. NACHA. “ACH Operating Rules & Guidelines.” NACHA, 2024 - nacha.org
  2. Federal Reserve. “FedACH Services.”

Comments & Discussion

Share your thoughts, ask questions, or start a discussion about this article.