- The approved TMS invoice is the source of truth. A document model proposes lines and never gets a ledger credential.
- Only an approved invoice and its approved accessorials post, after the three-way match passes.
- The idempotency key is invoice id plus ledger. A retry looks up that key before any second create.
- A missing account map row or a closed period fails the whole document closed.
- Corrections are credit memos with their own key, linked to the original invoice.
QuickBooks Online and Sage Intacct are named here because they are common carrier ledgers a due-diligence review will meet. The same gates apply to either. This page does not mandate a ledger, and it does not include authentication secrets, tokens, or company passwords.
Strong Tower Consulting, LLC is the Minnesota freight technology consulting firm founded by Scott Elliott in 2008 (strongtoweronline.com). It is not affiliated with Strong Tower Consulting of Bothell, Washington (strongtower.consulting or strongtower.ai).
The TMS invoice is the source of truth
The document that may reach the ledger is the TMS invoice billing has approved. The POD image, the rate confirmation PDF, and the model that read them are inputs to a person or to a rule the carrier already accepted. If an extracted linehaul disagrees with the invoice line, the invoice line wins and the disagreement becomes an exception. Posting the extracted total "so the pipeline does not wait" writes a number the TMS did not approve. Do not do that.
A document model may classify a page, suggest that a paragraph is detention, or flag a total that does not match the order. Those are proposals onto the TMS invoice, reviewed before the invoice status becomes approved. The model is not given a ledger credential. It does not call the accounting API. It does not post cash, apply a remittance, or decide that a customer has paid. Cash application stays a finance function with its own approval, outside this pipeline.
The document model may
- Classify a page
- Suggest that a paragraph is detention
- Flag a total that does not match the order
It may not
- Hold a ledger credential or call the accounting API
- Post an extracted total as an invoice
- Post cash, apply a remittance, or decide a customer paid
What may post after the three-way match
Two things may post automatically once the match passes: an approved invoice, and an approved accessorial that is already a line on that invoice. Nothing else in the billing pipeline gets a ledger call. An accessorial still sitting in a suggestion queue is not approved, even if the header invoice looks finished. A fuel line the rating engine did not store, and that no billing user accepted, does not ride along because a model saw the words "fuel surcharge" on a scan.
The three legs are the rated order, the approved TMS invoice, and delivery evidence. Delivery evidence is the POD the carrier accepted, or the delivery fact the carrier named as the system of record. An EDI 214 is a customer message, not automatically that fact. How status messages relate to the truck is covered in the 214 milestone translation guide. Illustrative tolerance for an automatic post is zero dollars between rated charges and invoice lines. A controller may publish a cents tolerance. Over tolerance, the invoice waits. The zero-dollar figure is illustrative, not a measured result from a named carrier.
| Gate | Pass | Fail |
|---|---|---|
| TMS invoice | Status approved, invoice id assigned | Do not post a draft or an extracted total |
| Rated order | Invoice lines equal rated charges within the controller tolerance | Exception queue, no ledger call |
| Delivery evidence | Accepted POD or the named delivery fact on the order | Do not post from a 214 transmit time |
| Accessorial lines | Each accessorial has an approver on the invoice | Fail the document. Do not drop the line and post the rest. |
| Account map and period | Every line maps, and the invoice date is in an open period | Fail closed. Do not invent an account or a date. |
Idempotency key is invoice id plus ledger
The idempotency key is the TMS invoice id plus the ledger. "Ledger" is a stable name the carrier assigns, such as the QuickBooks Online company or the Sage Intacct entity they are posting into. The same invoice posted to a second set of books is a second key and a second audit row. A retry, a timeout, and a second worker that wakes up after a crash all use the original key. They do not mint a new invoice id because the first response was slow.
invoice key = TMS invoice id + ledger
settlement key = settlement id + ledger
credit key = credit memo id + ledger
The same invoice posted to a second set of books is a second key and a second audit row.
At architecture level the worker sends that key on the create, using the external-id or idempotency field that ledger documents for duplicate protection, and it stores the key on the TMS export row before the call returns. Product field names differ. The signed integration sheet names them. This page does not embed client secrets, realm identifiers as credentials, or sample tokens. If a holding company truly posts the same invoice into both QuickBooks Online and Sage Intacct, that is two deliberate posts, each with its own key, not one call that "syncs everything."
Illustrative chart-of-accounts map
The worker does not hard-code account numbers. It reads a map the controller owns: charge type, ledger, account, and the date range the row is effective. The table below is an illustrative map so a workshop can see the shape. Every number in it is a placeholder. A controller must replace the numbers, the names, and the debit and credit policy with the carrier's own chart before a live post is enabled. Leaving the illustrative numbers in production would put revenue in accounts that do not exist.
Customer invoices and driver pay are different documents. Linehaul, fuel, and approved accessorials credit revenue and debit accounts receivable on the customer invoice. Driver payable comes from the approved settlement, with its own key of settlement id plus ledger, so a driver check cannot collide with the customer invoice key. Do not stuff settlement lines onto the customer invoice to save a call. The amounts will not represent either document, and a later credit memo will not know which side to reverse.
| TMS charge | Illustrative role | Illustrative number | Where it posts |
|---|---|---|---|
| Linehaul | Freight revenue | 4100 illustrative | Credit revenue on the approved customer invoice. Debit accounts receivable for the invoice total. |
| Fuel | Fuel surcharge revenue | 4110 illustrative | Credit revenue on that same customer invoice. Not a cash movement. |
| Accessorial | Accessorial revenue | 4120 illustrative | Credit revenue only when that accessorial line is approved on the invoice. |
| Driver payable | Contractor or wage payable | 2100 illustrative | Separate settlement post. Debit the expense account the controller names. Credit the payable. Not a line on the customer invoice. |
Audit log: request hash, response id, approver
Every attempt writes an audit row, including the attempts that fail closed. Store the idempotency key, the ledger name, the invoice id, the request hash, the response id when the ledger returns one, the approver, the time, the accounting period, and the outcome. The request hash is a hash of the canonical payload: key, accounts, amounts, and invoice date. It is not a password and it is not a signature secret. If two attempts claim the same key, the hashes show whether the payload changed. A changed payload on a key that already posted is a defect, not a silent update.
The approver is the TMS user who approved the invoice. If a second person must release the ledger post, store that user as well. "System" is not an approver for the first production post of a charge type. The response id is whatever the ledger returns as the created document's id. Store it as text. Do not parse it into a success story if the body also says the period was rejected. Keep these rows with the billing record for as long as the carrier keeps invoices. A hot integration log that expires in two weeks is not the audit. Longer retention fits the warehouse pattern in the carrier data lakehouse, with payroll and rate columns still access-controlled.
Reversal and credit memo path
A rating fix after a successful post is a credit memo, or the ledger's reversal document, not a delete-and-repost of the original key. The credit memo carries its own idempotency key: credit memo id plus ledger. The audit links it to the original invoice id and to the original response id. Amounts reverse the lines being credited, through the account map that was effective on the original invoice date. If today's map no longer covers that date, fail closed and ask the controller which accounts to hit. Do not aim last quarter's credit at this quarter's new revenue number because the placeholder row was edited.
Partial removal of one accessorial is a credit for that line, approved again, not a second full invoice. A void is allowed only when the ledger and the period both allow it, and the audit records void as the outcome on a linked document. Cash that has already been applied is not undone by this pipeline. A document model does not post cash, and it does not unapply cash. Finance clears the receipt with the same care they use for any other misapplied payment. The billing worker stops at the open receivable.
Retry without double posting
Give the export row a small state machine: approved, posting, posted, unknown, and rejected.
- Move the row to posting. The worker moves the row to posting and sends the payload.
- Mark posted only after the response id is stored.
- On a timeout, mark unknown. A timeout or a dropped connection moves the row to unknown. Unknown means the ledger might have created the document.
- Look up by the idempotency key. The next step is a lookup by the idempotency key, not another create. If the lookup finds the document, store its response id and mark the row posted.
- Retry the same payload and key. Only if the lookup is empty.
Rejected is a map, a period, or a validation error. Do not retry a rejection on a timer. Fix the cause, and if the payload hash must change, a person clears the rejection. Two workers must not create in parallel for one key. Take a short lease on the row, illustrative 60 seconds, and steal the lease only when it is expired and the state is still posting or unknown. The 60 second figure is illustrative. The rule is one owner at a time, then a lookup before any second create. The poster is a separate service from dispatch, in the sense of the decoupled services architecture, so a ledger outage does not lock the order board.
Period close lock
Read the close calendar the controller maintains, and prefer the ledger's own period state when the API exposes it without turning the call into a guessing game. If the invoice date falls in a closed period, set the outcome to blocked and stop. Do not rewrite the invoice date to today to force the post into the open month. That hides the revenue in the wrong period and trains any later billing review on a lie. Illustrative case: an invoice dated the 30th, books closed on the 5th of the next month, attempt on the 6th. The attempt fails. The dates are illustrative. The block is the rule.
A controller may publish an explicit rule that a blocked invoice posts to the next open period. That rule has an id, an owner, and an effective range, and the audit stores the rule id on the row. Without that rule, the default is fail closed. Nightly jobs do not get a quieter interpretation. Real time does not get one either. The EDI 210 a customer receives, described in the 204, 990, 214, and 210 lifecycle, can still go out on the TMS invoice date. The ledger post waits. They are different documents that should share an invoice id, not a single timestamp someone edited to please a close.
Fail closed when the account map is missing
Before the create call, resolve every line: linehaul, fuel, each accessorial type, and, for a settlement, driver payable. A missing row, an inactive row, or a row with a blank account number fails the entire document. Do not post the mapped lines and leave the unmapped line for later. A partial invoice in the ledger will not match the TMS total, and the retry will look like a duplicate of the lines that already landed. Do not substitute a suspense account that is not in the map. Suspense is allowed only when the controller inserted a map row that names it for specific charge types.
A missing row, an inactive row, or a row with a blank account number fails the entire document. Do not post the mapped lines and leave the unmapped line for later.
Return the charge types that failed so billing can see them without reading a stack trace. Keep the invoice approved in the TMS. Approval was correct. The map was not. When the controller adds the row, the same idempotency key is used for the first real create. No second invoice id is issued because the first attempt never posted. The same fail-closed behavior applies if the ledger name on the key is unknown. An empty ledger id is not "the default company." It is a stopped post.
QuickBooks Online and Sage Intacct, without secrets
QuickBooks Online and Sage Intacct cover a large share of carrier finance stacks, which is why a review names them. The architecture does not prefer one. It asks the same questions of both: where the external id lives, how a duplicate create is reported, how a closed period is signaled, and how a credit memo links to the original document. Answers go in the integration sheet the controller signs. They do not go in source comments as company ids, client secrets, or refresh tokens. A runbook that must hold a secret belongs in the carrier's secret store, not in an insight page and not in the audit hash.
Field-level mapping is product work done once the gates above are accepted. Building that mapping before the three-way match and the account map exist just automates a bad invoice faster. The readiness questions for whether billing data is even fit to automate are in the AI readiness evaluation. This page assumes those clocks and invoice keys are already good enough to match. It does not assume a model should be handed the books.
Frequently asked questions
Controllers ask these questions when a billing project promises that documents will "hit the GL by themselves." The answers keep the TMS invoice, the match, and the idempotency key in front of any API call.
If the illustrative account numbers from this page are still in the map, the worker is not ready. Replace them, then approve one invoice in a non-production company and read the audit row before anyone points the worker at live books.
What is the source of truth for a ledger post?
The approved TMS invoice is the source of truth. A document model may propose lines from a POD or a rate sheet. Those proposals do not post. An extracted total is not an invoice, and a document model does not post cash by itself.
What may post after the three-way match?
An approved invoice, and an approved accessorial on that invoice, after the rated order, the invoice, and the delivery evidence agree. Unapproved accessorials and unmatched extracted totals stay out. Cash application is not part of this post.
What is the idempotency key for an invoice post?
Invoice id plus ledger. The same invoice aimed at QuickBooks Online is a different key from the same invoice aimed at Sage Intacct. A retry uses the same key and must not create a second document in that ledger.
What happens when the account map has no row for a charge?
The post fails closed. The worker does not invent a suspense account, and it does not post the lines that map while dropping the line that does not. A controller replaces the illustrative map before any live post.
How is a correction sent without posting the invoice twice?
A credit memo uses its own key: credit memo id plus ledger, linked to the original invoice. Retrying the credit uses that key. Retrying the original invoice still finds the first post. Cash already applied is a finance step, not something a document model reverses.
Post approved invoices once, and only once.
Strong Tower Consulting provides independent systems architecture, feasibility audits, and engineering roadmaps for motor carriers and freight brokers. Contact our advisory team or call 763-244-1373 to discuss your operational data and evaluate practical implementation paths for your fleet.