gplay-purchase-verification
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePurchase Verification for Google Play
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
- 访问 Google Cloud Console
- 创建服务账号
- 授予"Service Account User"角色
- 下载JSON密钥
Grant API access
授予API访问权限
- Go to Play Console
- Users & Permissions → Service Accounts
- Grant service account access to your apps
- 访问 Play Console
- 用户与权限 → 服务账号
- 为你的应用授予服务账号访问权限
Verify In-App Product Purchase
验证应用内商品购买
Get purchase details
获取购买详情
bash
gplay purchases products get \
--package com.example.app \
--product-id premium_upgrade \
--token <PURCHASE_TOKEN>bash
gplay purchases products get \
--package com.example.app \
--product-id premium_upgrade \
--token <PURCHASE_TOKEN>Response
响应
json
{
"kind": "androidpublisher#productPurchase",
"purchaseTimeMillis": "1706400000000",
"purchaseState": 0,
"consumptionState": 0,
"developerPayload": "user_123",
"orderId": "GPA.1234-5678-9012-34567",
"purchaseType": 0
}json
{
"kind": "androidpublisher#productPurchase",
"purchaseTimeMillis": "1706400000000",
"purchaseState": 0,
"consumptionState": 0,
"developerPayload": "user_123",
"orderId": "GPA.1234-5678-9012-34567",
"purchaseType": 0
}Purchase states
购买状态
- = Purchased
0 - = Canceled
1 - = Pending
2
- = 已购买
0 - = 已取消
1 - = 待处理
2
Consumption states
消费状态
- = Yet to be consumed
0 - = Consumed
1
- = 未消费
0 - = 已消费
1
Acknowledge Purchase
确认购买
After verifying, acknowledge the purchase:
bash
gplay purchases products acknowledge \
--package com.example.app \
--product-id premium_upgrade \
--token <PURCHASE_TOKEN>Important: Unacknowledged purchases will be refunded after 3 days.
验证完成后,确认购买:
bash
gplay purchases products acknowledge \
--package com.example.app \
--product-id premium_upgrade \
--token <PURCHASE_TOKEN>重要提示: 未确认的购买将在3天后自动退款。
Consume Purchase (for consumables)
消费购买(针对可消耗商品)
For consumable items (coins, gems, etc.):
bash
gplay purchases products consume \
--package com.example.app \
--product-id coins_100 \
--token <PURCHASE_TOKEN>对于可消耗商品(如金币、宝石等):
bash
gplay purchases products consume \
--package com.example.app \
--product-id coins_100 \
--token <PURCHASE_TOKEN>Verify Subscription
验证订阅
Get subscription details
获取订阅详情
bash
gplay purchases subscriptions get \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN>bash
gplay purchases subscriptions get \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN>Response
响应
json
{
"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
}json
{
"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
订阅状态
- = Active
0 - = Canceled (still valid until expiry)
1 - = In grace period
2 - = On hold (payment failed, retrying)
3 - = Paused
4 - = Expired
5
- = 活跃
0 - = 已取消(在到期前仍有效)
1 - = 宽限期内
2 - = 暂停(付款失败,重试中)
3 - = 已暂停
4 - = 已过期
5
Payment states
付款状态
- = Payment pending
0 - = Payment received
1 - = Free trial
2 - = Pending deferred upgrade/downgrade
3
- = 付款待处理
0 - = 已收到付款
1 - = 免费试用
2 - = 待处理的延期升级/降级
3
Backend Implementation Example
后端实现示例
Node.js/Express
Node.js/Express
javascript
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 });
}
});javascript
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
Python/Flask
python
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)}), 400python
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)}), 400Handle Subscription Events
处理订阅事件
Real-time Developer Notifications (RTDN)
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:
python
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)设置Pub/Sub以接收订阅事件:
-
在Google Cloud Console中创建Pub/Sub主题
-
在Play Console中配置:
- 变现设置 → 实时开发者通知
- 输入主题名称
-
订阅事件:
python
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
取消订阅
bash
gplay purchases subscriptions cancel \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN>bash
gplay purchases subscriptions cancel \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN>Defer subscription
延期订阅
bash
gplay purchases subscriptions defer \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN> \
--json @defer.jsonbash
gplay purchases subscriptions defer \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN> \
--json @defer.jsondefer.json
defer.json
json
{
"deferralInfo": {
"expectedExpiryTimeMillis": "1709000000000"
}
}json
{
"deferralInfo": {
"expectedExpiryTimeMillis": "1709000000000"
}
}Revoke subscription (refund)
撤销订阅(退款)
bash
gplay purchases subscriptions revoke \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN>bash
gplay purchases subscriptions revoke \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN>Check Voided Purchases
查看已作废的购买
Get list of refunded/canceled purchases:
bash
gplay purchases voided list \
--package com.example.app \
--start-time 1706400000000 \
--end-time 1709000000000Remove entitlements for these purchases on your backend.
获取已退款/取消的购买列表:
bash
gplay purchases voided list \
--package com.example.app \
--start-time 1706400000000 \
--end-time 1709000000000在后端为这些购买移除用户权限。
Order Information
订单信息
Get order details
获取订单详情
bash
gplay orders get \
--package com.example.app \
--order-id GPA.1234-5678-9012-34567bash
gplay orders get \
--package com.example.app \
--order-id GPA.1234-5678-9012-34567Batch get orders
批量获取订单
bash
gplay orders batch-get \
--package com.example.app \
--order-ids "GPA.1234,GPA.5678,GPA.9012"bash
gplay orders batch-get \
--package com.example.app \
--order-ids "GPA.1234,GPA.5678,GPA.9012"Refund order
退款订单
bash
gplay orders refund \
--package com.example.app \
--order-id GPA.1234-5678-9012-34567 \
--revoke # Also revoke accessbash
gplay orders refund \
--package com.example.app \
--order-id GPA.1234-5678-9012-34567 \
--revoke # Also revoke accessSecurity 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
- ✅ 始终在服务器端验证,绝不信任客户端
- ✅ 安全存储购买令牌
- ✅ 在3天内确认购买
- ✅ 处理退款和取消
- ✅ 所有API调用使用HTTPS
- ✅ 对验证端点进行限流
- ✅ 记录所有验证尝试
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)
- ❌ 仅在客户端验证购买
- ❌ 暴露服务账号凭证
- ❌ 跳过确认购买步骤
- ❌ 在验证前授予权限
- ❌ 忽略已作废的购买
- ❌ 存储信用卡信息(符合PCI合规)
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)
- 用户在应用内完成购买
- 应用将购买令牌发送到你的服务器
- 服务器通过Google Play API验证
- 服务器确认购买(如果有效)
- 服务器向用户授予权限/内容
- 服务器存储购买令牌以备后续检查
- 服务器监听RTDN事件(取消、续订等)
Error Handling
错误处理
Common errors
常见错误
- - Service account not authorized
401 Unauthorized - - Purchase token invalid or expired
404 Not Found - - Purchase was refunded/canceled
410 Gone
- - 服务账号未授权
401 Unauthorized - - 购买令牌无效或已过期
404 Not Found - - 购买已退款/取消
410 Gone
Retry logic
重试逻辑
javascript
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)));
}
}
}javascript
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.
使用Google Play的测试账号进行测试购买,无需实际扣费。
Test verification
测试验证
bash
undefinedbash
undefinedVerify test purchase
Verify test purchase
gplay purchases products get
--package com.example.app
--product-id android.test.purchased
--token <TEST_TOKEN>
--package com.example.app
--product-id android.test.purchased
--token <TEST_TOKEN>
undefinedgplay purchases products get
--package com.example.app
--product-id android.test.purchased
--token <TEST_TOKEN>
--package com.example.app
--product-id android.test.purchased
--token <TEST_TOKEN>
undefinedMonitoring
监控
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.
跟踪以下指标:
- 购买验证成功率
- 确认率
- 退款率
- 订阅流失率
- 付款失败率
使用这些数据优化你的变现策略。