Roviniete.ro sells road vignettes and RCA car insurance to thousands of Romanian drivers every day. The business was healthy; the checkout underneath it was not. It ran on an aging payment integration with effectively one way to pay, payment confirmations that sometimes failed silently, and no way to renew an expiring vignette without buying it again from scratch.
The job was the whole payment experience: a new gateway, pay-by-card, direct bank-to-bank (open banking), reliable confirmations, automatic renewals, a wider catalog, and a hardened back office — on a shop that could never go dark. "Don't break the thing that's already taking money" was the constraint behind every one of those. Here's how the pieces went in.
Run the new gateway beside the old one
The tempting plan is a cutover: build the new PayU v4 integration, flip a switch, delete the old one. The tempting plan is also the one that turns a config typo into a revenue outage.
So we didn't cut over. We stood the v4 services up alongside the existing integration, behind the same checkout the customer already knew. The two card methods literally coexist in the system and are marked mutually exclusive, so an order goes through exactly one of them — new path or legacy path — never a muddle of both. That let capabilities land one at a time: card first, then open banking, then bank wire and renewals. If anything misbehaved, the blast radius was one payment method, not the shop.
A migration nobody can see is a migration you can roll back. Make the new path live next to the old one before you make it the only one.
Give people more than one way to pay
The old checkout's real tax wasn't the technology, it was the dead end: a customer without that one supported option simply left. So the point of the new gateway was breadth. On top of v4 we added pay-by-card, direct bank-to-bank open banking with the major Romanian banks, and a bank-wire option — three familiar ways to pay where there had effectively been one.
More methods is more surface, though — and the first sharp problem came straight out of one of them.
The signature has to match bytes you didn't write
The first sharp bug after going live lied about its own cause. Payment confirmations — PayU's server-to-server IPN callbacks — were being rejected as having an invalid signature, but only on RCA orders. Card vignettes confirmed fine. Every instinct says "the RCA merchant is misconfigured."
It wasn't the merchant. PayU signs the raw bytes of the request body, and a
naive verifier hashes the body it re-encoded from the parsed JSON. Those two
strings are identical right up until the payload carries a non-ASCII byte — a
Romanian name with a diacritic, an address with ș or ț. Re-encoding
normalises it differently, the hash diverges, the check fails. RCA orders just
happened to carry that data more often, so a content-shaped bug wore a
merchant-shaped costume.
$bodyHash = md5($rawRequestBody); // what PayU actually signed
$bodyHash = md5(json_encode($parsedBody)); // a different string the moment a "ș" appearsIf you re-serialize a payload before checking its signature, you're verifying your encoder, not the sender. Hash the bytes that arrived.
Confirmations that finish, renewals that don't ask twice
Two of the most valuable wins were the least glamorous. Confirmations now complete end-to-end through the v4 IPN handler instead of failing silently, so paid orders stop getting stuck and nobody on the team spends their afternoon reconciling payments by hand. And vignettes renew automatically — drivers stay covered without returning to rebuy from scratch, which quietly turns a one-off purchase into recurring revenue. Alongside that, the shop's catalog grew so it can sell more than it could before, all on the same hardened payment rails.
Lock the back office too
A payment platform is only as safe as the accounts behind it. The admin side now sits behind two-factor authentication: a one-time code mailed to the operator at login, with a resend path for when mail is slow. It's a small surface compared to the checkout, but it's the surface that can move money and data, so it got the same seriousness as everything customer-facing.
What it actually shipped
End to end: checkout migrated onto PayU v4 beside the old gateway with zero downtime; card, bank-to-bank, and wire payments where there was once effectively one option; IPN confirmations that complete reliably instead of failing silently; automatic vignette renewals; a broader catalog; and two-factor on the back office.
The lesson that paid off across all of it: ship onto the live system one isolated piece at a time, and verify against the bytes that actually arrive. Migrating without an outage and chasing a signature bug that pointed at the wrong suspect both got simpler the moment nothing went live big and nothing trusted a re-encoded copy of what the sender signed.