
healthretreat.ca
Keap to GoHighLevel: 19 years of data moved in four weeks, and the business never stopped
Fresh Start Health Retreat Centers, a Canadian residential health retreat, had run its CRM on Keap since 2012, with guest history back to 2007. They hired us on Upwork to move everything to GoHighLevel: 8,000+ contacts deduplicated and cleaned, 19 years of history carried over record by record, and the daily operational layer rebuilt. Delivered in four weeks around a business taking registrations and payments every day. Then we built the automations, fixed email deliverability, and stayed on as their GoHighLevel engineer.
19 years of data, and still trading
Keap held 8,000+ contacts, a large share of them duplicates, test records and dead entries, plus 19 years of invoices, tasks, notes and appointments. The job was to clean the list, move everything that mattered without losing a record, and rebuild the daily operational layer in GoHighLevel so Keap could be switched off.
We were hired on Upwork in July 2026 with a fixed deadline: GoHighLevel as the single source of truth by the end of the month, with a live business running registrations and payments through the CRM the whole time. Keap Classic since 2012. GoHighLevel adopted in February 2026 for marketing, full move decided in May. Keap decommissioned mid-August 2026.
Keap Classic, 2012 to 2026
Contacts, companies, tags, invoices, tasks, appointments and person notes. Exports plus the Keap REST and XML-RPC APIs for what the exports could not give.
Python migration scripts, reconciled
Per-contact extraction, a validated field map, idempotent writes through the GoHighLevel API, and an action log for every call so every record can be traced.
GoHighLevel as the single source of truth
Contacts, history, workflows, templates, calendars, invoicing and email. The client's in-house portal now reads guest and program data from GoHighLevel.
One migration. Nine workstreams.
Every invoice, task, appointment and note
For each contact: Keap tasks became real GoHighLevel tasks; invoices, appointments and notes became dated history notes in the client's own layout. Matched by email, idempotent on re-runs.
Field map verified against the live account
Every Keap field mapped to its GoHighLevel field and re-verified against the live account, with a dedicated folder for Keap-only data so nothing historical got mixed into live fields.
12 workflows, 90 trigger tags
When a consultant registers a guest, one tag sets 14 fields (dates, cycle, package, deposit, program), adds the guest tag, writes a note, and marks the opportunity won.
4 webform workflows, 15 forms
Capitalises names, fixes Canadian postal codes, assigns the consultant, creates the opportunity with the right program value, guards against duplicates, and moves the card when a call is booked.
71 templates, 48 snippets, 15 folders
Marketing and sales templates rebuilt in the GoHighLevel editor to the client's exact fonts, links and booking widget. 70 legacy templates archived, campaigns left inactive until approved.
Two funnels rebuilt in GoHighLevel
Two program landing pages the team could not edit, rebuilt in the GoHighLevel funnel builder with the consultation calendar embedded.
DKIM and DMARC fixed for two senders
Broken DNS records repaired for the GoHighLevel sending domains and Microsoft 365. Verified at Gmail and Yahoo, DKIM 100% at Google Postmaster within 48 hours.
Google Ads attribution restored
Diagnosed why ad traffic was not reporting in GoHighLevel and wrote the fix (account-level UTM template), applied by the client.
Documented, then retained
Run reports, field maps, DNS records and workflow specs on file. The client closed the Upwork project with a 5.0 review and moved to a direct ongoing engagement.
Extract, archive, clean, map, load, reconcile
8,000+ Keap contacts went in; duplicates, test records and dead entries came out before anything was loaded. Data was still changing in Keap while the migration ran, so the method had to be safe against a moving target: pull everything first, keep the raw archive, and prove every import against source counts.
Draft first, sign-off gates
Every workflow, template and campaign was built in draft and switched on only after the owner approved it.
Disclose deviations the day they are known
Every place the build differed from the spec went to the client in writing, with the reason and the cost of the alternative.
The client's communication system, not our own
Task tracker for instructions, chat for quick questions, three days' notice before big workstreams, end-of-day updates.
One run. Every contact accounted for.
The historical records run went through 6,561 contacts in a single pass: fetch from Keap, match the GoHighLevel contact by email, write tasks and history notes, log every call. The 49 errors were contacts the owner had already deleted from GoHighLevel, which is the correct outcome.
- 0 rate-limit responses across 45,750 API calls
- 25 transient Keap errors, all retried and recovered
- Keap quota never exhausted (29,896 down to 11,539)
$ python keap_to_ghl_notes_migration.py --run final [18:37:02Z] loaded 6,561 contacts (155 columns) [18:37:04Z] keap quota: 29,896 remaining ... [summary] contacts processed ........ 6,561 [summary] integration status ........ 6,512 success / 49 no-match [summary] history notes created ..... 685 contacts [summary] appointments written ...... 2,031 contacts [summary] invoice histories ......... 927 contacts [summary] tasks created ............. 3,172 (1,749 contacts) [summary] http 200 / 201 ............ 34,352 / 6,816 [summary] http 429 (rate limit) ..... 0 [summary] http 503 (transient) ...... 25, all recovered [summary] action log rows ........... 45,750 done. report written to Historical-Records-Run-Report.md

