47 seconds: A villain origin story

Wednesday, 11:15 AM. I'm at the PureGym entrance doing the universal gym app dance. Phone out, one bar of signal that immediately gives up because apparently the building is wrapped in aluminum foil. Connect to PureGym WiFi. Wait for it to actually connect. Open the app.

"Warming up..."

The entire feed loads—suggested workouts I'll never do, my "recent" activities from 2024, a motivational quote that makes me question humanity and myself. Oh, and would I like to enable push notifications? No. How about this special offer for personal training? Also no.

Finally, I can tap "Gym access." Loading spinner. The QR code materializes eventually like it's 2000 and I'm downloading a JPEG on dial-up. Someone behind me clears their throat. Scan at the pod, the barrier grunts open.

Total time: 47 seconds.

PureGym app loading Apple Wallet pass

Before: 47 seconds of app archaeology        After: 3 seconds of magic

I do this 6 days a week. That's 282 seconds of my life every week just trying to enter a building. As someone who's spent more than a decade optimising iOS apps for a living, watching myself suffer through this UX disaster daily was starting to feel like cosmic punishment.

Meanwhile, Amazon Fresh lets people walk out with £200 of groceries without even touching their phone. But here I am, performing a full mobile obstacle course just to lift heavy things and put them back down.

The eight-year PIN mystery

Before we dive into the technical meat, let me share the security contradiction that made me question reality itself.

I've been using the same 8-digit PIN code at PureGym turnstiles for eight years. EIGHT. YEARS. Same code. Never changed. Never expired. It survived COVID, three prime ministers, and the entire rise and fall of NFTs. I could literally tattoo this PIN on my forehead and use it as biometric authentication at this point.

But the QR code in the app? That bad boy refreshes every 60 seconds like it's protecting nuclear launch codes.

Think about this for a second. The physical keypad -- exposed to British weather, coated in a mysterious film of protein shake and regret, probably being livestreamed to TikTok by someone's ring doorbell -- accepts my ancient PIN without question. But the digital QR code needs cryptographic rotation that would make the NSA jealous.

This is peak security theatre. It's like having a £10,000 smart lock on your front door while leaving the windows open.

How I learnt to stop worrying and love mitmproxy

My first approach was embarrassingly naive. "I'll just screenshot the QR code and add it to Apple Wallet as a static image!"

Reader, I actually did this. Created a pretty pass, added my screenshot, proudly walked to the gym on Wednesday with my "innovation." The scanner beeped angrily at me like I'd personally insulted its mother. The screenshot from Monday was already dead.

That's when I discovered PureGym's QR codes are dynamic -- while technically they expire after about a week, the app refreshes them every minute for some reason.

But this raised an important question: if security was so critical, why does my prehistoric PIN still work everywhere?

Following the breadcrumbs on GitHub

Time to see if I was alone in my frustration. Now, most people would start by Googling. But we're engineers. We go straight to the source of truth: GitHub.

Searching for "PureGym" on GitHub is like opening a time capsule of developer frustration. Beautiful.

But nobody had cracked Apple Wallet. The gauntlet was thrown.

These digital archaeologists had unearthed the authentication endpoint:

POST https://auth.puregym.com/connect/token

grant_type=password&username={EMAIL}&password={PIN}&scope=pgcapi offline_access

Authorization: Basic cm8uY2xpZW50Og==

Decode that Base64 and you get ro.client: -- yes, that's a colon at the end. No secret. Just vibes. It's like leaving your house key under the doormat, but the door is also unlocked.

The crown jewel? Your 8-digit gym door PIN is your API password and you most likely didn't set it yourself. The same PIN that hasn't changed since the iPhone 8 was cutting-edge technology. I'm starting to think PureGym's security model was designed by someone with a very specific sense of humour.

Proxying my way to enlightenment

Time to see what the app actually does.

One could notice that at this point the PureGym API is basically crowd-sourced documentation all over Github, but I wanted to see the actual flow myself, so I needed to intercept its traffic, which meant proxy tools. The usual suspects: Charles Proxy, mitmproxy, Proxyman -- pick your poison.

