What does a defensible custom sales tax API integration architecture look like?

A defensible custom sales tax API integration architecture has four named layers: an order-system caller (Shopify checkout extension or app server), an integration service that calls the tax engine, an ERP write-back that records calculated tax on the invoice of record, and a reconciliation pipeline that compares the calculation log against the ERP record. Observability primitives ship with the system, not after the first incident.

Last updated: Sep 24, 2026 Sales Tax at Scale Team

Key takeaways

  • The four layers are non-negotiable. Order system, integration service, ERP write-back, and reconciliation. Skipping the reconciliation layer is the failure mode that surfaces during the first audit.
  • Where the code lives is an ownership decision, not just a technical one. Code in a Shopify Plus checkout extension is owned by the storefront team, code in an ERP customization (SuiteScript, AL extension, Acumatica customization, Sage Intacct Web Services consumer) is owned by the finance-systems team, code in a dedicated integration service is owned by platform engineering.
  • Six observability primitives ship before launch, not after the first incident: structured logging, distributed tracing (OpenTelemetry), idempotency keys, exponential backoff with jitter, rate-limit and circuit-breaker handling, and a written runbook with alerting on error rate and fallback activation.
  • The reconciliation pipeline is part of the architecture, not a bolt-on. A custom integration that calculates and writes back but cannot reconcile the calculation log against the ERP record at month-end is incomplete, because the audit asks for that reconciliation.
  • Per-ERP integration shape varies materially. NetSuite uses SuiteScript or a SuiteApp, Microsoft Dynamics 365 Business Central uses an AL extension, Acumatica uses a customization project, Sage Intacct exposes Web Services, and QuickBooks Online uses an App Store connector with the IPP SDK.
  • The 13,000+ jurisdiction surface is what the integration calls, not what it builds. Custom build means custom integration against a vendor calculation engine, not custom calculation.

The reference architecture, end to end

A custom sales tax API integration is four layers in sequence, each persisting a specific data artifact. Conflating any two of them, or skipping the reconciliation layer entirely, is the source of every "we passed the audit but it took three weeks to reconstruct the records" story.

Layer
Responsibility
Persisted artifact
1. Order-system caller
Captures buyer ship-to address and line items, fires the calculation request
Cart or order record with line-level taxability hints
2. Integration service
Calls the tax engine API, handles retry, idempotency, and fallback, returns the calculated tax to the order system
Calculation log entry: request ID, idempotency key, jurisdiction stack, rate components, response timestamp, rate-table version
3. ERP write-back
Posts the invoice or sales order to the ERP of record with the calculated tax fields populated
ERP invoice or sales-order record with tax breakdown by jurisdiction and line item
4. Reconciliation pipeline
Compares the calculation log against the ERP record at month-end, surfaces discrepancies
Reconciliation report with matched, unmatched, and delta-flagged transactions

Layer 1 fires the request

On Shopify Plus, that is a checkout UI extension or a Shopify Function attached to the cart calculation event; on a custom checkout, it is the app server intercepting the place-order handler. The payload carries the buyer's full ship-to address (state, county, city, ZIP, geocoded coordinates where available), each line item's SKU and amount, and any taxability hints the catalog encodes.

Layer 2 is the thin integration service

It accepts the calculation request, attaches an idempotency key tied to the order ID, calls the tax engine API, and returns the resolved tax. The response carries the jurisdiction stack, rate components per level, and a rate-table version identifier persisted with the calculation log. The log is the audit artifact: order ID, request ID, idempotency key, full jurisdiction breakdown, rate-table version, response status, and the timestamp the calculation was authoritative.

Layer 3 is the ERP write-back

The calculated tax flows from the integration service to the ERP as part of the invoice or sales-order creation: NetSuite via SuiteScript or the SuiteTalk REST API, Business Central via an AL extension or the Business Central API, Acumatica via a customization project hooking the SOOrderEntry graph, Sage Intacct via the Web Services API, or QuickBooks Online via the Intuit Developer SDK. The ERP record becomes the financial source of truth.

Layer 4 is the reconciliation pipeline

Run as a scheduled job at month-end or, above 500 daily orders, nightly. It joins the calculation log on order ID against the ERP record and flags any mismatch in tax amount, jurisdiction, or applied rate. This is the layer most custom builds skip, and it is the one the audit asks for.

Where the integration code physically lives

At TaxCloud we have watched a recurring pattern: the integration that has no clear owner is the one that rots after the engineer who built it leaves. Where the integration code physically lives is an ownership decision before it is a technical one.

