Drawlogue docs
v2.0.0
Live demoConsole demo Get help
● Monetise · Store products and verification

In-app purchases.
Store products and server verification.

Users buy plans, credit packs and Remove ads through Google Play or the App Store. Nothing is granted until your backend has verified the purchase with the store, and one purchase entitles one account at a time. This page covers the products, the store keys the backend needs, and what happens on a purchase.

Google Play and App Store onlyVerified on the serverOne purchase, one account at a time503 iap_unconfigured until set upShips off

01The store products

A product is an item you create in each store with a product id and a price. The app sells the ids you enter in the console; prices are set in the stores, never in the code.

You must create your own products. The product ids you see in the console (pro_monthly, pro_yearly, remove_ads, credits_small, credits_medium, credits_large) are placeholder names: no such product exists in any store account until you create it in your own Google Play Console and App Store Connect, and the app sells nothing until then. No product id, store key or store account of the author ships, and the backend verifies purchases only with the credentials you enter (check it yourself).

Example id (a name, not a product)ConsoleGoogle Play typeApp Store typeEffect after verification
pro_monthly, pro_yearly (and any weekly ids you add)Plans → the plan → products per periodSubscription (one per period)Auto-renewable subscriptionThe plan until the period ends; renewals extend it
A lifetime id you choose (for example pro_lifetime)Plans → lifetimeOne-time productNon-consumableThe plan for life
remove_adsStore & purchases → Remove-ads product idOne-time productNon-consumableNo ads
credits_small, credits_medium, credits_largeStore & purchases → Credit pack S / M / L product idOne-time product (bought as consumable)Consumable+ credits in the pack (default 50, 200, 600)

These ids are example names. No product with these names exists until you create it in your own Google Play Console and App Store Connect; you can also type the ids you already have. The console says so under each field. The example plan sells only monthly and yearly; weekly and lifetime are empty until you add ids. Click paths for creating the products: Google Play Console, App Store Connect. A free trial, if you want one, is an introductory offer on the store product; the plan's trial days only change the text in the app.

02Everything is configured by you

Purchases are entirely buyer-configured. Nothing of the author is in the code:

  • Product ids: the defaults above (pro_monthly, remove_ads, credits_small, …) are plain placeholder names, not products of any account. A product id only means something inside the store account that owns the app's package name or bundle id, which is yours after you rename the app (Release → ids). You can keep these names or enter your own in the console.
  • Verification endpoints: the backend talks only to the stores' official APIs, androidpublisher.googleapis.com (Google Play Developer API) and api.storekit.itunes.apple.com (App Store Server API; api.storekit-sandbox.itunes.apple.com in sandbox), authenticated with your service account and your App Store key from Console → Store & purchases (firebase/functions/src/iap.ts). No receipt is sent anywhere else.
  • The app's backend address is your own API_BASE_URL, set when you build; it is empty in the source (app/lib/core/env.dart), and the app shows a setup screen without it.
  • Credentials: the package name, service account JSON, issuer id, key id, .p8 key and bundle id are empty until you enter them; the secret ones are stored encrypted and written only to config/private (schema). No Google service account key and no App Store key is in the source: check it yourself.
  • Money goes from Google Play and the App Store to your developer accounts. The code pays and reports nothing to the author.

03No store key ships: check it yourself

In plain words: the two keys that let a server ask Google Play and the App Store about purchases are yours. They do not exist in the download. You paste them into the console after you install it, and they never leave your server.

KeyIn the downloadWhere yours goesWhere it is stored
Google Play service account (JSON key file)Not included. No .json key file, no private key block.Console → Store & purchases → Google Play service account (JSON), or none at all if you use the Cloud Functions runtime accountEncrypted in the console's storage; written only to config/private.iap.googleServiceAccountJson
App Store In-App Purchase key (.p8)Not included. No .p8 file, no private key block.Console → Store & purchases → In-app purchase key (.p8 contents)Encrypted in the console's storage; written only to config/private.iap.applePrivateKey
Package name, bundle id, issuer id, key idEmpty stringsThe fields next to the keysconfig/private.iap

