NCR CounterPoint to HubSpot with Skyvia, no custom code
An e-commerce bag manufacturer running NCR CounterPoint
- Manufacturing and e-commerce
- United States
- Managed sync, running since 2026
- Orders backfilled since July 2020
- 20,004
- Order lines under sync
- 38,559
- Source views read
- 8
- Fields mapped across five objects
- 86
- Jobs in the nightly chain
- 8
Sales ran in HubSpot while every order, price and inventory count sat in a point of sale database on the client’s own network, reachable only from inside it. A managed sync now carries customers, products, ship to addresses, orders and order lines into the portal on weekday evenings, keyed on the identifiers the source system already uses. Paid invoices advance their own deals to closed won. Nothing we host sits in the data path.
01 The Challenge
The point of sale runs on a SQL Server inside the client’s network, in a data centre, and it cannot be exposed to the internet. There is no listing for it in the HubSpot App Marketplace, so the database is the only interface that exists. It holds more than 724 base tables, over 20 of them involved in a single transaction.
Three routes were open. The reseller could build a REST API, scoped at roughly four weeks of their development time, which puts a third party in the path of every future change. They could schedule CSV exports, which still leaves every mapping decision to make. Or we could read the database directly and accept the join complexity.
The schema survey settled it. The database ships 263 pre-built views that already flatten the joins, so the work needed no query design, no vendor build and no write access of any kind. Eight of those views carry everything the CRM needed.
One constraint outlasted all of that. Six of the ten deal stages the sales team wanted describe work that happens before an order exists: quoting, samples, test runs, artwork. The point of sale knows nothing about any of them, so whatever we built had to write some stages and keep its hands off the rest.
02 The Solution
Eight views feed five objects. Customers become companies keyed on the customer number rather than the company name, which changes whenever the back office tidies a record. Ship to addresses become a custom object keyed on the customer number joined to the address id, because one customer can have several, though only 27 percent of orders name one. Products become products keyed on the SKU. Orders become deals keyed on the ticket number. Order lines become line items keyed on the ticket number joined to the line sequence.
Associations resolve by looking the business key up in HubSpot instead of carrying an internal record id, so there is no separate association pass. That only works when parents land before children, which is why every job runs in one chain rather than on its own schedule.
Deal stages shaped everything else. The sync writes only the four stages the source can prove, each derived from which view an order appears in: open, fulfilled, cancelled, and paid. It never writes the six stages a rep sets by hand. The step that handles open orders was later split in two for that reason, which is why seven numbered steps run as eight jobs: one pass closes out cancelled orders, and a separate pass creates new open orders and updates their fields without writing a stage at all. A deal a rep moved to artwork approval yesterday is still there after tonight’s run.
Closed won is derived, not synced. The accounts receivable view carries one row per invoice, and a zero balance on an invoice whose document number matches the deal advances that deal. Nobody retypes a payment into the CRM.
Three exclusions run on every step that touches them: eight retired customer records, eight deprecated category codes, one province written out in full.
03 The Outcome
The backfill loaded 20,004 orders reaching back to July 2020 along with 38,559 order lines, and the company record now carries the accounts receivable balance and its aging buckets beside them. A rep asking what a customer bought last spring reads it rather than asking someone to look.
Two operational facts came out of running it. The largest step, the order lines upsert, bursts through the portal read budget, so the step after it now waits 15 minutes instead of failing on a rate limit. And a run that fails without moving a single row is a connection that never opened, which is safe to retrigger, while a run that fails after moving rows is a data problem and is never retried automatically.
The run window moved for the same kind of reason. Starting in the early evening puts the whole chain, including a long night, clear of the overnight maintenance window on the client’s network that had been dropping the source connection mid run.
How the lifecycle runs now
-
Before the order
Sales rep, by hand in HubSpot
- Rep sets six pre-order stages, such as samples and artwork Next: The sync never writes these six stages.
- The sync never writes these six stages Next: Order posted in the point of sale database, in Order posted.
-
Order posted
Point of sale, inside the network
- Source: Order posted in the point of sale database Next: Platform reads the views through the agent, outbound only.
-
In Skyvia: Platform reads the views through the agent, outbound only Next: Companies, products, shipping locations load first, in Order in HubSpot.
-
Order in HubSpot
Skyvia jobs, parents before children
-
In Skyvia: Companies, products, shipping locations load first Next: Deals keyed on ticket number, open orders write no stage.
-
In HubSpot: Deals keyed on ticket number, open orders write no stage Next: Order names a ship to address?.
- Decision: Order names a ship to address? Yes: Deal linked to its shipping location. No: Left unlinked, ships to the billing address.
-
In HubSpot: Deal linked to its shipping location Next: Order lines upsert as line items.
- Left unlinked, ships to the billing address Next: Order lines upsert as line items.
-
In HubSpot: Order lines upsert as line items Next: Waits 15 minutes for the portal read budget, in Invoice paid.
-
-
Invoice paid
Accounts receivable job, last
- Waits 15 minutes for the portal read budget Next: Reads invoice balances, matched on document number.
-
In Skyvia: Reads invoice balances, matched on document number Next: Zero balance advances the deal to closed won.
- Outcome, done: Zero balance advances the deal to closed won
04 Stack
A managed cloud sync platform does the data movement. A lightweight agent runs on a Windows machine inside the client’s network and opens outbound HTTPS connections only, so no inbound firewall rule was needed and the database stays unreachable from outside. The SQL login is read only, and nothing is ever written back to the point of sale.
The one piece we wrote is a scheduled job that triggers each step in order and watches it to completion before starting the next. It attaches to an already running step rather than starting a second copy, treats a failure that loaded almost every row as a success, and raises an alert on a hard failure.
Operations is three things: the platform’s per record error log, the run history for each step, and a fixed order that puts a parent record in place before anything that points at it.
05 Architecture and data model
The picture is one line with a fan at the end. The point of sale database is read through the on-premises agent by the sync platform, which runs eight jobs in a fixed order and writes one HubSpot object per job. The only edge that runs backwards is the last one: it reads invoice balances and updates deals an earlier step created.
Every step is an upsert on its own key, so replaying a night that only half finished changes nothing that already landed correctly. A step that fails leaves the chain stopped rather than letting the next one run without its parent, and the fix is to rerun the chain from that step. Direction is one way throughout: nothing is written back to the point of sale, which is why the SQL login is read only rather than merely unused for writes. The one derived value that travels the other way is the deal stage, and it moves inside HubSpot.
One association the source cannot always supply is the link from a deal to a shipping location. The address id is set on 27 percent of orders, and the rest ship to the billing address, so the sync creates that link only where the source populates it and leaves the deal unlinked otherwise. Every other association key is populated on every row.
Eighty-six fields are mapped across the five objects. The excerpt below gives the match key and one or two fields per object rather than the whole sheet.
| Source view | HubSpot object | Direction | Match key |
|---|---|---|---|
| Customers with address | Company | To HubSpot | Customer number |
| Ship to addresses | Shipping locations, custom object | To HubSpot | Customer number and address id, linked to a deal on 27 percent of orders |
| Items with inventory | Product | To HubSpot | SKU |
| Ticket history | Deal | To HubSpot | Ticket number |
| Open orders and order history | Deal, stage only | To HubSpot | Ticket number |
| Ticket line items | Line item | To HubSpot | Ticket number and line sequence |
| Accounts receivable transactions | Deal, stage only | To HubSpot | Document number matched to ticket number |
| HubSpot property | Source column | Object | Note |
|---|---|---|---|
| Company id | CUST_NO | Company | The match key, held in a custom property |
| Payment terms | TERMS_COD | Company | Populated on all but one record |
| Accounts receivable balance | BAL | Company | Aging buckets follow in five more fields |
| SKU | ITEM_NO | Product | Also the association key from a line item |
| Available quantity | QTY_AVAIL | Product | On hand less committed, calculated at source |
| Order date | ORIG_ORD_DAT | Deal | The date placed, not the date invoiced |
| Amount | TOT | Deal | Order grand total |
| Extended price | EXT_PRC | Line item | Quantity times unit price |
| Print description | PROMPT_ALPHA_1 | Line item | Populated on 29 percent of lines |
| Shipping method | SHIP_VIA_COD | Shipping location | Default method for that address |
- Hosting
- Managed cloud sync platform, with an agent inside the client's network
- Runtime
- Scheduled sync packages, plus a scheduled job that chains the runs
- Cadence
- Weekday evenings, after the day's posting, eight jobs in a fixed order
- Pattern
- Managed sync over SQL
- HubSpot
- Sales Hub Enterprise, Commerce Hub Professional, Data Hub Starter
- Fields mapped
- 86
Eighty-six mapped fields across five objects, every one of them keyed on the identifier the point of sale already uses rather than on a name.
What we would do again, and differently
What we would do again
- Key every object on the source identifier from the first run, because every name in this data changes at some point and no identifier ever did.
- Decide which stages the sync is allowed to write before building any of it, so a stage a rep sets by hand survives every night that follows.
What we would do differently
- Chain the steps from one place on day one, rather than trusting staggered start times to keep parents ahead of the records that point at them. Queue delays make that assumption fail quietly.
- Measure how long the largest step takes on a full load before choosing the run window, not after a long night ran into the maintenance window on the client’s network.
Systems in this build
Partnerships and accreditations
HubSpot accreditations
Industry specializations
Partnerships
Have a system in the same shape?
Send the systems, the direction and the record volume. We reply with a scope and an effort band within one business day.
Thanks, we will reply within one business day
Prash or Sean reads every one of these and comes back with a scope, or with the one question we need answered first.