Four placements, each implying a different owning team for the runbook.

  1. Shopify Plus checkout extension or Shopify Function. Code lives in a UI extension package or a Shopify Function deployed through Shopify CLI. Storefront engineering owns it. Strengths: latency is sub-200ms because the calculation runs inside Shopify's checkout pipeline, and the deployment cycle matches the storefront release cadence. Constraints: the extension has limited runtime, no persistent state, and cannot make arbitrary outbound calls without going through Shopify's permitted endpoints. The integration service typically lives elsewhere; the checkout extension calls it.
  2. App server (custom checkout). Code lives in the order-management service, typically a Node, Go, or Python service the platform team already runs. Platform engineering owns it. Strengths: full control over retry, idempotency, and circuit-breaker logic. Constraints: the service is now in the synchronous checkout request path, and every tax engine latency spike becomes a checkout latency spike.
  3. Dedicated integration service. A standalone microservice whose only job is the tax engine call, the calculation log, and the ERP write-back. Platform engineering owns it. Strengths: independent deployment, clearer observability boundaries, easier ownership story when the order system and the ERP are owned by different teams. Constraints: an additional network hop in the request path and an additional service to operate.
  4. ERP customization. Code lives inside the ERP itself: SuiteScript inside NetSuite, an AL extension inside Business Central, a customization project inside Acumatica, a Web Services consumer wired into Sage Intacct. Finance-systems engineering owns it. Strengths: the tax calculation is structurally bound to the invoice of record, eliminating one whole class of write-back drift. Constraints: the ERP's customization model dictates the deployment cycle, the testing model, and the upgrade exposure.

Two failure modes recur. First, splitting code across placements without a single owning team: three teams hold three runbooks, none of which references the others. Second, placing the code where the deploy cadence does not match how often rate-table or jurisdiction updates ship.

The tax engine the integration calls is the boundary the layers above are designed around. TaxCloud calculates across 13,000+ jurisdictions through one API, which is the worked example of the API surface a custom integration is built to consume: a single endpoint, documented rate limits, a stable response schema, and the idempotency contract the integration service relies on.

Observability primitives that ship before launch

At TaxCloud we have watched enough custom integrations come into production to name the trend: the integration that survives is the one with the observability primitives in place before launch, not after the first incident. Teams that shipped without them spent the first Black Friday debugging blind, because a custom integration with no traces is a black box at exactly the moment it fails. Two engineering weeks at design time costs less than the incident plus the next quarter of trust with finance.

The six primitives that must be in place at launch:

  1. Structured logging. Every request through Layers 2 and 3 emits a structured JSON log line with the order ID, the request ID, the idempotency key, the tax engine response status, the latency, and the rate-table version returned. Free-text logs are not auditable; structured logs are searchable and joinable against the ERP record during reconciliation.
  2. Distributed tracing. OpenTelemetry instrumentation across Layer 1 through Layer 3 produces a single trace per checkout, with spans for the order-system call, the tax engine API call, the ERP write-back, and any retry attempts. When latency spikes, the trace shows which span caused the spike. Without it, root-cause analysis devolves into log correlation by timestamp.
  3. Idempotency keys. Every calculation call carries an idempotency key tied to the order or cart ID. If a network failure causes a retry that arrives after the original request already succeeded, the tax engine returns the same result for the same key rather than computing a second calculation, which would otherwise produce two ledger entries for one transaction and a filing discrepancy at reconciliation.
  4. Exponential backoff with jitter. Retries against the tax engine use exponential backoff with random jitter, capped at three attempts within the checkout latency budget. Jitter prevents the thundering-herd condition where concurrent failed checkouts all retry on the same schedule, amplifying load on a degraded engine.
  5. Rate-limit and circuit-breaker handling. A circuit breaker counts consecutive 429 responses; when the count crosses a threshold (typically 5 within a 10-second window), the circuit opens and routes subsequent requests to the fallback for a reset window of 30 to 60 seconds. Without it, a rate-limit response cascades into a self-perpetuating failure.
  6. Alerting and a written runbook. Alerts fire on tax engine error rate above a threshold, on fallback activation, and on Layer 4 reconciliation discrepancies above a defined tolerance. The runbook names the owning team for each layer, the escalation path, and the rollback procedure. A custom integration without a runbook is an integration without an owner during the incident, which is the wrong time to learn that fact.

A miscalculation that ships into the ERP without observability becomes an audit finding defended without a clear trace of what the system did and why. The primitives above are not engineering polish. They are the audit-defense surface in the engineering layer.

The reconciliation pipeline as part of the architecture

The reconciliation pipeline is part of the architecture, not a bolt-on. A custom integration that calculates and writes back but cannot reconcile the calculation log against the ERP record at month-end is incomplete, because the audit asks for that reconciliation and a custom build is the one most likely not to have it.

