CRM · Data · Dental lab

A CRM sync that wrote 3,244 fields and overwrote none.

Built for Carlos Ceramics Dental Lab, whose years of case history lived in one system while its doctor contacts lived in another.

Illustration: practice records from a case history merging into CRM contact cards, each change written to an audit log

Customer systems, at work.

See the customer systems services

Built
Practice profiles from case history, matched and merged into the lab's CRM
Stack
PythonGo High Level APICSV audit log
Timeline
August 2026. Profiles done August 13, written August 16 and 18
Status
Delivered. The records are in the lab's CRM
Scale
25,336 cases read, 325 practices profiled
324 of 325practices written into the CRM
3,244field writes, every one logged
0failures, 0 values overwritten
29roll-up cards for shared phones
The problem

Two lists about the same doctors that never met.

Two separate card files on opposite ends of a desk, one of case history and one of contacts, with a dashed line that does not meet in the middle.

The lab's case system knew which offices sent work, how often, and when they last did. Its CRM, where the doctors' contacts live, knew none of that. Anyone wanting to call an office that had gone quiet had to look it up in two places.

Joining them was not as simple as an import. Some practices share one phone number across several locations. Some records were duplicates. The lab's own status field was wrong in both directions. A careless sync would have overwritten good data with bad, or made one location look like a whole group.

What we built

A sync that can only add, and writes down everything it adds.

Seven pieces. The order matters: nothing can write until the data has been read, matched and shown in a dry run.

INPUTSTHE SYNCOUTPUTSLab systembackupTotals and datesonly. No patienttableCRM contactsGo High Level,where the labkeeps its doctors1PracticeprofilesCases sent, lastcase, client since2MatcherPhone number asthe key. Everymatch graded3Read-onlyclientThe file has noway to write5Roll-upcards29 groups thatshare one phone4Dry-runwriterWrites nothingunless told--live. Reads eachcontact first,fills blanks,never replaces avalue. Skips rowsalready done, so arerun is safe.Narrow tokenCannot send, startworkflows or pay6Do-not-writelistPatient fieldsstay untouchedDry run checkedThen it runs liveCRM records324 of 325practices updated7Audit fileOld value, newvalue, time andstatus for everyfieldguardraila person decides
How it fits together. Reading and writing live in different files. The only code that can write is the dry-run writer, and it changes nothing until the command says --live.
  1. Practice profiles from the lab's own backup

    analyze_backup.py reads the case history and keeps only totals: cases sent, last case date, client since. The patient table was never extracted, and tables we agreed not to open stayed closed. Each practice is judged against its own rhythm over at least four weeks, because a single cut-off for everyone fires on almost nobody.

  2. A matcher that grades every match

    Practices are matched to CRM contacts with the phone number as the key, and each match is graded before anything moves.

  3. A read-only client

    ghl_client.py talks to the CRM but contains no call that can create, change or delete anything. Reading cannot turn into writing by accident.

  4. One writer, dry run by default

    sync_write.py is the only code that writes. Without --live it only reports what it would do. Live, it reads each contact first and fills empty fields, never replacing a value that is already there. It skips rows already done, so it can be stopped and run again safely.

  5. Roll-up cards for shared phones

    Where several locations share one number, the CRM keeps one contact. Instead of letting one location speak for the group, those groups became 29 roll-up cards that say how many practices they cover.

  6. A do-not-write list

    Custom fields that patients fill in are on a permanent list the writer will not touch, whatever the command says.

  7. An audit file

    Every field change is appended to sync-audit.csv: the old value, the new value, the time and the CRM's answer. Any record can be traced back to the moment it changed.

The match, in numbers

Sure matches first. The rest waited.

We wrote the confident matches on day one and held back everything that needed a decision about duplicates and shared inboxes.

Match gradePracticesWhat happened
High confidence215Written on August 16: 2,150 fields
Weaker match40Held until duplicates were settled, then written August 18
Sharing a phone or email69Written August 18, grouped into 29 roll-up cards
No contact in the CRM1Left alone

The audit fileOne row per field changed

contactfieldold valuenew valuewritten atstatus
c_••••Cases sent(empty)••2026-08-16 ••:••200
c_••••Last case(empty)2026-••-••2026-08-16 ••:••200
c_••••Client since(empty)20••2026-08-16 ••:••200
The format, not real rows. Values are masked here. In the real file every write returned 200, and the old value column shows nothing was there to overwrite.
How it ran

Two batches, a dry run before each.

A big toggle switch set to
  • Dry run, then liveRun without --live, the writer lists every field it plans to fill and writes nothing. That list is what gets checked before anything goes live.
  • A narrow keyThe CRM token we used had no permission to send messages, start workflows or move money. Nothing was sent to any doctor, by design and by permission.
  • Counts and dates onlyThe CRM is not the place for case details, so none went in. The profile fields are totals and dates.
  • If a run stopsFinished rows are skipped, so a stopped run is simply started again. In this job there was nothing to redo: zero failures.
What we'd reuse

Any business with history in one system and customers in another.

An HVAC company with years of jobs in its field software and a separate marketing list. An accounting firm whose billing system knows who is active while the CRM does not. A property manager with leases in one place and owners in another. The parts carry over unchanged: read-only first, one writer, dry run, audit file, a list of fields nobody touches.

Is your customer history stuck in the wrong system?

Tell us where it lives and where you wish it was. We will say on a free call what a safe sync would look like.