Setting up a proxy in 2025 should be trivial, but mobile apps always had trust issues. Some certificate dancing later (shoutout to SSL Kill Switch and objection for making this bearable), I had traffic flowing:

GET https://capi.puregym.com/api/v2/member/qrcode # Note how auth is not versioned, this is v2, but all other apis are mostly v1s

{
  "QrCode":"exerp:checkin:<part1>-<part2>-<part3>",
  "RefreshAt":"2025-08-14T12:08:27.4349618Z",
  "ExpiresAt":"2025-08-21T12:02:27.4349618Z",
  "RefreshIn":"0:01:00",
  "ExpiresIn":"167:55:00"
}

# part1 is some static id, part2 is the now() timestamp, and part3 seems to change every time so probably is a salt

Look at that response. It literally tells me when to refresh despite a different expiration time. It's like the API is saying "Hey buddy, I know this is odd, but can you poll me every minute? Thanks, love you too."

PassKit: Apple's forgotten child

Here's something many iOS developers don't know: Apple Wallet passes aren't just static cards. They're basically tiny apps that can update themselves, send push notifications, and even react to your location. The framework is called PassKit, it's been around since iOS 6, and approximately 7 people have ever used it properly (definitely not me).

Building a pass requires:

  1. A JSON manifest (easy)
  2. Images in very specific dimensions (annoying)
  3. Cryptographic signatures (hello darkness my old friend)
  4. A web service for updates (time to write some Swift)

The certificate signing process deserves its own horror story but I will save it for my memoirs. As we all know, Apple hates us, and this particular framework is a good example of that. You need:

  • A Pass Type ID certificate from Apple Developer Portal
  • Apple's WWDR intermediate certificate
  • OpenSSL commands that look like you're summoning a demon

After three hours of certificate wrestling, I had a working .pkpass file. It's just a ZIP archive with delusions of grandeur, but when it works, it feels like magic (and when it doesn't, you can't easily tell because Xcode Simulator seems to treat Wallet passes differently from the actual devices).

The Swift backend nobody asked for

Most PassKit tutorials use Node.js. But I'm a Swift developer and believe in suffering, so I built the backend in Vapor.

Why? Because I could. And for type safety. But mostly because I could.

// The endpoint that makes the magic happen
app.post("v1", "devices", ":deviceId", "registrations", ":passType", ":serial") { req async in
// Device wants updates! Store that push token like it's bitcoin in 2010
let pushToken = req.body["pushToken"]
// ... store it somewhere
return Response(status: .created)
}

// The "oh you want updates?" endpoint
app.get("v1", "passes", ":passType", ":serial") { req async -> Response in
// Check if anything changed
if nothingChanged {
return Response(status: .notModified) // 304 baby!
}

// Generate fresh pass with new QR
let freshPass = try await generateNewPass()
return Response(body: freshPass, headers: ["Content-Type": "application/vnd.apple.pkpass"])
}

The best part of working with Apple Walet passes is this: when the QR code needs updating, I send a silent push notification. The device wakes up, fetches the new pass, updates the QR code, and the user never knows. It's like having a butler who refreshes your gym pass while you sleep.

The Great Gym Heist

Apple Wallet passes can appear on your lock screen when you're near specific locations. So I figured, as I travel around the UK a lot, I might as well stop by random PureGyms for an occasional bench press, so I needed coordinates for every PureGym in the UK. There are some location lists on the official website, but no coordinates.

Then I found it. The motherlode:

GET https://capi.puregym.com/api/v1/gyms/ # Back to v1, but judging by lots of null fields it used to do more stuff

// Lots of gyms
[
{
"id": 318,
"name": "Yeovil Houndstone Retail Park",
"latitude": "50.945449",
"longitude": "-2.671796",
"email": {...},
"gymOpeningHours": {...},
// ... more fields including something called "reasonsToJoin"
}
]

I scraped all locations just in case, and add the nearest to the generated pass. Now my membership appears on the lock screen whenever I'm near PureGyms in the country.

The only downside? My local PureGym is in a shopping center. Now my pass appears every time I go to M&S. Buying milk has become a moral dilemma -- am I shopping or avoiding the gym that's literally behind the wall?