The job is straightforward in shape and unforgiving in detail. At month-end (or nightly at high volume), pull the calculation log for the period from the integration service and pull the corresponding invoice or sales-order records from the ERP. Join on order ID. For each match, compare the calculated tax (Layer 2 result) against the recorded tax (Layer 3 ERP record). Three outputs:

  • Matched. Calculation log and ERP record agree within a defined tolerance (typically zero variance, occasionally a penny for rounding). These close.
  • Unmatched. A calculation log entry has no corresponding ERP record, or an ERP record has no corresponding calculation log. The most common causes: an order that was cancelled before the ERP write-back fired, a retry that produced a duplicate log entry without an idempotency key, or an ERP record posted manually outside the integration path.
  • Delta-flagged. Both records exist but the tax amount differs. The most common cause is a taxability override applied manually inside the ERP after the calculation. The delta queue is reviewed by a finance operator; engineering supplies the structured-logging context that makes each review tractable.

Three recurring sources of drift to instrument explicitly. First, timing differences between checkout completion and ERP invoice-post can land orders in different reconciliation periods if the join uses ERP-post date rather than order date.

Second, SKU-mapping mismatches between platform product IDs and ERP item IDs surface as exemption-status differences (a SKU coded taxable on Shopify but exempt in NetSuite, or vice versa). Third, returns and refunds land as separate ERP records and must be joined back to the original transaction explicitly; otherwise the reconciliation flags both as unmatched.

The tolerance threshold belongs to finance, not engineering. At 50,000 monthly transactions, a 0.1% discrepancy rate is 50 orders requiring manual review. The right pattern is to set a dollar-value tolerance per order and a count tolerance per jurisdiction, close anything below the line automatically, and route the rest to a queue that finance works through by the third business day of the month. That window is what creates the room to file before the typical 20th-of-following-month deadline in monthly-filing states.

This is also where the integration target matters at the architectural level. TaxCloud exposes the transaction-level calculation log through a reporting API, which is what Layer 4 pulls against to produce the reconciliation report without a manual data assembly step.

How the architecture differs by ERP

The four-layer shape is constant. The Layer 3 write-back and the Layer 4 reconciliation surface against the ERP shifts materially across the mid-market systems. The table below names the integration shape for each. The full per-ERP gotcha treatment lives in the per-ERP guides linked below.

ERP
Write-back mechanism
Reconciliation surface
Customization model
Where the code is owned
NetSuite
SuiteScript 2.x user event script on Sales Order or Invoice; SuiteTalk REST or SOAP for external services; published SuiteApp for productized integrations
Saved Search or SuiteAnalytics dataset over the period; SuiteScript scheduled script writes the join output to a custom record
SuiteScript 2.x, SuiteFlow, SuiteApp Bundles; deploys through SuiteCloud Development Framework
Finance-systems engineering with SuiteScript expertise
Microsoft Dynamics 365 Business Central
AL extension hooking the Sales Header / Sales Line tables; Business Central API endpoints for service-to-service writes
API queries against salesInvoices and salesOrders; report extension or background task for the join
AL extension language, deployed through Business Central admin center
Finance-systems engineering with AL expertise
Acumatica
Customization project hooking the SOOrderEntry or ARInvoice graph; published REST API as an alternative entry point
Generic Inquiry over invoice tables; customization-project scheduled task
Acumatica Customization Projects, deployed through the Customization Project Editor
Finance-systems engineering with Acumatica customization expertise
Sage Intacct
Web Services API (XML) called from an external integration service; native Sage Intacct triggers for synchronous server-side hooks
Web Services queries against SOTRANSACTION and ARINVOICE for the period
External integration service consuming Web Services; minimal native customization model
Platform engineering (the customization model is thin, so the integration service typically owns the relationship)
QuickBooks Online
App Store connector built against the Intuit Developer SDK; OAuth 2.0 application calling Invoice and SalesReceipt endpoints
Query API against the Invoice and SalesReceipt entities for the period
App Store connector model; no native scripting language inside QuickBooks Online
Platform engineering owning an external connector

Two patterns to name. First, NetSuite, Business Central, and Acumatica all support meaningful code-inside-the-ERP customization, which makes Placement 4 (the ERP customization placement described above) genuinely available. The trade-off is that the ERP's deployment cycle, testing model, and upgrade path govern the integration's lifecycle. Second, Sage Intacct and QuickBooks Online both push the integration outside the ERP boundary, which makes Placement 3 (the dedicated integration service) the dominant choice. There is no customization layer to live inside.