852 tasks unassigned
Their Keap owners were not GoHighLevel users. Tasks exist and are linked to the right contacts; only the owner is blank. Logged with the evidence, left for the client to decide.
1,178 duplicate notes removed
A mis-mapped import had doubled person notes on 590 contacts. A content-matching script deleted only exact matches: 0 errors, dry-run first.
366 linked contacts carried over
Family and partner relationships pulled from Keap through both REST and XML-RPC and delivered as a sheet for association in GoHighLevel.
One tag registers a guest
Programs run as dated cohorts in three blocks a year, across two program families, three lengths each, and up to three packages. Instead of consultants filling 14 fields by hand, each session, cycle and package combination is a trigger tag. Twelve published workflows cover the 90 combinations with the same six steps: update the fields, add the guest tag, write the note, remove the trigger, find the opportunity, mark it won.


14 fields from one tag
Program dates, cycle, package, start and finish, deposit, program name, area of focus, person type. No typing, no mismatches.
Opportunity closed automatically
The workflow finds the latest opportunity and marks it Won, so the pipeline matches reality without a second step.
Verified with the owner on day one
Published and tested together on real registrations the day they shipped. Spec and QA audit on file.
Fifteen forms, one clean intake
Every website form across four brands lands in one workflow: capitalise the first name, upper-case the postal code, assign the consultant if nobody owns the contact, check for an existing open opportunity, then create the card with the right program value in the New Leads pipeline. A fourth workflow moves the card to Consultation Booked the moment a call is booked on either public calendar.
Live-tested the night it shipped: name casing, postal code, assignment, program values, duplicate guard, booking move. All passed, test leads deleted.


Duplicate guard
An open card anywhere in the pipeline blocks a second one; a resubmission only updates the program field. Disclosed as the policy.
Deviations in writing
Opportunity name kept to First Last; postal code upper-cased without a forced space. Both would need paid custom code, both documented.
Internal calendar excluded
Only the two public booking calendars move cards. Internal bookings never touch the pipeline, on purpose.
187 Keap templates, sorted
The client's template sheet had 187 Keap emails across 15 sections. Each one was classified: archive, rebuild, or drop. The active set was rebuilt in the GoHighLevel editor with the client's fonts, custom values, brand links and booking widget. Transactional bodies became snippets so consultants can send them in two clicks.
- 71 templates live in 15 folders
- 48 snippets for sales and guest logistics
- 70 legacy templates archived, not deleted
- Seasonal campaigns built, left inactive until the owner approves


Keap history kept in its own lane
194 custom fields in 16 folders. The migration added a "Keap Historical Data" folder for fields that only exist because of the old system (person notes, created-by, old room number, email status) so the team never confuses legacy values with live ones.
- 172 contact, 10 opportunity, 12 company fields
- Program, travel, financial and pre-screening groups mapped 1:1 from Keap
- Registration fields reused by the 12 workflows, no new field design

