Every NetSuite shop above a certain transaction volume ends up writing an invoice reconciliation script. It usually starts as a 200-line SuiteScript that matches bank transactions to open invoices by amount and date. It works fine until the month you process 10,000+ invoices, and then it falls over — partial payments, multi-currency rounding, batched deposits, and the customer who pays three invoices with one wire all conspire to push your match rate down to 60% and your AR team into manual reconciliation hell.
Who this is for: NetSuite admins and SuiteScript developers at companies processing more than 5,000 invoices a month, where manual AR reconciliation is consuming 20+ hours of finance team time per week. This pattern is overkill below that threshold.
The problem with rule-based reconciliation at scale
The classic NetSuite reconciliation pattern is a scheduled SuiteScript that pulls unmatched bank lines from the Banking module, then iterates open customer invoices looking for an exact amount match. It works for the 70% of payments that arrive as a one-to-one match on the invoice total. The remaining 30% — partial payments, lump sums covering multiple invoices, payments arriving with a memo like “INV-4421 / 4422 / 4435 net of credit” — drop into the manual queue and become AR’s problem.
The temptation is to keep adding rules. Match on invoice number in the memo. Then fuzzy-match the memo. Then handle the case where the customer paid in EUR but the invoice was issued in USD. Each new rule catches a few more transactions and introduces three new edge cases. By the time you’ve handled the long tail, your script is 2,000 lines of nested conditionals and nobody on the team will touch it.
The pattern: structured extraction first, scoring second
The shift that works is to stop trying to match heuristically in code, and instead use an LLM to extract a structured “payment intent” from each bank transaction memo, then use a deterministic scorer to match that intent against open invoices. The LLM is doing one job — turning unstructured memo text into structured fields — and the matching itself stays in plain SuiteScript where it can be audited and tested.
// Step 1: extract structured intent from the memo
const extractionPrompt = `
You are parsing a bank transaction memo for an accounts receivable team.
MEMO: "${bankTxn.memo}"
AMOUNT: ${bankTxn.amount}
CURRENCY: ${bankTxn.currency}
DATE: ${bankTxn.date}
Return JSON with these fields:
- invoice_numbers: array of invoice numbers mentioned (empty if none)
- customer_hint: any company or person name mentioned
- payment_type: "single" | "batch" | "partial" | "unclear"
- notes: any other clarifying detail (FX, credit memo applied, etc.)
DO NOT guess invoice numbers not present in the memo.
DO NOT invent customer names.
If unsure on any field, return null for that field.
`;
The “do not guess” instructions matter as much as the schema. Hallucinated invoice numbers cause far worse pain than unmatched payments — an unmatched payment goes to a queue, a wrong match silently closes the wrong invoice and corrupts your AR aging.
The scoring layer stays deterministic
Once the LLM has extracted structured intent, the matching logic is plain SuiteScript and produces a confidence score. We pull candidate open invoices for the customer (using the customer_hint plus a lookup), then score each candidate against the bank line: exact amount match, invoice-number-in-memo match, date proximity, currency match, and partial-payment plausibility. A weighted sum gives a score from 0 to 1.
- Score > 0.9: auto-apply the payment. Log the match reasoning to a custom record for audit.
- Score 0.6 – 0.9: queue with the top 3 candidate matches surfaced for the AR clerk to confirm in one click.
- Score < 0.6: queue for manual investigation, with the LLM’s extracted notes attached as context.
The auto-apply threshold is the lever your CFO will care about most. We start clients at 0.95 for the first month, then drop to 0.90 once they’ve reviewed the audit log and trust the scoring. Anything below 0.85 should never auto-apply, no matter how clean the test data looked.
The SuiteScript plumbing
The script runs as a scheduled SuiteScript 2.1 on a 15-minute interval. It pulls unmatched bank lines from the last 24 hours, calls the LLM extraction endpoint (we use a queue worker outside NetSuite to manage rate limits and retries), then runs the scorer against open invoice candidates. Matches above threshold are applied via the standard record.transform from CustomerPayment, with the source bank line ID written to a custom field custbody_reconciliation_source.
Two operational notes from running this in production. First, log everything — every extraction, every score, every auto-apply — to a custom record. When the AR team asks “why did this payment get applied to invoice 4421 instead of 4422”, you need to be able to answer in 10 seconds. Second, build a one-click “undo” that reverses a payment application and re-queues the bank line. You will need it, probably in the first week.
What the numbers look like
Across the three clients running this pattern in production: rule-based baselines were auto-matching 65–72% of incoming payments. With the structured-extraction-plus-scoring pattern, auto-match rates are 89–94% at a 0.90 threshold, with a sub-0.5% incorrect-match rate (caught on the next bank statement reconciliation, never escalated to a customer dispute). Time from “payment received” to “invoice closed in NetSuite” dropped from a median of 3 days to under an hour.
The LLM cost runs at about $0.002 per bank transaction at current model pricing — a rounding error against the labor cost the pattern displaces. The bigger cost is the engineering time to build the scoring layer correctly the first time, which is why we recommend the threshold approach: ship the queue-for-confirmation flow first, watch a month of real data, then enable auto-apply.
FAQ
Why not use NetSuite’s built-in AI features?
NetSuite’s native AI features around AR are improving but still focused on prediction (likely payment date, churn risk) rather than the structured-extraction problem at the heart of reconciliation. If you have a clean memo format from a single payment processor, native features may cover you. For the messy real-world case of memos written by humans in 12 different formats, an external LLM is currently a better fit.
What about data residency and PII?
Bank transaction memos rarely contain PII beyond a company name and invoice number, but you should still route them through an LLM endpoint that meets your compliance posture — for most of our clients that means an enterprise-tier API with a no-training, no-retention contract, hosted in the same region as their NetSuite account. Do not pipe raw banking data to a consumer LLM endpoint.
Can this handle multi-currency?
Yes, but the scorer needs an explicit FX-tolerance band. We allow a 0.5% variance between the bank line amount (converted to invoice currency at the bank’s posted FX rate) and the invoice total. Anything wider drops the match score below the auto-apply threshold and surfaces for manual review, which is the right behaviour — FX variance is the leading indicator that something else is wrong with the payment.