A CSV file is easy to inspect. A NACHA ACH file is not.
At first glance, an ACH file looks like a collection of arbitrary numbers and padded text. In reality, it is a fixed-width format with strict rules for record length, ordering, totals, routing numbers, and padding.
That means “converting CSV to ACH” is not simply changing a file extension. You are building a structured payment instruction that must agree with your bank’s origination requirements.
This guide covers three practical approaches:
- Exporting an ACH file from payroll or accounting software
- Generating one programmatically
- Using a browser-based converter and validator
It also includes a small JavaScript validator you can use to catch structural errors before submitting a file to a bank.
What is inside a NACHA file?
According to the Nacha ACH developer guide, every record in a standard ACH file is exactly 94 characters long.
The first character identifies the record type:
| Code | Record |
|---|---|
1 |
File Header |
5 |
Batch Header |
6 |
Entry Detail |
7 |
Addenda |
8 |
Batch Control |
9 |
File Control or padding |
A minimal file normally follows this shape:
1 — File Header
5 — Batch Header
6 — Entry Detail
6 — Entry Detail
8 — Batch Control
9 — File Control
9 — Optional padding records
An ACH file can contain multiple batches. Each batch can contain multiple entries, and some entries may have addenda records.
The file is normally padded so its total record count is a multiple of 10. Padding records contain 94 number-nine characters.
Before converting the spreadsheet
A source spreadsheet should use explicit columns rather than relying on visual formatting.
For example:
routing_number,account_number,account_type,amount,name,identification
021000021,123456789,checking,1250.00,Example Employee,EMP-1001
026009593,987654321,savings,875.50,Example Contractor,CTR-2042
Before generating the ACH file, check:
- Routing numbers contain exactly nine digits.
- Account numbers are treated as text, not numbers.
- Leading zeroes have not been removed by Excel.
- Amounts have no currency symbols or thousands separators.
- Debit and credit intent is explicit.
- Each payment has a stable internal identifier.
- Consumer and corporate entries are not accidentally mixed.
- The effective date agrees with your bank’s processing rules.
Do not infer the account type from the account number. Your input needs to specify checking or savings explicitly.
Method 1: Export from payroll or accounting software
If your payroll, ERP, treasury, or accounting system already supports your bank’s ACH format, use that export first.
Typical workflow:
- Import or enter the payment instructions.
- Select the correct bank account and payment type.
- Choose the ACH or NACHA export option.
- Confirm the effective date and batch description.
- Export the file.
- Validate the exported file before uploading it to the bank.
Advantages
- Usually incorporates your company and bank configuration
- Can integrate approval and payment workflows
- Avoids maintaining custom payment-generation code
Limitations
- Export formats may be bank-specific
- Configuration mistakes can still create invalid files
- Some systems hide control totals and record-level details
- Moving between banks may require a new export profile
Even files created by established payroll software should be validated. A structurally valid file can still contain the wrong company ID, origin routing number, SEC code, or effective date.
Method 2: Generate the ACH file programmatically
Programmatic generation is useful when payments originate in your own application.
The important part is to treat every field as a fixed-width value.
For example:
function alpha(value, length) {
return String(value)
.toUpperCase()
.slice(0, length)
.padEnd(length, " ");
}
function numeric(value, length) {
const digits = String(value).replace(/\D/g, "");
if (digits.length > length) {
throw new Error(`Numeric value exceeds ${length} characters`);
}
return digits.padStart(length, "0");
}
function cents(value) {
const amount = Math.round(Number(value) * 100);
if (!Number.isSafeInteger(amount) || amount < 0) {
throw new Error("Invalid payment amount");
}
return numeric(amount, 10);
}
These helpers reflect two central formatting rules:
- Alphanumeric fields are generally left-justified and space-padded.
- Numeric fields are right-justified and zero-padded.
Do not build amounts with floating-point string manipulation such as:
String(amount * 100);
Round deliberately and verify that the result is a safe non-negative integer.
Validate routing-number checksums
A nine-digit ABA routing number includes a checksum.
function isValidRoutingNumber(value) {
if (!/^\d{9}$/.test(value)) return false;
const digits = [...value].map(Number);
const checksum =
3 * (digits[0] + digits[3] + digits[6]) +
7 * (digits[1] + digits[4] + digits[7]) +
(digits[2] + digits[5] + digits[8]);
return checksum % 10 === 0;
}
A valid checksum does not prove that the routing number is active or eligible for ACH. It only detects many common transcription errors.
Validate record length by bytes
JavaScript string length and file byte length are not always the same.
ACH records are fixed-width ASCII records, so validate encoded bytes:
function validateRecordLengths(contents) {
const lines = contents
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n")
.split("\n");
if (lines.at(-1) === "") lines.pop();
return lines.flatMap((line, index) => {
const bytes = new TextEncoder().encode(line).length;
return bytes === 94
? []
: [`Record ${index + 1} contains ${bytes} bytes instead of 94`];
});
}
This catches problems caused by:
- Truncated fields
- Extra delimiters
- Smart quotes
- Emoji
- Multi-byte characters
- Accidental spaces at the end of a record
- Incorrect line construction
A practical structural validator
The following validator does not replace full NACHA validation, but it catches several useful structural problems:
function validateAchStructure(contents) {
const lines = contents
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n")
.split("\n")
.filter((line, index, all) => line || index < all.length - 1);
const errors = [];
lines.forEach((line, index) => {
const recordNumber = index + 1;
const byteLength = new TextEncoder().encode(line).length;
if (byteLength !== 94) {
errors.push(
`Record ${recordNumber}: expected 94 bytes, found ${byteLength}`
);
}
if (!/^[156789]/.test(line)) {
errors.push(
`Record ${recordNumber}: unknown record type ${JSON.stringify(line[0])}`
);
}
});
if (!lines[0]?.startsWith("1")) {
errors.push("The first record must be a File Header record");
}
const logicalRecords = lines.filter((line) => line !== "9".repeat(94));
if (!logicalRecords.at(-1)?.startsWith("9")) {
errors.push("The last logical record must be a File Control record");
}
if (lines.length % 10 !== 0) {
errors.push("The total record count is not a multiple of 10");
}
return errors;
}
A production validator should go further and recalculate:
- Batch count
- Block count
- Entry and addenda count
- Entry hash
- Total debit amount
- Total credit amount
- Batch numbers
- Trace-number ordering
- Addenda indicators
- Service-class consistency
It should also distinguish the File Control record from padding records. Both begin with 9, but padding records consist entirely of nines.
Method 3: Use a browser-based converter and validator
A browser-based tool can be useful when:
- The source data is already in CSV or Excel.
- You do not want to install finance software.
- You need to inspect the generated records.
- You want a quick structural validation before bank upload.
I am building ACH File Converter for this workflow. It includes CSV/Excel conversion, viewing, editing, control-total calculation, and ACH validation.
The important step is not just generating the file. Review the result before submitting it:
- Confirm the origin and destination values.
- Check the company identification.
- Verify the SEC and service-class codes.
- Compare the entry count with the spreadsheet.
- Compare debit and credit totals.
- Confirm the effective date.
- Validate the routing numbers.
- Keep a copy of the source data and generated file.
- Follow your bank’s approval and test-file process.
Your bank may require values that cannot be inferred from a spreadsheet, including an assigned company ID, immediate-origin value, file modifier policy, or offset-account entry.
Balanced and unbalanced files
One frequent source of confusion is whether the file needs an offset entry.
A balanced file includes an offset entry that balances the debit and credit totals inside the file.
An unbalanced file does not include that offset. The bank applies settlement outside the submitted file.
Neither choice is universally correct. It depends on your ODFI’s requirements.
Do not automatically add an offset entry because the totals look unbalanced. Ask the bank which format it expects and which account information should be used.
Common reasons banks reject ACH files
The most common failures are often small formatting or configuration mistakes:
- A record contains 93 or 95 characters instead of 94.
- Excel removed a leading zero from a routing or account number.
- The routing-number checksum is invalid.
- Batch and file control totals do not match the entries.
- The file contains unsupported characters.
- The block count or padding is wrong.
- A batch header and control record use different batch numbers.
- The wrong SEC code was selected.
- The effective date is invalid or outside the bank’s accepted window.
- A company ID or immediate-origin value does not match the bank profile.
- A balanced file was submitted where the bank expects an unbalanced file, or vice versa.
A validator can detect structural errors, but only the bank can confirm customer-specific origination settings.
Final checklist
Before uploading a production ACH file:
- Validate all 94-byte records.
- Recalculate control totals from the entries.
- Confirm the routing-number checksums.
- Confirm the number of spreadsheet rows against ACH entries.
- Check the effective date and file modifier.
- Confirm balanced versus unbalanced requirements.
- Remove unexpected multi-byte characters.
- Test with non-production payment data first.
- Follow dual-control and approval procedures.
- Never send real account data through screenshots, public issue trackers, or chat messages.
Generating the file is only one part of the workflow. The safer approach is:
structured input
→ explicit field mapping
→ ACH generation
→ independent validation
→ human review
→ bank upload
That extra validation step is much cheaper than investigating a rejected or incorrectly configured payment batch.
Disclosure: I am the builder of ACH File Converter, the tool mentioned in this article.
AI assistance disclosure: AI was used to help edit and structure this article. The technical content and examples were reviewed by the author.
Top comments (0)