ACH addenda: telling the vendor what you just paid them for
A bare ACH credit arrives at your vendor as money with no explanation. Their AP clerk emails yours to ask which invoices it covers. Here's how to put the invoice numbers in the file — and the three counters that will get it rejected if you don't update them.
The problem is a phone call
You pay eleven invoices in one ACH credit. The vendor's bank shows them a deposit, your company name, and an amount. Nothing else.
So they call. Or email. And somebody on your side opens the payment, lists the invoices, and sends them over. Every payment run, for every vendor whose remittance you never automated.
That's the entire business case, and it's bigger than it sounds. Unapplied cash sits on the vendor's AR while they work out what it was for, which means your account looks past due while their clerk figures it out, which means dunning notices and credit holds on an account you actually paid on time.
The fix is a field in the file that has existed since the 1970s and that most NetSuite ACH templates don't emit.
The addenda record
NACHA files are fixed-width, 94 characters per line, with a record type in position one. A 6 record is an entry — one payment to one bank account. A 7 record is an addenda attached to the preceding entry, and record type 705 carries 80 characters of free-form remittance text.
In a NetSuite EFT template it's one line, emitted inside the payment loop immediately after the entry:
<#assign addendaText = payment.custbody_remit_detail!"">
<#if !addendaText?has_content>
<#assign addendaText = "PAYMENT " + payment.tranid!"">
</#if>
705${setLength(addendaText,80)}0001${setPadding(batchLineNum,"left","0",7)}
Three things about that line worth pointing out.
The text comes from a custom body field on the payment, not from the template. That means the invoice list is assembled where the business logic lives — a workflow, a user event, or the process that builds the payment — and the template's only job is formatting. Keep string-building out of FreeMarker.
There is always a fallback. If the field is empty, emit something. An addenda record with 80 blank characters is worse than useless: it consumes a line, changes your counts, and tells the vendor nothing. PAYMENT plus the transaction number is enough for them to trace it.
0001 is the addenda sequence number and the trailing seven digits are the entry detail sequence number, which must match the entry this addenda belongs to. Get that wrong and the addenda is orphaned.
Eighty characters is not much. Roughly six invoice numbers with separators. If a payment covers more than that, truncate and accept it, or split the payment — but decide deliberately, because setLength will silently cut whatever doesn't fit.
The part that gets your file rejected
This is the real content of this article.
A NACHA file is self-reconciling. Every batch ends with an 8 control record, the file ends with a 9 control record, and those records restate counts and totals that the bank recomputes. If your arithmetic disagrees with theirs by one, the whole file bounces.
An addenda record is a record. Add one per payment and you have just changed three counters:
1. The batch entry/addenda count. The 8 record's count field means entries plus addenda, not entries. So it has to be tracked separately and added in:
<#assign batchAddendaCount = batchAddendaCount + 1>
...
8${getBankServiceClassCode()}${setPadding(batchLineNum + batchAddendaCount,"left","0",6)}...
2. The file entry/addenda count. The 9 record carries the same total across all batches, so the addenda have to be rolled up there too:
<#assign lineCount = lineCount + batchLineNum + batchAddendaCount>
3. The block count. NACHA files are blocked in groups of ten lines. The 9 record reports the number of blocks, computed as total records divided by ten, rounded up — and when the file isn't a clean multiple, it's padded with lines of nines. Adding an addenda per payment shifts the record count, so both the block figure and the padding change:
<#assign value = (recordCount / 10)?ceiling>
One counter that is not affected, and it's worth knowing why: the entry hash. It sums the first eight digits of each receiving bank's routing number across the batch. Addenda records don't carry a routing number, so they don't contribute. Increment it in the entry loop only.
The thing that matters more than remittance detail
There's a bigger reason to generate the file from the ERP, and it has nothing to do with what the vendor sees.
Before this, paying and recording were two separate acts. Someone assembled the payment in the bank portal and sent it, then went back into the ERP afterwards to mark the bills paid. Two systems, two steps, in whatever order the day allowed.
At five o'clock on a Friday, sometimes the second step didn't happen.
Those bills stayed open. The next payment run picked them up, because from the ERP's point of view they had never been paid — and the vendor got the money twice. Then somebody has to notice, ask for it back, and reconcile a mess that touches AP, the bank, and the vendor relationship all at once.
Generating the file from the payment records makes that impossible. Producing the file is recording the payment; there is no window between them and no second step to forget. Duplicate payments stop being something you catch and start being something that can't happen.
That's the argument to make to a controller. The remittance detail saves the AP clerk a phone call. This saves the company from paying an invoice twice and never being certain how often it happened before anyone was counting.
Duplicate payments stop being something you catch and start being something that can’t happen.
Two other things this template does that are worth stealing
CCD and PPD in one file
Corporate payments use the CCD entry class. Payments to individuals — employee reimbursements, for instance — use PPD. Different classes cannot share a batch, so a run containing both has to produce two batches in one file.
The template splits payments by looking at which parent the bank detail record hangs off — vendor and customer records go to CCD, employee records go to PPD — and emits a complete batch for each, each with its own 5 header, its own entries, and its own 8 control.
FreeMarker makes this awkward because you can't concatenate sequences. The working approach is to build a comma-separated string of index expressions and evaluate it into a sequence:
<#assign ccdPaymentsStr = ccdPaymentsStr
+ "payments[" + payment_index?c?string + "],">
...
<#assign ccdPayments = ("[" + removeEnding(ccdPaymentsStr, ",") + "]")?eval>
It looks like a hack because it is one. It's also the standard way to partition a list in this environment, and it works reliably.
Validate the data, not the file
The format defines hard field widths — routing number exactly nine, company ID exactly ten, account number up to seventeen, bank name up to twenty-three, company legal name up to sixteen. NetSuite's EFT framework lets you attach validators to the configuration fields themselves:
<fieldValidator>
<fieldName>your_routing_number_field</fieldName>
<validatorList>
<validator type='len'>
<param name='minLength'>9</param>
<param name='maxLength'>9</param>
</validator>
<validator type='custom' />
</validatorList>
</fieldValidator>
The custom validator on a routing number runs the ABA checksum — a mistyped digit is caught at data entry rather than by the bank two days later.
The principle generalises well beyond ACH: enforce format constraints where the data is entered, not where the file is generated. A validation failure on a vendor record is a five-second fix by the person who caused it. The same error surfacing as a rejected payment batch costs a day and involves three people.
Where this fits
Alongside the wire side of the same project. Different formats — ACH is fixed-width NACHA, international wires are ISO 20022 XML — and both came down to the same two things: getting the data out of the ERP that was already there, and respecting the format's own arithmetic.
The addenda change took an afternoon. It removed a recurring conversation between two AP departments permanently, which is a good ratio for an afternoon.