We built a one-way finance posting flow between HubSpot and Exact Online for a B2B client expanding into the US: HubSpot stays the operational source of truth, Exact Online stays the accounting source of truth, and approved invoices post from the first into the second as journal entries, with sync status written back so finance never leaves HubSpot to see what happened.
The client asked not to be named, so this is an anonymous case study. Everything else is real: the architecture, the constraints and the decisions. SpotDev is a HubSpot Diamond partner and Custom Integration Accredited; this is the engineering write-up of a live production build.
The brief
The client's commercial world already lived in HubSpot: deals, companies, contacts, invoice preparation, approval context and day-to-day operational status. Their accounting world lived in Exact Online: posted entries, debtor records, ledger treatment and payment outcomes. They needed those two worlds joined, reliably and auditably, without moving either team out of the system it works in.
| Before | After |
|---|---|
| Two disconnected sources of truth: deals, invoice preparation and operational status in HubSpot; posted entries, debtors and payment outcomes in Exact Online. Finance re-keys and cross-checks by hand. | One ordered pipeline. Approved, sent invoices flow from HubSpot into Exact Online US as journal entries; debtors are created or updated just in time; sync status, identifiers and errors are written back so finance has one place to look. |
Simple to state. The interesting part is everything the real environment did not give us.
The three constraints that shaped the build
| Constraint | What it ruled out | What we did instead |
|---|---|---|
| No Commerce Hub | HubSpot's native billing and item catalogue; any assumption about property naming | A strict field-mapping layer the client's admin maintains, with pre-flight checks that go green when every required field is bound |
| Exact posts journal entries, not item-driven invoices | Posting SalesInvoices and matching items | Post SalesEntries driven by General Ledger accounts; this one decision shaped debtor handling, line construction, credit notes and write-back |
| A lean footprint | A message-queue tier and separate worker fleet | One Next.js application image deployed as web plus cron roles, PostgreSQL for run history and audit, in-process retries with backoff and jitter |
The third constraint deserves a pause, because it runs against the reflexive answer. A queue is the standard way to process background work reliably, but at this volume it would have added an entire moving part to operate and pay for. In-process retries with a durable run log in PostgreSQL gave us the reliability and the audit trail without the tier, with a clean seam if throughput ever demands more. Verdict: buy infrastructure when you need it, not before.
The pipeline
Every invoice, whether picked up by the nightly job or pushed manually, runs the same ordered pipeline, and each stage is named so a failure records exactly where it failed:
- Fetch the invoice from HubSpot.
- Gate it against the eligibility rules.
- Resolve the debtor in Exact, creating or updating it just in time.
- Sync the invoicing contact (optional, feature-flagged).
- Build the Exact SalesEntry payload.
- Post it to Exact.
- Write back the result to HubSpot.
Only the right invoices, only once
Before anything posts, an invoice has to earn its place: routed to the US entity, not a draft, not already posted, a resolvable debtor, at least one line, a valid currency. One rule is deliberately strict and non-configurable: an invoice only syncs once it has actually been sent to the customer, because a sent date is proof a human reviewed and issued it. That rule is enforced twice, in the HubSpot workflow that can trigger a manual sync and again server-side, so no path can post an unsent invoice. The same filter the nightly job uses is surfaced in the dashboard as a live eligibility view, so finance can see exactly what the next run will pick up.
Debtors created just in time
Exact had no pre-existing customer list to look up against, so the integration creates and maintains debtors from HubSpot companies on demand. The first posted invoice for a company creates the Exact account, keyed on the HubSpot company record ID. Later syncs compare the current HubSpot snapshot against what was created and only write an update when something material changed, using a hash to avoid pointless round-trips. Legacy finance references resolve straight to existing debtors rather than creating duplicates.
A line can never leave without a General Ledger account
With no item master, the GL account is what tells Exact how to treat each line. Product types map to GL codes through a table the client's own admin maintains, translated to Exact's internal identifiers at post time. An unknown code surfaces as a clear, specific error rather than a silent omission. And where a line's product type is blank or unmapped, it falls through the configured default to a hardcoded Other revenue account, logged every time it fires. A line is therefore never sent without a GL, and unmapped product types stay visible. Verdict: it is the difference between "the sync failed" and "the sync succeeded, and told you which line needs a product type".
VAT: the client computes, we transmit
VAT resolves per line with a clear priority: a line-level code if present, otherwise a configured default, otherwise the code is omitted so Exact applies the customer's own default. HubSpot calculates the amounts and the integration transmits them; Exact does not recalculate. We deliberately did not hardcode a fallback rate the way we did for GL, because the correct VAT is jurisdiction-specific and a wrong hardcoded rate would misclassify tax. Deferring to Exact's default is the safe answer.
Credit notes for free
Credit notes reuse the entire pipeline with one change: the entry type flips to Exact's credit-note value, amounts post as negative while quantities stay positive, and the tax code translates through a lookup table mirroring the GL table. Because the pipeline is invoice-unit and field-mapping driven, a credit note is a small, well-understood extension rather than a parallel system.
Idempotent by design
Strictly one HubSpot invoice to one Exact entry. The integration checks whether an entry with the same reference already exists and skips records that already carry an Exact identifier, so a retry or a double trigger cannot create a second entry. On success it writes the Exact entry identifier, the human-readable number, the sync status, a timestamp, the run type and any error back to HubSpot. The field mapping is strict: an unbound write-back field fails loudly and names the field, rather than silently dropping the value.
Built to be operated by humans
A finance integration is judged less by its happy path than by what it does when something is wrong, and by whether a non-engineer can understand what it did.
Dry-run previews. Any invoice can be previewed without posting. The dry run executes the whole pipeline up to, but not including, the post, and returns exactly what would be sent, using the same builder as the live path, so what you preview is literally what would be sent. It works with live sync switched off, which is exactly when you most want to validate mappings.
Human-readable everything. Exact identifies accounts by opaque internal identifiers. We added a reverse lookup that turns each identifier back into its account code and name, and a tag that flags any line which fell back to the safety-net account. That single readability change turned a five-minute cross-reference into a glance.
A forensic snapshot per sync. Every sync stores the HubSpot context it resolved, the payload it built, Exact's response and the stage at which anything failed, expandable on the invoice page. When a sync fails at the debtor stage, you see the exact context that produced the failure.
Feature flags with real defaults. Anything that writes to the live accounting system and was not ready ships dormant behind a flag that defaults off. Contact export and payment reconciliation both shipped inert, to be exercised first in dry run, then against a disposable test debtor.
Security
Operational secrets are encrypted at rest in PostgreSQL with AES-256-GCM, not left in environment variables, with only masked previews in the UI. Every secret change, retry and toggle is audit-logged. Webhook tokens compare in constant time. Dashboard sign-in uses magic links with a verification step designed so corporate email scanners cannot burn the link, and access is role-based. Exact's OAuth tokens rotate on refresh and persist atomically, so a restart never lands the integration in a broken token state.
Ready for the second entity, without paying for it yet
The client's next phase adds a second legal entity in another country. The architecture treats an entity as a namespace: division, journal, GL and VAT tables, default codes and field mappings are all per-entity, with the nightly job iterating entities. Phase 1 ships as a single-entity instance of that shape, so Phase 2 is additive rather than a rebuild.
What we achieved
- Posts approved, genuinely sent invoices from HubSpot into Exact Online US as journal entries, with debtors created or updated just in time, no item master required and no duplicate finance records.
- Routes every line to the correct GL account and VAT code, and can never post a line without a GL.
- Handles credit notes through the same pipeline with the correct sign convention and tax treatment.
- Is idempotent, so a retry or a double trigger cannot create a second entry.
- Writes sync status, identifiers and errors back to HubSpot so finance has one place to look.
- Ships with a full operating surface: dry-run previews, forensic snapshots, human-readable GL, a live eligibility view, pre-flight checks, an audit log and encrypted secret management.
- Runs on a single lean image with no queue tier, and is already shaped for a second entity.
The engineering lessons worth keeping
Match the accounting model, not the tutorial. The temptation was to post invoices because that is the mental model. The client's Exact posts journal entries. Following the real accounting model removed an entire category of work and made credit notes trivial.
Make the invisible visible. The two most valuable changes in the build were pure visibility: readable names for opaque identifiers, and a forensic snapshot of every sync. Neither changes what is posted; both dramatically shorten the time to understand what happened, which in a finance integration is most of the job.
Fail loud where it matters, and never leave a hole. Strict field mapping that fails with a specific message beats a silent fallback that posts the wrong thing. Where a value genuinely must never be blank, a baked-in safety net with a logged warning beats a failure, as long as it is honest about when it fired.
Buy infrastructure when you need it, not before. No queue, no worker fleet. A durable run log and in-process retries gave reliability and auditability at a fraction of the operational cost, with a clean upgrade path if scale ever demands more.
This build is part of how we approach every integration: real software, not workarounds. See how we connect HubSpot to finance systems, browse the Exact Online integration in our library, or read our complete guide to HubSpot integrations. Builds of this class start from around £10,000 fixed price; our guide to custom HubSpot integration costs explains how that price is shaped. Ready to talk specifics? Request a quote.
Frequently asked questions
Can HubSpot integrate with Exact Online?
Yes. There is no deep native connection for every accounting model, but a custom integration can post approved HubSpot invoices into Exact Online as journal entries, create and update debtors automatically, and write sync status back to HubSpot. The build described here is exactly that.
Do you need Commerce Hub to invoice from HubSpot?
No. This build ran without Commerce Hub and without an item master. Invoice data prepared in HubSpot was posted to Exact Online as General Ledger driven journal entries, with a mapping layer instead of a product catalogue.
Why post journal entries rather than sales invoices?
Because that matched the client's real accounting model. Their Exact Online administration posts straight into the sales journal with every line driven by a GL account, so the integration posts SalesEntries. Following the real model removed item matching entirely and made credit notes trivial.
How are credit notes handled?
Through the same pipeline as invoices with one change: the entry type flips to Exact's credit-note value, amounts post as negative while quantities stay positive, and the tax code translates through a lookup table. No parallel system needed.
What does a custom finance integration like this cost?
This class of build starts from around £10,000 fixed price, scoped after a discovery conversation. Our guide to custom HubSpot integration costs covers how the price is shaped.
How is a finance integration like this secured?
Credentials and tokens are encrypted at rest with AES-256-GCM rather than left in environment variables, every secret change and retry is audit-logged, webhook tokens are compared in constant time, and OAuth tokens rotate and persist atomically so restarts cannot break the connection.
Stay Updated with Our Latest Insights
Get expert HubSpot tips and integration strategies delivered to your inbox.

