skills/gplay-purchase-verification/SKILL.md
Server-side purchase verification for in-app products and subscriptions using Google Play Developer API. Use when implementing receipt validation in your backend.
npx skillsauth add tamtom/gplay-cli-skills gplay-purchase-verificationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill when you need to verify in-app purchases or subscriptions from your backend server.
Client-side verification can be bypassed. Always verify purchases on your server:
Your backend needs a service account with permissions to verify purchases.
gplay purchases products get \
--package com.example.app \
--product-id premium_upgrade \
--token <PURCHASE_TOKEN>
{
"kind": "androidpublisher#productPurchase",
"purchaseTimeMillis": "1706400000000",
"purchaseState": 0,
"consumptionState": 0,
"developerPayload": "user_123",
"orderId": "GPA.1234-5678-9012-34567",
"purchaseType": 0
}
0 = Purchased1 = Canceled2 = Pending0 = Yet to be consumed1 = ConsumedAfter 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.
For consumable items (coins, gems, etc.):
gplay purchases products consume \
--package com.example.app \
--product-id coins_100 \
--token <PURCHASE_TOKEN>
gplay purchases subscriptions get \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN>
{
"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
}
0 = Active1 = Canceled (still valid until expiry)2 = In grace period3 = On hold (payment failed, retrying)4 = Paused5 = Expired0 = Payment pending1 = Payment received2 = Free trial3 = Pending deferred upgrade/downgradeconst { 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 });
}
});
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
Set up Pub/Sub to receive subscription events:
Create Pub/Sub topic in Google Cloud Console
Configure in Play Console:
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)
gplay purchases subscriptions cancel \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN>
gplay purchases subscriptions defer \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN> \
--json @defer.json
{
"deferralInfo": {
"expectedExpiryTimeMillis": "1709000000000"
}
}
gplay purchases subscriptions revoke \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN>
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.
gplay orders get \
--package com.example.app \
--order-id GPA.1234-5678-9012-34567
gplay orders batch-get \
--package com.example.app \
--order-ids "GPA.1234,GPA.5678,GPA.9012"
gplay orders refund \
--package com.example.app \
--order-id GPA.1234-5678-9012-34567 \
--revoke # Also revoke access
401 Unauthorized - Service account not authorized404 Not Found - Purchase token invalid or expired410 Gone - Purchase was refunded/canceledasync 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)));
}
}
}
Use Google Play's test accounts to make test purchases without charging real money.
# Verify test purchase
gplay purchases products get \
--package com.example.app \
--product-id android.test.purchased \
--token <TEST_TOKEN>
Track these metrics:
Use this data to improve your monetization strategy.
development
App vitals monitoring for crashes, ANRs, performance metrics, and errors via gplay vitals commands. Use when asked to check app stability, crash rates, ANR rates, or performance data from Google Play Console.
development
User and grant management for Google Play Console via gplay users and gplay grants commands. Use when asked to manage developer account users, permissions, or app-level access grants.
development
Beta testing groups and tester management for Google Play closed testing tracks. Use when managing testers and beta groups.
tools
Bulk-localize subscription display names, descriptions, and offer tags across all Google Play locales using gplay. Use when you want to fill in subscription metadata for every language without clicking through Play Console manually.