payment

Revenue is flat but currency keeps appearing — the gaps in IAP receipt validation

"The revenue report and the currency logs disagree" — one Monday morning reconciliation

An operations lead reconciling the month notices something odd. The paid currency the game server granted far exceeds the purchases recorded in the store console. The gap is not a handful of transactions; it is hundreds.

Breaking it down by account reveals a pattern. One account "bought" the same item more than forty times in a day with no matching store record at all. Another was refunded moments after paying, and still holds the currency.

The reaction at this point is usually "has store payment been broken?" That is unlikely. Payment processing at Google and Apple is solid. What the attacker targets is not the payment, but the moment your game concludes the payment finished and releases the currency.

Payment cracks target the grant, not the payment

An in-app purchase proceeds roughly like this.

  1. The user pays through the store.
  2. The store returns proof of purchase — a receipt or token — to the client.
  3. The game checks that proof and grants the currency.

Steps 1 and 2 belong to the store and are hard to touch. Step 3 is the problem. Depending on who checks that proof and where, this step can be sturdy or effectively wide open.

If the game checks the proof only inside the client, the checking code sits within the attacker's reach too. That is precisely what payment bypass tools do. They do not deceive the store; they tell your game the payment is done.

Four doors tampering comes through

Four patterns show up repeatedly in the field.

1. Validating only on the client

The most common and the most damaging. Intercept the payment response and flip it to success, or pin the validation function's return value, and it passes straight through. The store has no record of any of it.

2. Receipt replay

Submitting one legitimately purchased receipt over and over. The server does validate, but if it never checks whether that receipt has already been used, the same proof can collect currency indefinitely. This is the classic reason reconciliation and grants drift apart.

3. Entitlements that survive a refund

Pay, receive the currency, request a refund. The money goes back at the store while the in-game currency stays put. Repeat it and you are minting currency for free.

4. Treating a pending state as success

A purchase can sit in a state that is not yet final. Treat that as success and the currency is already gone even if the payment never completes.

All four have something in common. Not one of them broke the payment. They exploited gaps in the game's own validation procedure.

The validation lifecycle — what has to finish before you grant

Thinking of "receipt validation" as a single check is how cases 2 and 3 slip through. Validation is not an event at purchase time; it is a procedure that continues after the purchase.

Stage What happens If you skip it
Hand off The client passes the proof it received to the server Only client validation remains, exposing case 1
Verify The server confirms the proof with the store API Forged proof passes
De-duplicate Compare against proofs already processed Exposed to replay (case 2)
Grant Add currency only in a confirmed state Granting on pending (case 4)
Acknowledge Tell the store the purchase was handled Automatic refund, lost revenue
Follow up Receive refunds and cancellations, revoke entitlements Exposed to refund abuse (case 3)

All six stages belong on the server. The client's role ends at handing over the proof.

Google Play: what to confirm server-side

Google explicitly recommends backend verification in its Play billing security guidance. The essentials the documentation lays out:

  • Send the purchaseToken the client received to your server and record every token value.
  • The token is globally unique and safe to use as a primary key. Check whether you have seen it before as the first step.
  • Confirm the actual purchase with the Google Play Developer API's Purchases.products:get and Purchases.subscriptionsv2:get.
  • Use obfuscatedAccountId or obfuscatedProfileId to run eligibility and abuse checks at account level.
  • Grant only when the purchase state is confirmed (PURCHASED). Never grant on a pending state.
  • Revoke invalid purchases explicitly by calling Orders:refund with revoke=true.

There is one operational trap that is easy to miss here. A purchase left unacknowledged for three days is refunded automatically. Revenue does leak from projects whose validation logic is perfectly sound but that skipped the acknowledgement call.

App Store: are you still on the old approach?

What to check on Apple's side is different, because the validation mechanism itself changed.

The older approach sent receipt data to Apple's verifyReceipt endpoint. That path is now legacy; the current standard is querying transaction information through the App Store Server API. This API returns transactions signed as JWS, so your server can verify the signature against Apple's public certificates.

If your project carried over an old blog post or a code sample from a few years back, it is worth checking which approach you are actually running. Confirm the exact procedure and current state in the official documentation — payment APIs change faster than this article does.

What to check before granting

Anything here that does not apply to you is a door standing open right now.

Validation procedure

  • Receipt and token validation runs on the server.
  • The store API confirms the purchase actually exists.
  • Processed tokens are stored and checked for duplicates before granting.
  • Currency is granted only in a confirmed state, never on pending.

After the purchase

  • The store is sent an acknowledgement after granting.
  • Refund and cancellation notifications revoke currency or entitlements.
  • Invalid purchases found later are revoked explicitly.

Account and operations

  • Purchases are recorded against accounts, so abnormal repeat purchases on one account are visible.
  • Revenue figures and granted amounts are reconciled on a regular schedule.
  • Whether the client that sent the request has been tampered with is part of the judgement.

What OZero Security covers, and what it does not

On this topic it makes sense to start with what we do not do.

What we do not cover

  • We do not validate receipts or purchase tokens for you. Calling the store API to confirm a purchase's authenticity and state is your server's job. We cannot take that place, and we do not claim to.
  • Duplicate prevention, refund revocation and reconciliation are likewise your server's and your operations team's work.

What we do cover

  • Judging whether the client making that request can be trusted. We identify executable binary tampering, debugger attachment and hooking, and untrusted runtime environments such as emulators and rooted devices, raising the cost of bypassing them.
  • Detecting MOD APK repackaging and re-signing, and responding according to your project's policy (log, callback or quit). A build redistributed with altered payment logic falls here.
  • With the Pro Add-on, collecting telemetry from suspicious clients so you can act on them from the back office under your operational policy.

The two have an order. Server validation comes first. If the server does not properly confirm the proof, no amount of client hardening helps. Once server validation is in place, whether the client sending the request is a tampered build becomes the next question.

In closing: you are protecting the grant, not the payment

Treating in-app purchase security as "keeping payment from being broken" points you in the wrong direction. Store payment is already sturdy. What needs protecting is how your game confirms the claim that a payment finished.

If reconciliation and granted amounts are drifting apart, the cause is usually one of those six stages sitting empty. De-duplication and refund revocation in particular are easy to leave out at build time, and the damage from missing them accumulates quietly.

Next steps

  1. Check the protection scope — platform support and what each plan (Standard / Pro) defends, on the product page.
  2. Start a quick integration — import it into an existing project from Package Manager > My Assets (Asset Store purchase) or as a .unitypackage (portal download).
  3. Talk to us — if you have seen payment-related tampering or have a live-service issue, send it our way.

Check your server validation first, then look at the client sending the request.

Start with a client you can trust — OZero Security's Native integrity verification.

Related reading