27 smart lists, one per job
Guests room list, master data, arrivals report, allergies, birthdays, arrival and departure: the lists the operations team runs the week from.
Two pipelines, 14 stages
A five-stage New Leads pipeline from form submission to consultation booked and won, and a nine-stage planning pipeline for the next program year.
Portal reads from GoHighLevel
The client's in-house guest portal pulls program fields over the GoHighLevel API, so one record feeds both systems.
Two senders. Both signed.
In October the client's email was landing in spam. The DKIM and DMARC records for the GoHighLevel sending domains had been entered incomplete, and Microsoft 365 was not signing the root domain at all. Fixed in two DNS sessions, one record at a time, with a full zone backup first.
| Record | Before | After |
|---|---|---|
| k1._domainkey.lc | k=rsa (key missing) | Full 225-char DKIM key |
| _dmarc.lc | v=DMARC1 (no policy) | p=none with reporting address |
| k1._domainkey.notifications | k=rsa (key missing) | Full DKIM key, domain verified |
| selector1/2._domainkey | absent | Microsoft 365 DKIM CNAMEs added |
| Gmail test | spam | SPF, DKIM, DMARC pass, inbox |

A live business is not a sandbox
The owner set the rules. Our job was to build inside them and never make him wonder what had changed.
Unsubscribe links by hand
The platform default stays off. Every marketing email carries the owner's exact footer and is approved before it goes anywhere.
Phones stay where they are
The existing phone system was not replaced. The CRM handles email, tasks, pipeline and registrations; calls stay on the client's provider.
Pipeline only up to registration
After a guest registers, everything is tasks, not stages. The pipeline reflects sales, not operations.
Backup, one record, verify, repeat
Full zone export before any change, one record per save, byte-compare before confirm, public DNS check after. Root records never deleted without explicit OK.
Nothing deleted, nothing live by accident
Legacy templates archived, not deleted. Campaigns built inactive. Old webforms untouched. Client files never removed.
Test residue cleaned
Every test contact is prefixed and removed after the test. Test sends go from the sales mailbox. No credentials in docs, tasks or chat.
In Vasili’s words
“He goes above and beyond, advanced GHL Admin, API, Scripting, coding skills and he just kept on pulling more skills out to get the project done. Top skill: SMARTS and EASY to work with. He gives you 100% of his attention and once he is clear, does miracles, plus has positive radiance around him. He honestly told us, Keap is not my top skill but I know what needs to be done to help you move to GHL. Because he has advanced API/Coding skills move became possible. All lists, automations, historical records and more are in GHL before deadline. Highly recommend Razi as top expect on Upwork!”
Keap off. Client kept.
Keap was decommissioned in mid-August 2026 with every contact and every historical record accounted for. The client closed the Upwork project with a 5.0 review and a video testimonial, then moved to a direct ongoing engagement for GoHighLevel support and new builds.
Timeline: hired July 19. Historical run July 31. Migration complete and Keap off August 16. Landing pages rebuilt August to September. Webform intake live September 15. Email authentication fixed October 24 to 26. Ongoing engagement from October 27.
Python · Keap REST + XML-RPC · GoHighLevel API
Plus the GoHighLevel workflow, funnel and email builders, Microsoft 365, Mailgun through LeadConnector, Google Postmaster, and DNS at the client's host.
Everything is on file
Run report, 45,750-row action log, per-contact result sheet, DNS zone backups, authentication test results, workflow specs and a full handover document.
Still building for them
Lead-nurture email sequences, an affiliate and referral program, and a payments dashboard are the next scoped projects.
Three things we learned
The full case study as a PDF
The full write-up with the screenshots above, formatted for printing or forwarding. Shared with the client's permission. Private data stays hidden.
Download the PDFMoving off Keap, HubSpot, ActiveCampaign or a spreadsheet?
Tell us what you are trying to automate or move. We will say what fits, what does not, and what it would cost.
Book a free 15-minute call