gplay-purchase-verification
Purchase Verification for Google Play
Use this skill when you need to verify in-app purchases or subscriptions from your backend server.
Why Verify Purchases Server-Side?
Client-side verification can be bypassed. Always verify purchases on your server:
- Prevent fraud and piracy
- Ensure user actually paid
- Check subscription status
- Handle refunds and cancellations
Authentication Setup
Your backend needs a service account with permissions to verify purchases.
Create service account
- Go to Google Cloud Console
- Create service account
- Grant "Service Account User" role
- Download JSON key
Grant API access
- Go to Play Console
- Users & Permissions → Service Accounts
- Grant service account access to your apps
Verify In-App Product Purchase
Get purchase details
gplay purchases products get \
--package com.example.app \
--product-id premium_upgrade \
--token <PURCHASE_TOKEN>
Response
{
"kind": "androidpublisher#productPurchase",
"purchaseTimeMillis": "1706400000000",
"purchaseState": 0,
"consumptionState": 0,
"developerPayload": "user_123",
"orderId": "GPA.1234-5678-9012-34567",
"purchaseType": 0
}
Purchase states
0= Purchased1= Canceled2= Pending
Consumption states
0= Yet to be consumed1= Consumed
Acknowledge Purchase
After verifying, acknowledge the purchase:
gplay purchases products acknowledge \
--package com.example.app \
--product-id premium_upgrade \
--token <PURCHASE_TOKEN>
Important: Unacknowledged purchases will be refunded after 3 days.
Consume Purchase (for consumables)
For consumable items (coins, gems, etc.):
gplay purchases products consume \
--package com.example.app \
--product-id coins_100 \
--token <PURCHASE_TOKEN>
Verify Subscription
Get subscription details
gplay purchases subscriptions get \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN>
Response
{
"kind": "androidpublisher#subscriptionPurchase",
"startTimeMillis": "1706400000000",
"expiryTimeMillis": "1709000000000",
"autoRenewing": true,
"priceCurrencyCode": "USD",
"priceAmountMicros": "4990000",
"paymentState": 1,
"cancelReason": null,
"userCancellationTimeMillis": null,
"orderId": "GPA.1234-5678-9012-34567",
"linkedPurchaseToken": null,
"subscriptionState": 0
}
Subscription states
0= Active1= Canceled (still valid until expiry)2= In grace period3= On hold (payment failed, retrying)4= Paused5= Expired
Payment states
0= Payment pending1= Payment received2= Free trial3= Pending deferred upgrade/downgrade
Backend Implementation Example
Node.js/Express
const { google } = require('googleapis');
async function verifyPurchase(packageName, productId, token) {
const auth = new google.auth.GoogleAuth({
keyFile: '/path/to/service-account.json',
scopes: ['https://www.googleapis.com/auth/androidpublisher'],
});
const androidpublisher = google.androidpublisher({
version: 'v3',
auth: await auth.getClient(),
});
const result = await androidpublisher.purchases.products.get({
packageName: packageName,
productId: productId,
token: token,
});
return result.data;
}
// Endpoint
app.post('/verify-purchase', async (req, res) => {
const { packageName, productId, token } = req.body;
try {
const purchase = await verifyPurchase(packageName, productId, token);
if (purchase.purchaseState === 0) {
// Purchase is valid
// Grant access to user
// Acknowledge purchase
res.json({ valid: true, purchase });
} else {
res.json({ valid: false });
}
} catch (error) {
res.status(400).json({ error: error.message });
}
});
Python/Flask
from google.oauth2 import service_account
from googleapiclient.discovery import build
SCOPES = ['https://www.googleapis.com/auth/androidpublisher']
SERVICE_ACCOUNT_FILE = '/path/to/service-account.json'
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES)
androidpublisher = build('androidpublisher', 'v3', credentials=credentials)
@app.route('/verify-purchase', methods=['POST'])
def verify_purchase():
data = request.json
package_name = data['packageName']
product_id = data['productId']
token = data['token']
try:
result = androidpublisher.purchases().products().get(
packageName=package_name,
productId=product_id,
token=token
).execute()
if result['purchaseState'] == 0:
# Purchase is valid
return jsonify({'valid': True, 'purchase': result})
else:
return jsonify({'valid': False})
except Exception as e:
return jsonify({'error': str(e)}), 400
Handle Subscription Events
Real-time Developer Notifications (RTDN)
Set up Pub/Sub to receive subscription events:
-
Create Pub/Sub topic in Google Cloud Console
-
Configure in Play Console:
- Monetization Setup → Real-time developer notifications
- Enter topic name
-
Subscribe to events:
from google.cloud import pubsub_v1
subscriber = pubsub_v1.SubscriberClient()
subscription_path = subscriber.subscription_path(project_id, subscription_id)
def callback(message):
data = json.loads(message.data)
if 'subscriptionNotification' in data:
notification = data['subscriptionNotification']
notification_type = notification['notificationType']
purchase_token = notification['purchaseToken']
# Handle different events
if notification_type == 1: # SUBSCRIPTION_RECOVERED
# Subscription was recovered from account hold
pass
elif notification_type == 2: # SUBSCRIPTION_RENEWED
# Subscription renewed successfully
pass
elif notification_type == 3: # SUBSCRIPTION_CANCELED
# User canceled subscription
pass
elif notification_type == 4: # SUBSCRIPTION_PURCHASED
# New subscription purchase
pass
elif notification_type == 7: # SUBSCRIPTION_EXPIRED
# Subscription expired
pass
elif notification_type == 10: # SUBSCRIPTION_PAUSED
# Subscription paused
pass
elif notification_type == 12: # SUBSCRIPTION_REVOKED
# Subscription revoked (refunded)
pass
message.ack()
subscriber.subscribe(subscription_path, callback=callback)
Subscription Management
Cancel subscription
gplay purchases subscriptions cancel \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN>
Defer subscription
gplay purchases subscriptions defer \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN> \
--json @defer.json
defer.json
{
"deferralInfo": {
"expectedExpiryTimeMillis": "1709000000000"
}
}
Revoke subscription (refund)
gplay purchases subscriptions revoke \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN>
Check Voided Purchases
Get list of refunded/canceled purchases:
gplay purchases voided list \
--package com.example.app \
--start-time 1706400000000 \
--end-time 1709000000000
Remove entitlements for these purchases on your backend.
Order Information
Get order details
gplay orders get \
--package com.example.app \
--order-id GPA.1234-5678-9012-34567
Batch get orders
gplay orders batch-get \
--package com.example.app \
--order-ids "GPA.1234,GPA.5678,GPA.9012"
Refund order
gplay orders refund \
--package com.example.app \
--order-id GPA.1234-5678-9012-34567 \
--revoke # Also revoke access
Security Best Practices
DO:
- ✅ Always verify on server, never trust client
- ✅ Store purchase tokens securely
- ✅ Acknowledge purchases within 3 days
- ✅ Handle refunds and cancellations
- ✅ Use HTTPS for all API calls
- ✅ Rate limit your verification endpoint
- ✅ Log all verification attempts
DON'T:
- ❌ Verify purchases only on client
- ❌ Expose service account credentials
- ❌ Skip acknowledging purchases
- ❌ Grant access before verification
- ❌ Ignore voided purchases
- ❌ Store credit card info (PCI compliance)
Common Verification Flow
- User makes purchase in app
- App sends purchase token to your server
- Server verifies with Google Play API
- Server acknowledges purchase (if valid)
- Server grants access/content to user
- Server stores purchase token for future checks
- Server listens for RTDN events (cancellations, renewals)
Error Handling
Common errors
401 Unauthorized- Service account not authorized404 Not Found- Purchase token invalid or expired410 Gone- Purchase was refunded/canceled
Retry logic
async function verifyWithRetry(packageName, productId, token, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await verifyPurchase(packageName, productId, token);
} catch (error) {
if (error.code === 404 || error.code === 410) {
throw error; // Don't retry if purchase is invalid
}
if (i === retries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
Testing
Test purchases
Use Google Play's test accounts to make test purchases without charging real money.
Test verification
# Verify test purchase
gplay purchases products get \
--package com.example.app \
--product-id android.test.purchased \
--token <TEST_TOKEN>
Monitoring
Track these metrics:
- Purchase verification success rate
- Acknowledgment rate
- Refund rate
- Subscription churn rate
- Failed payment rate
Use this data to improve your monetization strategy.
More from tamtom/gplay-cli-skills
gplay-cli-usage
Guidance for using the Google Play Console CLI in this repo (flags, output formats, pagination, auth, and discovery). Use when asked to run or design gplay commands or interact with Google Play Console via the CLI.
108gplay-gradle-build
Build, sign, and package Android apps with Gradle before uploading to Google Play. Use when asked to create an APK or AAB, configure signing, or set up build pipelines.
95gplay-submission-checks
Pre-submission validation for Google Play releases covering metadata, screenshots, bundle integrity, data safety, and policy compliance. Use when preparing a release to avoid rejections and catch issues before submitting.
93gplay-metadata-sync
Metadata and localization sync (including Fastlane format) for Google Play Store listings. Use when updating app descriptions, screenshots, or managing multi-locale metadata.
93gplay-signing-setup
Android app signing, keystores, and Play App Signing setup. Use when configuring signing for new apps or migrating to Play App Signing.
91gplay-iap-setup
In-app products, subscriptions, base plans, and offers setup for Google Play monetization. Use when configuring in-app purchases or subscription products.
89