gplay-purchase-verification

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Purchase 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

创建服务账号

  1. Go to Google Cloud Console
  2. Create service account
  3. Grant "Service Account User" role
  4. Download JSON key
  1. 访问 Google Cloud Console
  2. 创建服务账号
  3. 授予"Service Account User"角色
  4. 下载JSON密钥

Grant API access

授予API访问权限

  1. Go to Play Console
  2. Users & Permissions → Service Accounts
  3. Grant service account access to your apps
  1. 访问 Play Console
  2. 用户与权限 → 服务账号
  3. 为你的应用授予服务账号访问权限

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

购买状态

  • 0
    = Purchased
  • 1
    = Canceled
  • 2
    = Pending
  • 0
    = 已购买
  • 1
    = 已取消
  • 2
    = 待处理

Consumption states

消费状态

  • 0
    = Yet to be consumed
  • 1
    = Consumed
  • 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

订阅状态

  • 0
    = Active
  • 1
    = Canceled (still valid until expiry)
  • 2
    = In grace period
  • 3
    = On hold (payment failed, retrying)
  • 4
    = Paused
  • 5
    = Expired
  • 0
    = 活跃
  • 1
    = 已取消(在到期前仍有效)
  • 2
    = 宽限期内
  • 3
    = 暂停(付款失败,重试中)
  • 4
    = 已暂停
  • 5
    = 已过期

Payment states

付款状态

  • 0
    = Payment pending
  • 1
    = Payment received
  • 2
    = Free trial
  • 3
    = Pending deferred upgrade/downgrade
  • 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)}), 400
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)}), 400

Handle Subscription Events

处理订阅事件

Real-time Developer Notifications (RTDN)

Real-time Developer Notifications (RTDN)

Set up Pub/Sub to receive subscription events:
  1. Create Pub/Sub topic in Google Cloud Console
  2. Configure in Play Console:
    • Monetization Setup → Real-time developer notifications
    • Enter topic name
  3. 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以接收订阅事件:
  1. 在Google Cloud Console中创建Pub/Sub主题
  2. 在Play Console中配置
    • 变现设置 → 实时开发者通知
    • 输入主题名称
  3. 订阅事件
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.json
bash
gplay purchases subscriptions defer \
  --package com.example.app \
  --token <SUBSCRIPTION_TOKEN> \
  --json @defer.json

defer.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 1709000000000
Remove 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-34567
bash
gplay orders get \
  --package com.example.app \
  --order-id GPA.1234-5678-9012-34567

Batch 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 access
bash
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
  • ✅ 始终在服务器端验证,绝不信任客户端
  • ✅ 安全存储购买令牌
  • ✅ 在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

常见验证流程

  1. User makes purchase in app
  2. App sends purchase token to your server
  3. Server verifies with Google Play API
  4. Server acknowledges purchase (if valid)
  5. Server grants access/content to user
  6. Server stores purchase token for future checks
  7. Server listens for RTDN events (cancellations, renewals)
  1. 用户在应用内完成购买
  2. 应用将购买令牌发送到你的服务器
  3. 服务器通过Google Play API验证
  4. 服务器确认购买(如果有效)
  5. 服务器向用户授予权限/内容
  6. 服务器存储购买令牌以备后续检查
  7. 服务器监听RTDN事件(取消、续订等)

Error Handling

错误处理

Common errors

常见错误

  • 401 Unauthorized
    - Service account not authorized
  • 404 Not Found
    - Purchase token invalid or expired
  • 410 Gone
    - Purchase was refunded/canceled
  • 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
undefined
bash
undefined

Verify test purchase

Verify test purchase

gplay purchases products get
--package com.example.app
--product-id android.test.purchased
--token <TEST_TOKEN>
undefined
gplay purchases products get
--package com.example.app
--product-id android.test.purchased
--token <TEST_TOKEN>
undefined

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.
跟踪以下指标:
  • 购买验证成功率
  • 确认率
  • 退款率
  • 订阅流失率
  • 付款失败率
使用这些数据优化你的变现策略。