Decide who owns what
"Two-way" doesn't mean every field flows both ways. For a dental clinic: the practice-management system owns appointments, treatment history and clinical notes; GoHighLevel owns marketing consent, lead source, tags and conversation history; contact details are owned by whichever system the patient updated last, with the other notified. We write this ownership table before touching an API, and it settles 90 percent of the design.
Sync on events, batch the rest
Where either system offers webhooks, changes sync within seconds: a booking in the practice system appears in GoHighLevel and triggers reminders; a new lead in GoHighLevel appears in the practice system as a prospective patient. Where there are no webhooks, a scheduled job polls every few minutes for changes since the last run. Bulk operations (initial load, corrections) run as batches overnight with rate limiting, following the patterns in custom code around your CRM.
Conflict rules
Two systems will occasionally disagree. "Last write wins" sounds simple and quietly destroys data when clocks differ or a batch job runs late. Our default: the owning system wins for its fields, always. For shared fields like phone number, the most recent human edit wins over any automated write, and the loser is logged as a note so nothing vanishes silently.
Families share emails. Businesses share phones. Matching contacts by email or phone creates merged records that are very hard to separate later. Store the other system's ID on each record and match on that. Fall back to email plus name only for the initial load, with a review list for anything ambiguous.
Stable IDs
Every GoHighLevel contact carries a custom field holding the industry system's record ID, and vice versa. All subsequent syncs match on those IDs. It is the single decision that prevents duplicates, and it has to be made before the first record moves.
The nightly report
Every night a job counts records on both sides, samples fifty and compares field by field, and emails a short report: counts, mismatches, and any records that failed to sync with the reason. Drift gets caught the next morning instead of three months later. The report is the same idea as the reconciliation step in our migration method, run continuously.
Starting one-way
We almost always launch one-way first: industry system to GoHighLevel, so marketing has clean data. Only when that has run cleanly for a few weeks do we enable the reverse direction, field by field. Two-way from day one is where most sync projects earn their reputation.
Key takeaways
- Each field has exactly one owner system. The other side is read-only for that field.
- Sync changes as they happen where the API allows; batch the rest on a schedule.
- Write the conflict rule before writing code: last write wins is rarely right.
- Store each system's ID on the other side, and never match on name or email alone.
- A nightly reconciliation report catches drift before a customer does.