Skip to content
Back to skills

Game Iap

ASecurity

Implement in-app purchases in a game or app — Google Play Billing, Apple StoreKit 2, one-time unlocks, consumables, subscriptions, entitlement caching for offline play, restore, refund handling, server-side receipt validation, sandbox testing, and the store-policy rules that get IAP builds rejected. Use when adding, debugging, or reviewing purchases, entitlements, paywalls, "remove ads" products, or premium unlocks.

  • 2 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added October 17, 2026
developmentgotestingdebugginggitapibackenddocumentation

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 6 files and shows the line behind each finding

Scanned October 17, 2026

npx -y skills add thomaslimited/game-shipping-skills --skill game-iap --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Game Iap?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Game Iap
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/thomaslimited-game-iap/badge)](https://www.skillsdirectory.com/skills/thomaslimited-game-iap)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: game-iap
description: Implement in-app purchases in a game or app — Google Play Billing, Apple StoreKit 2, one-time unlocks, consumables, subscriptions, entitlement caching for offline play, restore, refund handling, server-side receipt validation, sandbox testing, and the store-policy rules that get IAP builds rejected. Use when adding, debugging, or reviewing purchases, entitlements, paywalls, "remove ads" products, or premium unlocks.
license: MIT
metadata:
  version: "1.0.0"
  specs-verified: "2026-09-24"
---

# Game IAP

Purchases are the part of a game most likely to lose real money to a bug, and the part least
covered by tests because the store is remote and asynchronous. This skill gives the architecture
that makes IAP testable, the platform APIs, and the policy rules that cause rejection.

## Decide first

Answer these three before writing code. They determine everything else.

| Question | Consequence |
|---|---|
| **Consumable, non-consumable, or subscription?** | Non-consumable (level pack, remove-ads) → entitlement, must survive reinstall via restore. Consumable (gem pack) → must be *consumed* after granting or the user can never rebuy. Subscription → renewal state, grace periods, a server is effectively mandatory. |
| **Does the game have a server?** | No server → entitlements live in local save and the store is the only source of truth. Yes → validate receipts server-side and keep entitlement server-side; the client becomes a cache. |
| **Must purchased content work offline?** | Yes → you must cache entitlements locally and define what happens when the store is unreachable (see [Offline and refunds](#offline-and-refunds)). This is the single most common source of angry reviews. |

**Never gate content on a network call at launch.** A player who paid and boarded a plane must
still have their content.

## Core architecture

One interface, three implementations. This is the whole trick — it makes IAP unit-testable
without a store, and it is what lets you ship a desktop or web build that simply sells nothing.

```
StoreBackend (interface, async via callbacks/signals)
├── PlayBillingBackend    — Android
├── StoreKitBackend       — iOS
├── FakeStoreBackend      — tests, debug builds, screenshots
└── NullBackend           — platforms where you do not sell (desktop, web)
```

The interface needs exactly five outbound events and four inbound calls:

```
events:  purchase_finished(sku, success, error)
         purchases_synced(owned_skus[])      # the store's complete list
         sync_failed(error)                  # offline — do NOT clear entitlements
         prices_loaded({sku: localized_price})
         availability_changed(available)

calls:   start()          # connect, then immediately sync + load prices
         purchase(sku)
         restore()        # re-query owned products
         is_available()
```

`purchases_synced` carrying the **complete** list (not a delta) is what makes refunds work: when
the store answers, it replaces local state rather than merging into it.

Full worked implementation, including the reconciliation edge cases:
`references/reference-implementation-godot.md`

## Product catalog as data

Keep products in a data file (JSON), not in code, with: `id`, display name key, description key,
`type`, fallback price, and **what it unlocks**. Then:

- `is_covered(sku)` answers "does the player already have this?" including bundles — a
  `supporter_bundle` must cover `full_unlock`, which must cover `grade_2..grade_5`. Without a
  coverage rule you will sell someone content they already own.
- The store, the paywall UI and the tests all read the same list, so a typo shows up once.

**Product IDs are permanent.** Google Play and Apple both refuse to reuse an ID after it exists,
even deleted. Decide the naming scheme before creating the first one. Lowercase, underscores or
dots, no version numbers in the ID.

## Prices always come from the store

Never display a hard-coded price. Currency, tax-inclusive display and regional pricing are the
store's job, and showing the wrong one is a policy violation in several markets.

Fallback rule when the store has not answered yet: show the *local* list price only for the
currency you actually know (e.g. your home market), and show no price at all for everyone else —
a wrong currency is worse than a bare "Buy" button.

## Offline and refunds

Four states, and code must handle all four:

| State | Behaviour |
|---|---|
| Store reachable, sync succeeds | Store list **replaces** local entitlements. Refunded purchases disappear here — this is the only place a refund can be honoured. |
| Store unreachable | Keep cached entitlements untouched. Never downgrade a paying player because of a dropped connection. |
| Purchase completes *while* a sync is in flight | The sync result may predate the purchase. Track SKUs granted since the sync started and re-add them to the synced list, or the player loses what they just bought. |
| Purchase pending (slow card, cash payment, family approval) | Grant nothing yet. Show "pending". Google returns `PENDING`; grant only on `PURCHASED`. |

The third row is the bug nobody writes a test for. Write the test.

## Platform notes

- **Android** — `references/android-play-billing.md`. Play Billing Library 8+ is mandatory for new
  uploads and updates since **31 Aug 2026**; latest is 9.1.0. v8 renamed "in-app items" to
  "one-time products" and removed `querySkuDetailsAsync`, `queryPurchaseHistory` and the
  no-arg `enablePendingPurchases`.
- **iOS** — `references/ios-storekit.md`. StoreKit 2 (`Product`, `Transaction`,
  `Transaction.updates`) is the default; it verifies signatures for you. You must run a
  `Transaction.updates` listener for the whole app lifetime or you will drop purchases made
  outside your flow.
- **Server validation** — `references/server-receipt-validation.md`. Required the moment a
  purchase grants something a cheater would want. Play: Developer API + Real-time developer
  notifications (RTDN). Apple: App Store Server API + Server Notifications V2.

## Policy rules that cause rejection

- **Digital content must use the platform's IAP.** Both stores. Taking Stripe/PayPal/bank transfer
  for in-game content gets the app removed. (Physical goods and real-world services are the
  exception, and must *not* use IAP.)
- **US App Store only, since May 2025**: apps may include buttons and links to external purchase
  pages with no entitlement required. Outside the US storefront the old restriction still applies.
  Do not build a link-out flow globally on the strength of a US headline.
- **A restore path is mandatory** on iOS for non-consumables. A visible "Restore purchases"
  button. Rejections under 3.1.1 for a missing one are routine.
- **Kids' apps**: purchase entry points must sit behind a parent gate, and the gate must not be
  solvable by a young child (no simple "tap the biggest number"). See `app-auth` and
  `store-listing` for the Families/Kids declarations that follow from selling to children.
- **Do not promise content you gate later.** Changing a bought unlock into a subscription is a
  refund event and a review-bomb event.

## Testing

Ranked by what actually catches bugs:

1. **FakeStoreBackend unit tests** — every row of the offline/refund table above, plus
   double-purchase, purchase-while-owned, and cancelled flows. These run in CI with no store.
2. **Android closed testing / license testers** — real Play dialogs, no charge. License testers
   are set at the account level in Play Console, not per build. Purchases still have to be
   refunded/consumed between runs for consumables.
3. **iOS sandbox / StoreKit configuration file** — the `.storekit` file lets you test in the
   simulator without App Store Connect at all; do both, because the file does not catch
   App Store Connect misconfiguration.

Checklist before you call IAP done: `references/pre-launch-checklist.md`

## Sources

Specs verified 2026-09-24:
- [Play Billing release notes](https://developer.android.com/google/play/billing/release-notes)
- [StoreKit](https://developer.apple.com/documentation/storekit)
- [App Review Guidelines 3.1](https://developer.apple.com/app-store/review/guidelines/#in-app-purchase)

Files in this skill

  • SKILL.md8.1 KB
  • references/android-play-billing.md5.9 KB
  • references/ios-storekit.md4.7 KB
  • references/pre-launch-checklist.md2.6 KB
  • references/reference-implementation-godot.md6.7 KB
  • references/server-receipt-validation.md3.8 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.