Integration
Bridging GoHighLevel to a back office that closes leads after 72 hours.
A multi-site housing operator ran sales in GoHighLevel and reporting in a legacy back office whose API matched leads by email only and auto-closed them after 72 hours of silence. The bridge that worked stamped the back-office ID onto every CRM contact, looked leads up on demand instead of migrating 14,000 of them, and treated a 201 response as a claim to verify, not a result.
Contents
A 201 response from an API means the server accepted your request. It does not mean the lead exists, the note was saved, or anyone will ever see it. I learned that from a back-office system that said 201 to every action we posted against a closed lead and quietly dropped all of them.
This is the story of bridging that system to GoHighLevel for a housing operator with two dozen locations, what broke, and the rules the build left behind.
The setup
Sales and follow-up ran in GoHighLevel, one sub-account per location. Leasing records, next-action logs and the reports the owners actually read lived in a legacy back office with an API that consisted of very little: an endpoint to log a "next action" against a lead, a way to list open leads, and a lead-creation call. Authentication was HTTP Basic. Four action types were allowed. Facebook, one of their busiest inbound channels, was not one of them.
Nobody wanted to replace either system. The office knew the back office, the sales team lived in the CRM, and the reporting the leadership team trusted only counted what appeared in the back office's next-action log. So the job was a bridge: every conversation in the CRM had to show up as an action against the right lead in the back office, without anyone changing how they worked.
Three constraints that shaped everything
Leads auto-close after 72 hours of inactivity. Closed leads vanish from the open-leads call. A sync that only reads open leads silently misses every re-inquiry, and the natural reaction, "no open lead found, create one", manufactures duplicates.
Writes to closed leads succeed and disappear. Post an action to a closed lead and the API returns 201. The action never reaches the next-action log or the UI. Success status is a claim, not a result.
Matching is by email only. The endpoint ignores the lead ID you send and matches on the email address. Some leads have no email. Some have a case-variant of the same address on two records. Both cases misattribute activity or fail outright with "lead does not exist".
None of these are bugs you can file. They are how the platform works, and the build had to be correct on top of them.
What we built
Stamp first, then key on the ID. GoHighLevel got a custom field, back_office_lead_id, across all 25 locations. A stamping workflow finds each contact's back-office lead once, by email or phone, and writes the ID onto the CRM contact. After that, every workflow keys on the ID rather than the email, which removes the case-variant and no-email failures for anything already stamped.
Look up on demand, do not migrate. The back office held more than 14,000 leads. The obvious plan, pull them all and load them into the CRM, was rejected. Instead, about 11,000 of them were mirrored into a Supabase table used only for deduplication, and unmatched contacts are looked up one at a time when they first need syncing. Nothing historical is rewritten. A bad match affects one record, not the whole book.
Keep an event ledger, not a snapshot. The first version logged sync results into a table that upserted on (lead, community). That is a snapshot: it overwrites, so it cannot tell you how many actions happened this week. A separate post_events table records every write as an event. Reports read the ledger. The snapshot exists only to answer "what is the current state of this lead".
Verify every write. Because a 201 proves nothing here, the workflow checks that an action actually appears where the business will look for it before it marks the sync successful. When it does not, the row goes back into the queue instead of into a green tick.
Forward-only rollout. Every new version of the sync processes activity from its start time onward. It never re-touches historical leads. A rule like that only exists because of the incident that taught it, and this one has one.
What broke first
An early rollout ran logic that could touch historical records and re-opened a large number of closed leads at once. That is the origin of the forward-only rule.
A stray test workflow, left active in a second n8n project, posted duplicate notes against live leads for a while before anyone noticed. It was deactivated, and dedup on the message ID became mandatory for any conversation sync.
A weekly report workflow was pointed at the snapshot table instead of the event ledger, so its headline counts did not match real activity. The fix was a one-node change, but the lesson is the point: a report that reads the wrong table looks exactly like a report that reads the right one.
The first node of the sync had no retry configured. One transient API error killed the whole cycle for that run. Retry on the first external call is now a default, not an option.
What we asked the vendor for
Three endpoint changes went to the back-office developers through the client: retrieve a lead by ID, email, or phone regardless of open or closed status; a lead-status call returning open or closed; and action logging that accepts all three identifiers and counts correctly against closed leads. The governing principle was that every lead endpoint must accept each identifier independently, because some leads have no email and some have no phone. Until those land, the stamp-and-lookup design is what makes the bridge work.
Limitations, plainly
Outbound Facebook replies cannot be logged as a standard action because the platform has no action type for that channel. The conversation sync has a rolling 24-hour lookback, so a message that falls outside its window is not recoverable later. A lead created through the API returns an ID but does not appear in the open-leads list, which is still open with the vendor. None of these are hidden from the client; they are written down next to the workflows.
My take
Most integration failures on closed or legacy systems are not caused by the missing API. They are caused by treating a thin API as if it were a full one: trusting success codes, keying on fields that are not stable, and migrating data that should have been looked up. The bridge that survives is the one that assumes the platform is telling the truth about its constraints and lying about its successes.
If your office runs on a system nobody will integrate with, that is usually the sign it is worth bridging, not replacing.
Questions people ask
What do you do when a CRM or back-office system has no proper API?
Find the thinnest reliable write path it does have, even if it is one endpoint, and build around its constraints instead of pretending they are bugs. Then put a staging table between the two systems so every write is deduplicated, logged as an event, and replayable. Migrating everything in bulk is usually the wrong first move; stamping IDs and looking records up on demand is safer and reversible.
Why did you stop trusting a 201 response?
Because this back office returned 201 Created for actions posted to a closed lead and then never showed them in its reports. A success status only proves the request was accepted, not that the data landed where the business will look for it. Every sync now verifies the write by reading it back or by checking the report it was supposed to appear in.
Should I migrate all my old leads into the new CRM before connecting the two?
Only if you can prove every record will match cleanly, which you usually cannot. In this build, bulk-pulling 14,000 leads was rejected in favour of on-demand lookups: when a contact in the CRM has no back-office ID stamped on it, the workflow looks it up by email or phone once, stamps the ID, and from then on uses the ID. Old data stays where it is; new activity flows correctly.
Have a process like this leaking time or leads?Thirty minutes, no deck, and you leave with a plan.
Book a call