Introduction
"When, where, and what did this customer buy?" — can you see that at a glance?
When your in-store POS and your EC order management live in separate systems, this is surprisingly hard to see.
For an apparel and gear EC business I worked on, I built a pipeline that syncs transaction data from a store POS (Smaregi) and an EC order manager (NextEngine) into the CRM (HubSpot) as Deals.
This article walks through the overall picture and the thinking behind the design.
Why hold sales as "Deal objects"
The customer 360 view
A CRM organizes everything around the customer (Contact). If you attach sales to a Contact as Deals, then just opening a customer's page shows you everything they have ever bought.
Rather than keeping sales as a flat aggregate table, you hold them as a set of transactions tied to each customer. That is the foundation of what people call the customer 360 view. Marketers can hold conversations with full purchase context, without any special effort.
LTV across store and EC
Some people buy in store, some buy online, and many do both — real customers move freely across channels. But when systems are split, you can never see a person's true total spend.
Consolidating transactions into one CRM lets you finally calculate cross-channel LTV (lifetime value). You can properly value a customer who looks small on EC alone but is a regular in-store. That really pays off when deciding where to focus your efforts.
A foundation for cross-sell
When purchase history is laid out as Deals and line items, you can build cross-sell conditions like "recommend B to people who bought A" directly with the CRM's segmentation tools.
For example, you could pull "people who bought a jacket last season but haven't bought gloves yet" and email them. Because sales data is structured as transaction objects, this kind of filtering becomes practical to run with reasonable effort.
What gets synced — the big picture
Breaking one transaction into three objects
A single transaction is broken into three kinds of objects in the CRM. The transaction itself becomes a Deal, the buyer becomes a Contact, and each purchased product becomes a LineItem.
By relating these three correctly, "who bought what, for how much" is preserved in a structured form. The detailed design of this breakdown is covered in the mapping sub-article.
The flow of data
The sync fetches transactions from POS/EC, transforms them, and writes them to the CRM. Let's lay out the order of processing to grasp the whole.
Pull transaction data for the target period from Smaregi and NextEngine
Look up a Contact by email or customer number, create one if none exists
Map the transaction to a Deal and product rows to LineItems
Associate the Deal with the Contact and attach LineItems to the Deal
We verify this pipeline safely with the Dry Run described later, then let it run for real.
The difficulty of identity matching
Matching on email and customer number
The most nerve-racking part of the sync is "identity matching" — correctly tying a buyer to an existing Contact. If the same person registered separately in-store and online, or changed their email, you can easily create them as a different person.
In this project, I use email as the primary key while also using the membership number (customer number) as a secondary key. Relying on either one alone leads to misses, so matching against multiple keys turned out to be the pragmatic answer.
Preventing duplicates
If matching fails and Contacts get duplicated, your hard-won customer 360 view gets fragmented. If store transactions hang off one Contact and EC transactions off another, LTV won't come out right either.
Fixing duplicates afterward is painful
Merging split Contacts is laborious, and it also forces you to redo associations. That's exactly why you should design the matching logic carefully at the entry point and confirm results in advance with the Dry Run below.
Mechanisms for safe operation
The reassurance of Dry Run
Writing straight to a production CRM is scary. The first sync in particular has a large volume, and a mapping mistake could create a flood of wrong Deals.
So I built a Dry Run mode that writes nothing and only reports "what would be created or updated." You review the results, confirm there are no issues, and then switch to the live sync. Just having this "chance to pause" makes operating the system feel completely different.
Daily sync and manual runs
For steady-state operation, a daily automatic sync runs via QStash (a scheduler), pulling in the previous day's transactions in a batch. On top of that, you can trigger a manual run from the admin screen at any time.
Running daily keeps the CRM always up to date, and manual runs let you flexibly handle situations like "I want to fill a gap right now" or "I need to re-sync a specific period."
Conclusion
The aim of syncing POS and EC transactions into CRM Deals is to make each customer's cross-channel purchase picture visible in one place. Structuring sales as transaction objects builds the foundation for LTV evaluation and cross-sell campaigns.
Three sub-articles dig deeper into this theme.
Designing Deal & Line-Item Data Mapping
How transactions map to Deals, Contacts, and LineItems, with property design explained.
Owner Mapping — Assigning Reps from Store Codes
Designing and operating the table that resolves store codes to HubSpot owners.
Safe Production Rollout with Dry Runs
A safe procedure for confirming results without writing, then switching to live sync.