The Apple Watch support

Plot twist: Apple Wallet passes automatically sync to Apple Watch. No extra code needed. Double-tap the side button, scan with my wrist, I'm in.

Time: 3 seconds.

That's a 93% reduction in entry time. As someone who optimises code for a living, this number makes me inappropriately happy.

 

Numbers that matter (to me)

Let's talk metrics, because what's a technical blog post without unnecessary statistics:

  • Original app time: 47 seconds
  • Apple Wallet time: 3 seconds
  • Time saved per visit: 44 seconds
  • Visits per week: 6 (I skip Sundays, God rested, so do I)
  • Annual time saved: 3.8 hours
  • Number of times other gym members have asked "is there an app for that?": 23
  • Number of times I've had to explain this isn't official: 23
  • Number of times they've asked me to make them one: 23
  • Number of times I've had to explain copyright law: 23

Bonus round

Since I was already deep in PureGym's API, I figured why not add gym capacity to my Home Assistant setup? The attendance endpoint is beautiful:

GET https://capi.puregym.com/api/v1/gyms/234/attendance

{
"totalPeopleInGym": 29, // They all want my bench
"totalPeopleInClasses": 0 // No one wants to do pilates
"maximumCapacity": 0, // This particular gym is huuuge (it's not actually, I am yet to find one that actually reports this value)
}

I made a few cards for Home Assistant dashboard that hangs next to my door that definitely help me go to the gym more (or to justify not going because it's too busy):

I was also wondering if I culd prove, with data, that I'm avoiding the gym efficiently but actually I usually come there at their least busy times so I must be a natural.

The uncomfortable engineering truth

This whole project took a weekend. A weekend to solve a problem that PureGym has had years to address.

But here's what I learnt: sometimes the best features come from outside the organization. PureGym probably has a roadmap, sprint planning, and very good reasons for not implementing Apple Wallet. Maybe it's not a priority. Maybe they have data showing only 0.3% of users would use it. Maybe their KPIs are based on the number of online classes previewed in the app, and forcing users to see them every time the app loads secures someone's annual bonus.

Maybe someone in product genuinely believes the current app experience is fine (bless their heart).

But users don't care about your Jira tickets. They care about experiences. And sometimes, those of us on the outside, armed with mitmproxy and too much free time, can prototype those experiences faster than a planning meeting can be scheduled.

The ethical elephant

Am I technically violating terms of service? Probably. Could PureGym break this tomorrow? Absolutely. Should I be doing this? Gestures vaguely at the 47-second entry time.

I follow some ground rules:

  • Cache everything (in case one's API is held together with prayers)
  • No automation beyond personal use
  • Don't share the actual service (hence the 23 explanations)
  • Be ready for it to break any moment

Should I package this up properly? Probably not: it's a proof of concept that solves my specific problem. Plus, PureGym would probably just hire me to shut it down, and I'm not ready for that level of corporate responsibility.

What's next?

The possibilities are endless and mostly ridiculous:
Shame Notifications: "You were literally 100 meters from the gym and walked past it"
SaaS Dreams: Package this properly, get sued immediately, become a cautionary tale at product management conferences

For now, I'm enjoying my 3-second gym entry and the knowledge that I've optimised away 3.8 hours of annual friction. In the grand scheme of things, it's meaningless. But in the petty scheme of things? It's everything.

And if you're from PureGym reading this—let's talk. I've already done half the work for you. 😉

PureGym app loading Apple Wallet pass

Before: 47 seconds of app archaeology        After: 3 seconds of wrist magic

Working on something that needs iOS expertise, reverse engineering skills, or someone who obsesses over shaving 44 seconds off everyday interactions? I build exceptional mobile experiences and occasionally solve problems that shouldn't exist. Drop me a line at work@drobinin.com.


Tuesday Triage

Every week, I sift through thousands of articles, posts, tweets, videos, facts, and trivia to bring you the crème de la crème in technology, arts, cooking, mixology.

    Don't miss out – subscribe to monthly emails for free or upgrade to a weekly subscription and get access to the archive of all previous and future editions.