Yardi Voyager to HubSpot through a Microsoft Fabric lakehouse
A five-brand senior living operator in Atlantic Canada
- Senior living
- Atlantic Canada
- Integration, live since 2026
- Contacts under sync
- 72,000
- Deals in the acquisition pipeline
- 45,000
- Timeline events sent
- 260,000
- Properties reconciled each run
- 133
- Live since
- August 2026
A senior living operator running five brands kept every prospect, tour and resident record in Yardi Voyager, and ran marketing in HubSpot with none of it. We built a two way sync through a Microsoft Fabric lakehouse inside the operator’s own tenant. It reads Yardi every 6 hours, writes website leads, contact edits and marketing activity back hourly, and keys every record on the Yardi identifier rather than on the email address.
01 The Challenge
The operator runs five brands across a portfolio of senior living communities. Every enquiry, tour, move in and move out is recorded in Yardi Voyager. The marketing team worked in HubSpot and could see none of it, so campaign lists were built by hand from exports, and nobody on either side could tell whether a person had already been contacted.
The source system decided most of the design. The senior housing interface is SOAP, and it is a fixed contract: it exposes the operations it exposes, and there is no route around them. It has no webhooks and no changed since push, so anything in the middle has to poll and remember where it read up to. Email cannot be the identity key, because family members share an address and the source system opens a second card when a person returns after a gap. The interface has no operation that sets a prospect’s status, so the sync could move data in both directions but could never move a prospect through the funnel from the HubSpot side. And the source carries fields that must never reach a marketing system, so we built the mapping outward from an exclusion list rather than from everything the schema offers.
02 The Solution
We put a lakehouse between the two systems and gave it all the work. Inbound, a pipeline runs four notebooks in a fixed order, each gated on the one before it: property configuration first, then prospects, then residents, then the move events. Raw responses land in a bronze layer exactly as the source returned them, a silver layer holds the cleaned rows, and a gold layer holds the sync’s own memory. That memory is the watermarks, the ledgers, the property routing table and a conflicts table.
Contacts are upserted on the Yardi contact identifier and deals on the prospect identifier, both held as unique HubSpot properties. Prospect status sets the deal stage on creation only, so a consultant who moves a deal keeps that move. Lead status passes through as the source writes it, and a value that does not fit a HubSpot dropdown lands in the conflicts table for review, rather than failing the run or being dropped in silence.
Outbound is a staged pipeline. Three feeds collect work: submissions to forms on an allow list, edits to watched fields on contacts already linked to Yardi, and the marketing email sends and form fills that belong on the prospect card. Each change is staged in a queue, transformed, then put through four gates before anything is written. The gates are a master write switch, an environment lock that refuses a live write from the test path, a blackout guard for the source system’s maintenance window, and a portal gate that refuses HubSpot writes unless the connected portal is the production one. Every write is read back to confirm it landed, then recorded in a ledger, and that ledger is what makes a re-run safe.
03 The Outcome
The sync has run in production since August 2026. It holds about 72,000 contacts and about 45,000 deals in the resident acquisition pipeline, and it has sent about 260,000 timeline events, which is the prospect and tour history that used to exist only inside the property system. Each inbound run reconciles 133 properties against the marketing routing list, so a community that appears in the source but has not been classified for marketing shows up as drift rather than as silence.
Which communities sync is a row in that routing table, not a line of code, so switching one on is a configuration change and the next scheduled run picks it up. The first run for a new community brings its whole history at once, which is worth planning for. The conflicts table has proved the most useful of the gold tables in practice. It is where a vocabulary change in the source system surfaces, before anyone notices a gap in a report.
How the lifecycle runs now
-
Enquiry
Website forms in HubSpot
- Source: Website form submitted in HubSpot Next: Allow-listed form collected for the outbound queue.
- Allow-listed form collected for the outbound queue Next: Staged in the queue and transformed, in Lead in Yardi.
-
Lead in Yardi
Outbound, hourly, through four gates
-
In Microsoft Fabric: Staged in the queue and transformed Next: All four safety gates pass?.
- Decision: All four safety gates pass? Yes: Prospect created in Yardi, read back to confirm. No: Refused before anything is written.
-
In Yardi Voyager: Prospect created in Yardi, read back to confirm Next: The write is recorded in the ledger.
- The write is recorded in the ledger Next: New Yardi ids stamped back on the contact.
-
In HubSpot: New Yardi ids stamped back on the contact Next: Lakehouse reads Yardi every 6 hours, in Prospect in HubSpot.
- Refused before anything is written
-
-
Prospect in HubSpot
Lakehouse, inbound every 6 hours
-
In Microsoft Fabric: Lakehouse reads Yardi every 6 hours Next: Contact merged on the Yardi contact id.
-
In HubSpot: Contact merged on the Yardi contact id Next: Deal upserted on the prospect id, stage set once.
-
In HubSpot: Deal upserted on the prospect id, stage set once Next: Lead status fits the HubSpot dropdown?.
- Decision: Lead status fits the HubSpot dropdown? Yes: Lead status passes through as written. No: Value held in the conflicts table for review.
-
In HubSpot: Lead status passes through as written Next: A consultant moves the deal, and the move holds, in Tours and marketing.
- Value held in the conflicts table for review Next: A consultant moves the deal, and the move holds, in Tours and marketing.
-
-
Tours and marketing
Consultants and the marketing team
- A consultant moves the deal, and the move holds Next: Tour and activity history as timeline events.
-
In HubSpot: Tour and activity history as timeline events Next: Marketing email sends written to the prospect card.
-
In Yardi Voyager: Marketing email sends written to the prospect card Next: Resident status, move in date and care level, in Move in.
-
Move in
Residents and move events, inbound
-
In HubSpot: Resident status, move in date and care level Next: Move in arrives as a timeline event.
- Outcome, done: Move in arrives as a timeline event
-
04 Stack
The integration runs entirely inside the client’s own Microsoft Fabric tenant, on a small node pool with a dynamic executor range. The code is PySpark notebooks, with one shared utilities notebook loaded first by every other. That one holds the SOAP client, the HubSpot client, the credential fetch and the portal gate. Credentials come from a key vault at run time, so rotating one takes effect on the next run with no code change.
The inbound chain runs 4 times a day and takes about 2 hours. Activity history runs nightly on its own schedule, because its pulls are long. Outbound runs hourly, and skips cleanly inside the blackout windows. Operations is a two minute look at the run history, then the ledgers, because a run can report success without having moved anything.
05 Architecture and data model
The source is read on a schedule into the lakehouse, which is the only component that talks to both sides. Inbound, the lakehouse upserts contacts and deals and writes move and activity history as timeline events. Outbound, website form submissions and contact edits stage in a queue, pass the gates, and are written to the source, with every attempt and its result recorded in a ledger. Neither end calls the other directly, which is what lets either side be paused for maintenance without losing work. On resume, the watermarks say where to read from and the ledgers say what has already been sent.
| Source object | HubSpot object | Direction | Match key |
|---|---|---|---|
| Prospect contact | Contact | Two way | Source contact identifier |
| Prospect | Deal | To HubSpot | Source prospect identifier |
| Resident | Contact fields | To HubSpot | Source contact identifier |
| Move in, move out, transfer | Timeline event | To HubSpot | Event reference in the sent ledger |
| Prospect activity | Timeline event | To HubSpot | Activity reference in the sent ledger |
| Website form submission | Prospect | To source | The integration’s own external reference |
| Contact edit | Contact record | To source | Source contact identifier |
| Marketing email send | Prospect activity | To source | Engagement reference |
A lead created in the source from a HubSpot form is stamped straight back onto the HubSpot contact with its new identifiers, which is what stops the sync manufacturing duplicates. The next inbound run then merges it instead of creating a second record.
The object model maps 72 contact and deal properties in total. The 10 that carry the prospect and resident story are below. Phone numbers are written as digits only, because the source accepts a formatted number and stores nothing.
| Source field | HubSpot property | Object | Direction |
|---|---|---|---|
| ContactID | Yardi contact id | Contact | Two way |
| ProspectID | Yardi prospect id | Deal | Two way |
| PropertyCode | Yardi property code | Contact | To HubSpot |
| ProspectStatus | Deal stage | Deal | To HubSpot, on creation only |
| SourceName | Lead source | Deal | To HubSpot |
| HomePhoneNumber | Phone | Contact | Two way, digits only on write |
| MobilePhoneNumber | Mobile phone | Contact | Two way, digits only on write |
| Status | Resident status | Contact | To HubSpot |
| MoveInDate | Resident move in date | Contact | To HubSpot |
| CareLevelCode | Resident care level | Contact | To HubSpot |
- Hosting
- Microsoft Fabric, inside the client's own tenant
- Runtime
- PySpark notebooks over a lakehouse, chained by a pipeline
- Cadence
- Inbound every 6 hours, activity history nightly, outbound hourly
- Pattern
- Polling on a watermark
- HubSpot
- Marketing Hub Enterprise, Content Hub Enterprise, Data Hub Professional
- Fields mapped
- 72
72 contact and deal properties in the object model, keyed on the source contact identifier for contacts and the prospect identifier for deals.
What we would do again, and differently
What we would do again
- Key every object on the source identifier, never on the email address.
- Keep the list of which communities sync in a table the operator can edit, so adding one is a configuration change and not a release.
What we would do differently
- Prove every field end to end in a test environment before mapping it, rather than trusting the published schema.
- Agree which contact relationships are in scope before the first full load, not after it.
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.