Three ways to verify it in the source:

  1. The list of secret fieldsadmin/src/sync.ts, SECRET_KEYS, names every secret the console can hold, including iap.googleServiceAccountJson and iap.applePrivateKey. They are write-only fields without a default (admin/src/manifest.ts, section store), and the backend's defaults for them are empty strings (firebase/functions/src/api/defaults.ts, DEFAULT_PRIVATE.iap).
  2. One commandFrom admin/ run tool/check_no_keys.sh. It searches the whole source folder for key files (.p8, .p12, .pem, keystores, service account files, google-services.json, GoogleService-Info.plist, .env), private key blocks and literal API keys, prints file names only, and ends with PASS no key or credential file is in ….
  3. The testsadmin/test/manifest.test.ts ("no ad id, store key or store identifier ships as a default") and firebase/functions/test/unit/defaults.test.ts fail if a default stops being empty.

Terminal: macOS or Linux as shown; on Windows use WSL or Git Bash (how).

Run the check on your own copy too

After you have set up the project, the same command fails as soon as a .env, a service account file or a key file lies inside the source folder. Run it before you send your copy to a developer or put it in a repository.

04Google Play verification

  1. Package nameConsole → Store & purchases → Android package name: your application id (for example com.yourcompany.yourapp).
  2. Access to the Play Developer APIEnable "Google Play Android Developer API" in Google Cloud, then invite the service account in Play Console → Users and permissions with View financial data for your app. By default the backend uses the Cloud Functions runtime service account (<project-number>-compute@developer.gserviceaccount.com).
  3. Or a separate service accountPaste its whole JSON key into Google Play service account (JSON) instead, and invite that account's e-mail the same way. The console's test only checks the file's shape ("Service account … — the file is complete."); nothing is sent.

Every click: Provider dashboards → Google Play Console.

05App Store verification

  1. Create an In-App Purchase keyApp Store Connect → Users and Access → Integrations → In-App Purchase → generate a key and download the .p8 file (it downloads once).
  2. Enter four valuesConsole → Store & purchases: iOS bundle id, App Store Connect issuer id, In-app purchase key id and In-app purchase key (.p8 contents) (paste the whole file including the BEGIN and END lines). The test checks that the key parses as an EC key.
  3. EnvironmentApp Store environment: Sandbox (TestFlight) while you test, Production for the live app.

The backend reads each transaction from the App Store Server API with a token signed by this key. Every click: Provider dashboards → App Store Connect.

06Switch purchases on

  1. Products approvedCreate and activate the products in both stores (Apple needs them submitted with an app version the first time).
  2. PlansConsole → Plans: product ids, features, limits, Active (Plans and paywall).
  3. Master switchConsole → Store & purchases → Sell in-app purchases on, Save.
  4. Check Status"Store purchases configured" and "Plans are sellable" should pass.
  5. TestAndroid: upload a build to an internal testing track and add licence testers in Play Console. iOS: TestFlight with a sandbox account and the Sandbox environment.

07What happens on a purchase

  1. The store chargesThe app buys through Google Play Billing or StoreKit and receives a purchase token (Android) or transaction id (iOS).
  2. The app asks the backendPOST /v1/purchases/verify with the product id and the token.
  3. The backend verifieswith Google or Apple, maps the product id to a plan and period (or to Remove ads or a credit pack), then grants it once per order, stores purchases/{platform}_{orderId} and, when the store reports a price, writes one revenue event for the Revenue centre.

08One purchase, one account at a time

  • A plan or Remove ads bought once belongs to one account at a time. If the same store purchase is restored while signed in to another account, the entitlement moves to that account and is removed from the previous one.
  • A credit pack is credited once. Restoring or re-sending the same order never adds credits again.
  • A plan granted by hand in the console (Users) is separate from the store plan and is never touched by a store purchase or restore; the user gets the active plan with the higher rank.
  • For a plan to survive a reinstall or a new phone, the user should sign in (Google, Apple or e-mail) and tap Restore purchases.

09Before verification is set up

503 iap_unconfigured

Until the package name (Android) or the four Apple values (iOS) are set, verification answers 503 {"error":"iap_unconfigured"} and grants nothing. The app then leaves the store transaction pending instead of completing it: the store delivers it again on the next start and the app retries, and Google Play refunds a purchase that stays unacknowledged for 3 days. Still, switch purchases on only after verification is set up, so no buyer waits. A purchase the backend rejects for good (an invalid token) is completed and not granted. Fixes: Troubleshooting → purchases.

10Records and account deletion

When a user deletes their account, purchases and revenueEvents are kept (with the user id as an opaque id) because you may have to keep financial records. Mention this in your privacy policy (Account deletion).