# In-app purchases and subscriptions done right

> In-app purchases and subscriptions in a mobile app: store rules, server-side receipt validation, restoring purchases and the common mistakes.

Published: 2025-12-25 · Source: https://subxcat.com/articles/in-app-purchases-basics · Author: SUBXCAT

In-app purchases are payments for digital things inside a mobile app, handled by the store's own system: StoreKit on Apple, Google Play Billing on Google. The store charges the money and handles refunds and cancellations; your job is to understand correctly what the person bought and grant them access.

In a demo, a purchase looks simple: tap a button, see the system sheet, get premium. In real life subscriptions renew, fail to renew, get refunded and move to new phones, and all of that has to end up in your database.

## What goes through in-app purchases

The rule in general terms: anything digital consumed inside the app goes through the store. Subscriptions, premium features, credits, in-game currency, content. Physical goods and real-world services — delivery, rides, concert tickets — are paid through a regular payment provider.

There are three kinds of purchase:

- **consumables** — credits, coins: buy, spend, buy again;
- **non-consumables** — a permanent unlock, bought once;
- **auto-renewing subscriptions** — access for a period that renews itself.

Prices are set in the store console, not in code. On the purchase screen, show the price the store returns: it's already in the user's currency and local format. A subscription also needs the period, the price, and an honest line saying it renews automatically. Only the user can cancel a subscription, in the store's settings — you can't do it for them, but you can show where it's done.

## Server-side receipt validation

The core rule: the app doesn't decide whether someone is premium. The server does.

After a purchase, the app gets proof from the store — a signed transaction on Apple, a purchase token on Google. It sends that to your server, and the server checks it with the store itself through the store's server API: is the purchase real, which product, valid until when. Only then does the server record access.

It's convenient to keep access in one table, wherever the money came from:

```sql
create table entitlements (
  user_id      bigint not null,
  product      text   not null,
  source       text   not null,  -- apple, google, web
  expires_at   timestamptz,
  original_txn text   not null unique
);
```

The unique original transaction ID protects you from granting twice when the same purchase arrives both from the app and from a store notification.

## Store server notifications

Renewals, cancellations, refunds, card problems — all of this happens without the app, sometimes when it hasn't been opened for weeks. The only way to find out is the notifications the store sends to your server: App Store Server Notifications on Apple and Real-time developer notifications on Google.

Connect them on day one. Without them, a refunded subscription stays active forever, and a renewed one looks expired until the person opens the app. Mind the grace period too: when a card fails, the store keeps retrying for a while, so don't cut access in the first minute.

## Restoring purchases

A "Restore purchases" button is mandatory — Apple will reject the app without it. It's for people who changed phones or reinstalled the app: the store hands back their previous purchases, and you grant access again.

Decide in advance what happens when a purchase was made under one app account and someone restores it while logged into another. Moving access to the current account, or saying "this subscription belongs to a different login" — both are fine. Not deciding at all is the problem.

## Common mistakes with in-app purchases

- Access is granted from a flag the app sends, with no server check.
- Access is granted when the button is tapped, not after the purchase is confirmed.
- On iOS the transaction is never finished after granting access, so the store delivers it again on every launch.
- On Android the purchase isn't acknowledged within a few days, and Google refunds it automatically.
- Refunds aren't handled, so people who got their money back keep access.
- The price is hardcoded as text in the UI and drifts from the real one.
- Products aren't submitted for review along with the build, so the reviewer can't buy anything.

Test in the sandbox: Apple has sandbox accounts, Google has license testers, and in both, subscriptions renew on an accelerated clock, so a month passes in minutes.

You don't have to write all of this yourself — there are services that handle validation and notifications for you. That saves time but adds a dependency, so you still want the entitlements table on your side.

## In short

- Digital goods inside the app go through in-app purchases; physical ones use regular payments.
- The server grants access after validating the purchase with the store, not the app.
- Store server notifications are needed from day one: renewals, cancellations, refunds.
- A restore button is mandatory, and the rule for switching accounts is decided upfront.
- Finish transactions on iOS and acknowledge purchases on Android.

SUBXCAT can build purchases and subscriptions so that access in your database matches what people actually paid for.