A separate constraint worth surfacing: NetSuite, Business Central, Acumatica, and Sage Intacct are not native TaxCloud integrations. The reference architecture applies to any of them, and the worked example of the tax engine the integration consumes (single-API calculation across 13,000+ jurisdictions, with a stable response schema and a documented reporting API) holds regardless of which ERP sits at Layer 3. QuickBooks Online has a native TaxCloud App Store connector, which removes the custom-build decision for that ERP unless the brand's catalog or channel mix specifically requires it.

What this means for the engineering org at $20M to $80M

A brand at $20M to $80M on Shopify Plus or custom checkout, running NetSuite, Business Central, Acumatica, Sage Intacct, or QuickBooks Online, arrives at this guide because build-vs-buy has already landed on build. The architectural decisions that follow determine whether the integration is an asset or a liability inside 18 months.

Three things this audience holds in hand by the end of the architecture phase. First, a named owner for each of the four layers. Storefront engineering owns Layer 1 in a Shopify Plus context; platform engineering owns Layer 2 and (where placement runs through a dedicated service) Layer 3; finance-systems engineering owns Layer 3 in NetSuite, Business Central, or Acumatica; finance owns Layer 4's tolerance thresholds while engineering owns its instrumentation. Second, the six observability primitives in place before the first calculation ships. Third, the reconciliation pipeline live before the first month-end close, not retrofitted before the first audit.

The tax engine the integration calls is the boundary the rest of the architecture is designed around. The reader here has already decided to build the integration; the question is what the integration target looks like. TaxCloud is built for that: a single API call resolving across 13,000+ jurisdictions, a reporting API the Layer 4 reconciliation pipeline pulls against directly, a calculation log structured for audit production, and Certified Service Provider filing across the 24 SST member states as the brand's filing footprint expands.

FAQ

Common questions

How does a custom sales tax API integration architecture differ from using a native ERP connector or an iPaaS middleware integration?

A custom integration owns the code in Layers 1 through 4 directly: the order-system caller, the integration service, the ERP write-back, and the reconciliation pipeline. A native connector ships Layers 2 and 3 as a packaged product with a vendor on the hook for breakage. An iPaaS middleware integration (Celigo, Workato, Boomi) owns the orchestration of Layers 2 and 3 as recipes but typically still requires the brand to own reconciliation. Custom buys flexibility and observability at the cost of ownership.

Where should the calculation log be persisted: inside the integration service, inside the ERP, or in a separate datastore?

A separate datastore (typically the integration service's own database) is the right default. The calculation log is the audit artifact and the reconciliation source; binding it to the ERP couples its retention and query model to the ERP's, which is rarely tuned for the audit-window retention sales tax requires. The log should retain the full request, the full response, the idempotency key, and the rate-table version for at least 7 years (the conservative default across a multi-state footprint, given some states assert audit windows up to 7 years).

Does a Shopify Plus brand still need Layer 3 (ERP write-back) if Shopify is the system of record?

If the ERP is the financial system of record, yes. Shopify Plus is the order management surface for the direct channel, and Shopify Tax produces a calculation at checkout, but the invoice of record for accounting and filing typically sits in NetSuite, Business Central, Acumatica, Sage Intacct, or QuickBooks Online. Layer 3 is what carries the calculation from the order system to the financial source of truth. Brands where Shopify is the only authoritative record (no separate ERP) can collapse Layers 3 and 4 against the Shopify Orders API directly.

How do webhooks fit into this architecture?

Webhooks are how Layer 1 fires Layer 2 in an async pattern. The order-system emits an order/create or order/paid webhook; an integration-service endpoint receives it, fires the tax engine call, and writes back to the ERP. The pattern works well for B2B and subscription contexts where the checkout latency budget does not apply. For DTC checkout where the buyer sees the tax amount before submitting, calculation must remain synchronous; webhooks are useful for Layer 3 write-back but not for Layer 1 to Layer 2.

What is the minimum engineering capacity required to maintain a custom integration after launch?

One dedicated engineer with backup, plus a finance-operations partner who owns the Layer 4 reconciliation queue. The engineer carries the integration-service runbook, rate-table version monitoring, the per-ERP customization (where placement is inside the ERP), and the tax engine API upgrade cycle. Below that capacity, no single person knows the full request path, which is the failure shape that turns a 3-hour incident into a 3-week reconstruction.

What changes during an ERP migration (e.g., QuickBooks Online to NetSuite)?

Layer 3 changes shape entirely; Layers 1, 2, and 4 are mostly stable. The integration service continues calling the tax engine and persisting the calculation log; the write-back target moves from the QuickBooks Online Invoice endpoint to a NetSuite SuiteScript or SuiteTalk write. The reconciliation pipeline reorients its ERP-side query against the new ERP's invoice tables. Plan the migration with a parallel-write window: write to both ERPs for a defined period, reconcile both, and cut over once the new ERP's reconciliation report matches the old one.