Case study · Melbourne gym / fitness studio

Fitness · memberships · Workflow rescue (Stripe → CRM)
When renewals paid in Stripe but the CRM said expired
- stripe
- crm
- webhooks
- melbourne
- gyms
A Melbourne gym had successful charges in Stripe while the desk still saw lapsed memberships. How we traced the break, fixed webhooks and duplicates, and made subscription status boring again.
Context
The business runs class packs and rolling memberships through Stripe, while staff live in a booking tool and a lightweight CRM for tasks and follow-ups. That is a normal stack. The pain starts when the member list in the CRM stops matching who Stripe thinks is active.
Front desk staff were opening Stripe in one tab and the CRM in another before they would trust a renewal. Finance still trusted Stripe for cash, but ops could not run the floor from one screen. Names and organisations in this write-up are anonymised; the mechanics are common across Melbourne studios.
What broke
Three symptoms showed up together: successful renewals in Stripe with no matching CRM update, two CRM records for the same member email, and failed renewals that only surfaced when a member complained at the desk.
- Webhook endpoint returning 4xx. A signing secret had been rotated during an unrelated deploy. Stripe kept retrying; the CRM never saw the subscription events.
- Duplicate identities. Trial forms created a CRM person while Checkout created a Stripe Customer with a slightly different email variant. Automations attached to the wrong record or skipped entirely.
- CRM fields edited by hand. Staff toggled “active” in the CRM when someone paused in real life. Stripe became the financial truth while the CRM pretended to be the membership truth. Both could not win.
Approach
We started with a single payment timeline for a known renewal: timestamp in Stripe, delivery status on the webhook, and activity on the CRM contact within the same five-minute window. That removed the guesswork about whether the CRM was “randomly wrong.”
Engineering work was deliberately small: restore a healthy webhook URL and signing secret, make the handler idempotent so retries do not duplicate rows, and map subscription lifecycle events into read-only CRM fields that automation owns. Humans still own notes and tasks; machines own status copied from Stripe.
For duplicates we picked a merge rule (one canonical email per member), backfilled Stripe Customer IDs onto the surviving CRM record, and stopped creating second contacts from checkout unless no match exists.
What improved
- Desk staff stopped cross-checking Stripe for every check-in.
- Fewer “I paid but your system says expired” conversations at peak hour.
- Finance and ops could agree on who was active before weekly reporting.
- The weekly CSV ritual between Stripe exports and spreadsheets shrank to exception handling only.
Lessons you can reuse
Pick one authority for money and subscription state. Stripe should win for “is this card paying us on schedule?” Replicate that into the CRM as derived fields, not the other way around.
Treat silent webhook failure like an outage. Retries, logs, and a simple alert when no CRM write happens within minutes of a successful charge save weeks of goodwill.
If this pattern sounds familiar, the triage steps in When Stripe and your CRM disagree are the same checklist we used on day one.