Build it into your app
Google Play subscriptions
For Android apps that sell subscriptions through Google Play and have no backend of their own. Your app sends the purchase token to the gateway with an API key; the gateway asks Google with your service account and answers whether the subscription is active. The check is free.
#Before you start
- A Play Console login that is the account owner or an admin, so it can invite users.
- A Google account that can create a project in Google Cloud console (any Google account can).
- Your app’s package name (applicationId), for example com.example.app, and at least one subscription product created in Play Console.
- About 15 minutes of clicking, then up to a day for Google to switch the permission on.
#1. Get the service account JSON
The JSON file is the service account’s key. It lets the gateway ask Google about your app’s purchases and nothing else. Google calls it a service account key; it is not the OAuth client file (client_secret_….json).
- 1
Open Google Cloud console and pick a project
console.cloud.google.comUse the project selector at the top. Any project works; creating a new one just for Play checks keeps things tidy.
- 2
Turn on the Play API
APIs & Services → Library → Google Play Android Developer API → EnableSearch for the name exactly. If the button says Manage, it is already on.
- 3
Create the service account
IAM & Admin → Service accounts → Create service accountGive it any name, such as play-verify. Skip the optional steps that grant project roles or user access: it needs no Cloud role. Click Done.
- 4
Download its JSON key
Open the service account → Keys → Add key → Create new key → JSON → CreateThe browser downloads a .json file. Keep it safe: Google never shows it again, and anyone with it can read your app’s purchases.
- 5
Copy the service account email
It looks like play-verify@your-project.iam.gserviceaccount.com and is shown on the service account page and inside the file as client_email. You need it in the next step.
Create new key is greyed out or refused?
#2. Give it access in Play Console
- 1
Invite the service account
play.google.com/console → Users and permissions → Invite new usersPaste the service account email as the email address. A service account does not need to accept the invite.
- 2
Grant these two permissions
App permissions → Add app → choose your appTick View financial data, orders, and cancellation survey responses, and Manage orders and subscriptions. Leave everything else off. Ticking the same two under Account permissions instead covers all your apps.
- 3
Send the invite
Apply → Invite userThe service account now appears in Users and permissions as Active.
Why these permissions
Google can take up to a day to switch it on
#3. Put it on an API key
- 1
Open the key your app uses
Console → API keys → EditUse a key just for the app (see below).
- 2
Fill in Google Play subscription check
Set up Google Play checkEnter the package name exactly as in your app’s build.gradle (applicationId), choose the JSON file from step 1, and save.
- 3
Read what the save tells you
The gateway tries the file with Google before storing it. If Google still refuses, the dialog says what is missing, usually the API from step 1 or the permission from step 2. The file is stored encrypted and never shown again; upload it again to replace it.
Use a key just for the app
#4. Check from the app
Check right after a purchase, and again when the app opens with the token you saved. Do not block app start on the answer: show the last saved result and update it when the check returns. The same token is answered from a one-minute cache. Each key may check 120 times per minute by default; the key’s Google Play settings show its current limit, and support can raise it for one key. Calls over the limit answer 429 and are not logged, so keep the last saved answer.
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:in_app_purchase/in_app_purchase.dart';
const apiKey = 'sk-YOUR_KEY';
Future<Map<String, dynamic>?> checkSubscription(String purchaseToken) async {
final res = await http.post(
Uri.parse('https://www.aisplice.store/v1/google-play/verify'),
headers: {
'Authorization': 'Bearer $apiKey',
'Content-Type': 'application/json',
// Request signing on for this key? Then this call must be signed too:
// ...signedHeaders('POST', '/v1/google-play/verify'),
},
body: jsonEncode({'purchase_token': purchaseToken}),
);
if (res.statusCode != 200) return null; // keep the last saved answer
return jsonDecode(res.body) as Map<String, dynamic>;
}
// In your purchaseStream listener:
Future<void> onPurchase(PurchaseDetails purchase) async {
if (purchase.status != PurchaseStatus.purchased &&
purchase.status != PurchaseStatus.restored) return;
final token = purchase.verificationData.serverVerificationData;
final status = await checkSubscription(token);
if (status != null && status['active'] == true) {
await saveEntitlement(token, status); // your local storage
unlockPaidFeatures();
}
if (purchase.pendingCompletePurchase) {
await InAppPurchase.instance.completePurchase(purchase); // acknowledges on Android
}
}#5. Decide what to unlock
| Check answer | What the app does | Typical cause |
|---|---|---|
| active: true, in_trial: true | Unlock. Show “Trial ends” with expiry_time. | Free trial |
| active: true | Unlock until expiry_time. | Paid, including canceled subscriptions that have not run out yet |
| subscribed: true, active: false | Lock and show a renew screen. | Expired, paused, on hold |
| subscribed: false | Lock and show the subscribe screen. | Unknown token, pending payment |
| HTTP 502 / 503 | Keep the last answer you saved and retry later. | Google or the service account is not answering |
Save expiry_time with the answer. Between checks, treat the subscription as usable until that time, so a phone that is offline keeps working.
#6. Acknowledge every purchase
Unacknowledged purchases are refunded after 3 days
#7. Protect the key in the app
A key inside an app can be read from captured traffic. Turn on request signing for that key so a copied key cannot be used on its own, obfuscate the build, and keep a quota ceiling on the key.
Signing covers every call made with the key
#8. Check what happened
Every check, including cached answers and failures, is listed in Console → Request logs as a free “Google Play check” on the key. Search by the full purchase token, its last characters, or the order ID (GPA.…) a user sends you from Google’s receipt email.
- google_play_api_disabled: the Play API from step 1 is not enabled in the project the JSON file belongs to.
- google_play_permission_denied: the invite from step 2 is missing, lacks the two permissions, is for another app, or is not active yet.
- google_play_credentials_invalid: the JSON key was deleted or disabled in Google Cloud. Create a new key and upload it on the API key.
- The save says the file is not a service account key: you picked the OAuth client file. Download the key from the service account’s Keys tab instead.
- Every check says the purchase was not found: the package name on the key differs from the app’s applicationId, or the token comes from another app.
- Test purchases by license testers answer test_purchase: true.
Still stuck?
Send us the request ID or the error message and we will look into it.