How to Keep Mindbody Data in Sync With Your Own Database
Pulling data out of Mindbody is easy. Call GET /client/clients, page through the results, write them into a table, and by the end of the afternoon you have a copy of a studio's client list sitting in your own Postgres.
Keeping that copy correct for the next two years is the actual project.
Someone cancels a booking from the front-desk iPad while your nightly job is halfway through page 40. A receptionist merges two duplicate client records. A recurring class shifts by an hour when daylight saving ends. None of these throw an error. The sync job keeps logging success while the two databases quietly drift apart, and you find out six weeks later when a member asks why the app is still showing a class that was cancelled.
That gap — between "the integration works" and "the integration is still right in month eighteen" — is what this article is about.
Why keep a copy at all
The tempting shortcut is to skip the database and proxy every read straight through to Mindbody. It falls apart for three reasons.
Rate limits are per site. A single screen like "members who haven't visited in 60 days" turns into hundreds of calls against a quota shared with every other integration the business runs.
There are no cross-site queries. Each location is its own SiteId, passed as a request header. Any franchise-level reporting has to be assembled on your side regardless.
Latency compounds. One call is fine. Six sequential calls to render a dashboard is not, and Mindbody pages are capped, so "one call" is rarely one call.
Once you accept that you need a local copy, the only real question is how to keep it honest.
The architecture
We run two paths into the same tables: a fast one driven by webhooks, and a slow one that assumes the fast one is lying.
| Component | Responsibility |
|---|---|
| Webhook receiver | Verify the signature, persist the raw envelope, return 200. Nothing else. |
| Queue | Decouple delivery from processing so a slow write never costs us a subscription. |
| Projector | Apply events idempotently into normalized tables. |
| Reconciler | Sweep the Public API on a schedule and repair anything the events missed. |
| Raw event log | Append-only record of every envelope received, for replay and forensics. |

The fast path keeps the copy current. The slow path keeps it correct.
The important property is that the reconciler, not the webhook stream, is the source of truth. Webhooks only make the copy feel instant.
Four things that will break your sync
1. Events arrive twice, and out of order
Mindbody documents this plainly: events are not guaranteed to be delivered only once, and not guaranteed to arrive in chronological order. An updated event can land before the created event it follows.
Two guards handle it. Put a unique index on message_id and drop anything you have already seen. Then store event_instance_origination_date_time on every row as source_updated_at, and make the upsert conditional:
insert into mb_client (site_id, mb_client_id, payload, source_updated_at)
values ($1, $2, $3, $4)
on conflict (site_id, mb_client_id) do update
set payload = excluded.payload,
source_updated_at = excluded.source_updated_at
where excluded.source_updated_at > mb_client.source_updated_at;
A late event now becomes a no-op instead of a regression.
The delivery contract matters just as much. If Mindbody doesn't get a 2xx within ten seconds it retries every fifteen minutes for three hours, then deactivates the subscription. A receiver that does real work inline will eventually take your integration offline during an unrelated database incident. Verify the X-Mindbody-Signature HMAC, write the envelope, acknowledge, and process later.
2. Webhooks are not a complete picture
The event catalogue covers sites, locations, clients, class schedules, roster bookings, appointments, staff and sales. That is most of what moves in a studio — but not all of it, and a subscription that silently deactivates loses events with no backfill mechanism.
So the reconciler runs on three cadences: an hourly delta against the endpoints that accept a LastModifiedDate filter, a nightly full sweep of the small reference sets (locations, staff, class descriptions, pricing options), and a weekly full sweep of clients and visits. Hash each normalized row, compare, and only write on mismatch — the log of what it repaired is the honest health metric for the whole integration.
3. IDs are only unique inside a site
Client 12345 at one location has nothing to do with client 12345 at another. Every primary key is composite: (site_id, mindbody_id). Getting this wrong is cheap to fix on day one and extremely expensive to fix after a franchise onboards its second location.
Clients also get merged. When client.merged fires, the losing ID disappears from the API but still exists in your foreign keys, your analytics and your customer's saved links. Keep an alias table mapping retired IDs to survivors and resolve through it on read.
4. Timestamps are the quiet killer
Class and appointment times come back in the site's local time. Store the location's IANA timezone once, convert on write, and keep the original string alongside the UTC value so a mis-parse is debuggable rather than invisible.
The same caution applies to the delta watermark. Never advance it to now() — advance it to the maximum LastModifiedDate you actually observed, then subtract a fifteen-minute overlap on the next run. Overlapping costs you a few duplicate upserts, which the conditional write already absorbs. Not overlapping costs you rows you will never notice are missing.

Acknowledge first, decide later. The staleness check is what makes out-of-order delivery safe.
The honest trade-offs
This design buys correctness with latency and quota, and it is worth being explicit about the bill.
Your copy is eventually consistent. Normally that means a few seconds; when a subscription drops it means up to one reconcile interval. Anything where being wrong costs money — remaining class capacity at the moment of booking, current account balance, contract status at checkout — should still read live from Mindbody. Use the local copy for lists, search, reporting and segmentation.
The reconciler consumes API quota that could have served customer traffic, so its cadence needs to be a deliberate number, not a default. And the raw event log grows quickly. We keep ninety days hot and archive the rest, because the first time you need to replay three weeks of bookings to fix a projector bug, that log is the difference between an afternoon and a support crisis.
Where this architecture belongs
The same two-path pattern applies well beyond Mindbody — it is the right shape for any third-party system that exposes at-least-once webhooks plus a rate-limited REST API:
- Multi-location franchises that need reporting the vendor's dashboard can't produce
- Custom member apps that want their own search, recommendations and push logic
- Marketing and CRM automation driven by visit history and lapsed-member segments
- Data warehouses where Mindbody is one of several operational sources
The second and third bullets are why we ended up building this on BXR, a London boxing studio whose member app runs on Mindbody for scheduling and memberships. Two requirements pushed work onto our side of the boundary. Apple Pay and Google Pay had to run through a custom Stripe layer, which meant mapping services and memberships across both platforms and reconciling transactions as each one changed. And Klaviyo needed behavioural events, bookings, cancellations, check-ins and in-app engagement, that Mindbody does not expose in usable form. Both depend on a local copy that stays accurate for years, not just on launch day.
Working on a Mindbody integration, or inheriting one that has quietly drifted? Talk to our team — we have built this pattern in production and are happy to review yours.


