How we integrated Netopia into the NovaFit front desk: one-off and recurring payments behind a secure proxy
A technical case study on bringing online payments and auto-renewing memberships into a Next.js front-desk app — without card data ever touching our servers. The proxy architecture, tokenization for recurring charges, and the hard part: idempotent callbacks.

A gym’s front desk runs on a very concrete rhythm: a client walks in, picks a membership, pays, and heads to the floor. When we built the reception app for NovaFit — an internal Next.js app the staff use at the desk every day — one question was unavoidable: how do we add online card payments without turning a front-desk app into a small payment processor?
The answer was a Netopia integration designed in two layers: one-off payments for memberships paid once, and recurring payments for memberships that renew on their own. And one principle drove every technical decision: card data must never touch our servers.
One-off vs. recurring — and why it matters for a gym
Not every membership behaves the same at payment time, and the difference isn’t cosmetic.
| One-off | Recurring | |
|---|---|---|
| When it’s used | Memberships paid once, “+1” offers, trial periods | Monthly, semi-annual or annual memberships that renew |
| What the system keeps | Nothing about the card | A token from Netopia, bound to the card |
| At renewal | The client pays again manually | The system charges the token automatically |
| Consent | Implicit through the act of paying | Explicit, confirmed by the client at reception |
For a gym, recurring matters because it removes the monthly friction — the client no longer has to show up specially or re-enter a card. But precisely because it’s an automatic charge, we tied it to a firm rule: nothing happens to a member’s money without their knowledge and consent. The recurring toggle on the sales screen only turns on through an explicit consent step, and for longer memberships (semi-annual, annual) that consent is mandatory, not a reflexive checkbox.
If you want the broader picture of how we approach payment work, here’s our take on online payment integrations.
The “proxy through the backend” architecture: why reception never talks to the bank directly
The most important architectural decision was to not let the reception app collect or transmit card data. Instead, the transaction flows through our own backend acting as a proxy to Netopia:
- Reception asks the backend for a payment order (who, which membership, what amount).
- The backend builds and seals the order, then produces the redirect.
- The client enters the card on Netopia’s hosted page, not on a form of ours.
- Netopia confirms through a signed callback, and reception creates the membership.
Why does this matter? Because sensitive card data stays inside Netopia’s perimeter. Our server never sees it, never stores it, never carries it. In compliance terms, that dramatically reduces PCI scope — you can’t leak what you don’t hold. That’s not a security slogan; it’s a direct consequence of the design: less surface, less to defend.
What “sealing” looks like in practice
Netopia (the mobilpay protocol) works like a sealed envelope. The order is an XML that we seal with a randomly generated symmetric key; that key is in turn encrypted with Netopia’s public certificate. In practice, only Netopia can open the envelope. On the way back, the callback (IPN) is sealed by Netopia and we open it with the merchant’s private key — which also gives us the assurance that the message genuinely comes from them, not from a third party hammering our endpoint.
One detail that cost us real time is worth mentioning, because documentation doesn’t warn you about it: on recent Node versions, a security hardening measure (related to CVE-2023-46809, the Bleichenbacher attack) blocks exactly the decryption mode the protocol requires. We had to implement the envelope “unwrapping” step ourselves, carefully handling the padding format. It’s a good illustration of what a real integration means: you don’t glue an SDK on, you understand the cryptography underneath well enough to survive the cases where the real world doesn’t behave like the examples.
The concrete flow, from button to membership
Step by step, an online sale looks like this:
- Initiate — the receptionist picks the member and the membership type. If it’s an eligible recurring membership, they toggle recurring payment, with the client’s consent.
- Redirect — the backend seals the order and sends the client to Netopia’s payment page.
- Payment — the client enters the card on Netopia’s page; that’s where it’s collected, validated and authorized.
- IPN — Netopia sends the confirmation callback to the backend, which marks the order paid.
- Confirmation — reception picks up the order and creates the membership in the panel, complete with bonus days, referral and the other business rules.
The subtle point is that steps 3–4 happen outside our reception app. The client might close the tab, lose their connection, come back five minutes later. That’s why confirmation doesn’t rely on the person “returning to our page”, but on the server-to-server callback.
The hard part: idempotency, statuses, reconciliation
This is where the quality of the integration was actually decided.
Idempotent callbacks. An IPN can arrive several times for the same order — that’s normal, not an error. If you handle each callback naively, you risk creating two memberships for a single payment. We bound membership creation to the order through a unique reference, so that the second IPN no longer produces a second membership. One payment, one membership, no matter how many times Netopia knocks.
Clear statuses. An order moves through states (initiated → paid → fulfilled). Confirming the payment and creating the membership are two separate things, precisely so we can safely retry the second step if the first succeeded but the second didn’t.
Reconciliation for “paid, but never seen”. The classic case: the client pays on Netopia but closes the tab before returning. The order is paid, but reception never got to pick it up. We added a scheduled job that looks for paid-but-unfulfilled orders and finalizes them on its own — creating the membership under a “system” actor, so the history makes clear it wasn’t a staff member at the desk. In the test environment this job is “grounded”: it never creates real memberships from a sandbox payment.
Recurring renewal is itself a scheduled job: it checks memberships nearing expiry, confirms recurring is active, and charges the token kept from Netopia — without ever re-collecting card data. This “it just happens, in the background” part connects naturally to how we think about automating repetitive tasks: predictable, boring work is done by the system, so people can focus on clients.
The outcome for the NovaFit team
In short: reception can sell memberships with online payment, and recurring memberships renew themselves — without anyone re-entering a card and without any sensitive data passing through the gym’s app. The desk staff get a short, few-step flow; the client pays on a bank page they recognize; and reconciliation covers the cases where the real world goes sideways.
It’s the kind of integration you don’t notice when it works — which is exactly the point. The full details on the app itself are in the NovaFit Recepție case study.