Two apps, one source of truth: building the DRiV PushPress to GoHighLevel sync
A gym owner had two tools that refused to talk to each other. Instead of forcing a third bigger tool on him, we built a small bridge that runs unattended and hands him back about 40 hours a month. Here is how it works.
The problem nobody wants to admit they have
Todd Occhiuto runs DRiV Fitness, a CrossFit gym in St. Augustine, Florida. Like most gym owners, he ended up with two systems that each do one job well. PushPress handles membership: who signed up, who paid, who is on hold, who walked in the door this morning. GoHighLevel handles marketing: the pipelines, the texts, the email sequences, the follow-up that turns a free trial into a member.
The trouble is that neither tool knows the other exists. When someone bought a free trial in PushPress, nothing happened in GoHighLevel. When a member paused their membership, the CRM had no idea. So Todd (or someone on his team) sat in the middle, copying status changes from one screen to the other. That is not a software problem. That is a person doing data entry that a computer should be doing.
The obvious fix is to buy a bigger tool that claims to do everything. We did the opposite. We built a small bridge that does one thing and runs on its own.
Why a bridge beats a bigger tool
The pitch for an all-in-one platform always sounds good in the demo. The reality is a migration, a retraining, and a year of paying for features you do not use while losing the two things you actually liked. Todd did not need a new gym software. PushPress already works. GoHighLevel already works. What he needed was for them to behave like one system.
A bridge respects what is already there. It treats PushPress as the single source of truth for everything about membership, and it pushes that truth into GoHighLevel as tags, pipeline stages, and custom fields. Nothing in either tool changes. Todd keeps clicking the same buttons he always has. The sync just makes the second screen update itself.
Autosterea has been a great help managing our gym's digital needs. Todd O., DRiV Fitness
Webhooks, not polling
There are two ways to keep two systems in step. You can poll, which means asking PushPress every few minutes whether anything changed. Or you can subscribe to webhooks, which means PushPress tells you the moment something happens. Polling is lazy to build and expensive to run: you hammer an API thousands of times a day to catch the handful of moments that matter, and there is always a delay between a change and the catch.
We went with webhooks. The sync registers for six PushPress events and reacts to each one as it lands:
- checkin.created (someone walked into a class)
- customer.created and customer.details.changed (a new person, or an updated phone or email)
- customer.status.changed
- enrollment.created (a trial or membership was purchased)
- enrollment.status.changed (paused, pending cancel, canceled, or a trial marked complete)
When a free trial enrollment flips to completed, the CRM contact moves to the Free Trial Completed stage and swaps a tag, all within a second of it happening in PushPress. No clock, no polling loop, no wasted API calls. The system is asleep until there is real work to do.
Email is the key
The hardest part of any sync is deciding when two records are the same person. PushPress has its own customer IDs. GoHighLevel has its own contact IDs. They share nothing. If you get matching wrong you create duplicate contacts, and duplicate contacts are how a member gets two welcome emails and Todd loses trust in the whole thing.
We use email as the join key, with a three-step lookup that gets cheaper every time it runs. First, check a small local map of PushPress IDs to GoHighLevel IDs that the sync builds as it goes. If we have seen this person before, we are done in one read. Second, if they are new to us, search GoHighLevel by email, and if we find them, record the mapping and stamp the PushPress ID onto a custom field so we never have to search again. Third, only if no contact exists do we create one, then save the mapping.
Phone numbers get normalized to a consistent format on the way in, so a member entered as (904) 555 1234 in one tool and 9045551234 in the other does not slip through as two people. The result is that the same human stays one record across both systems, which is the entire point.
Built to run unattended
This sync has no staff. It lives on a small Ubuntu box behind Caddy, runs as a systemd service that restarts itself if it ever falls over, and starts on boot. The whole thing is Node, Express, and TypeScript with a flat JSON file for its mapping and event log. No database server to babysit. For a gym's volume that is not a shortcut, it is the right size.
A few decisions matter when nobody is watching. The webhook endpoint answers PushPress with a 200 immediately, then does the real work afterward, so a slow GoHighLevel call can never make PushPress think the delivery failed and retry it. Opportunity creation is defensive: we look for an existing one before creating, and if GoHighLevel still throws a duplicate error, we catch it, find the real record, and update that instead of crashing. Lifecycle tags are mutually exclusive, so when membership-active is added, free-trial-completed is removed in the same step, and the CRM never shows a contradiction.
One honest note from the build. PushPress signs its webhooks, but it uses a header called webhook-signature, not the x-pushpress-signature we first assumed. We logged the raw headers, found the real name, and now verify against it. Small thing, but it is exactly the kind of undocumented detail that decides whether a sync is trustworthy or just usually right.
What Todd got back
The number that matters is about 40 hours a month. That is the time that used to go into copying statuses between two screens, now handed back to a gym owner who would rather be coaching. The free trial follow-up fires on its own. A membership hold flows from the website form, to GoHighLevel, to Todd, into PushPress, and back into the CRM with a confirmation, without anyone re-typing it. Check-in dates stay current so the marketing side always knows who has gone quiet.
None of this required a bigger tool. It required one small piece of software that knows two APIs, treats email as the truth, listens instead of asks, and runs without a person standing over it. We built it, and now it just runs. That is the job.
Wondering what one system would replace for you?
