Verify Google Play purchases server-side without the Java SDK
Server-side purchase verification on Google Play is one of those tasks where the official docs steer you toward a specific stack — pull in the Google API Java client, set up OAuth, wire up the AndroidPublisher service, hand-roll the request. If your backend is Node, Go, Python, Rust, or basically anything else, you’re either translating that boilerplate or reaching for a community wrapper.
There’s a shorter path: gplay validates receipts from a single command line. It’s the same Google Play Android Publisher API call, just without the ceremony.
The full flow, condensed
Section titled “The full flow, condensed”Say your Android app sent your server a purchaseToken and a productId after a one-time purchase or subscription. You need to confirm with Google that it’s real, not consumed, and not refunded.
gplay purchases subscriptionsv2 get \ --package com.example.app \ --token "$PURCHASE_TOKEN"Output (minified JSON, one line — shown here formatted for reading):
{ "kind": "androidpublisher#subscriptionPurchaseV2", "subscriptionState": "SUBSCRIPTION_STATE_ACTIVE", "regionCode": "US", "lineItems": [{ "productId": "monthly_pro", "expiryTime": "2026-08-05T12:34:56Z", "autoRenewingPlan": { "autoRenewEnabled": true } }], "acknowledgementState": "ACKNOWLEDGEMENT_STATE_ACKNOWLEDGED", "linkedPurchaseToken": ""}Your backend parses that JSON and decides whether to grant entitlements.
Node.js example
Section titled “Node.js example”import { execFile } from 'node:child_process';import { promisify } from 'node:util';
const run = promisify(execFile);
export async function verifySubscription(packageName, token) { const { stdout } = await run('gplay', [ 'purchases', 'subscriptionsv2', 'get', '--package', packageName, '--token', token, ]); const purchase = JSON.parse(stdout); return { active: purchase.subscriptionState === 'SUBSCRIPTION_STATE_ACTIVE', productId: purchase.lineItems?.[0]?.productId, expiresAt: purchase.lineItems?.[0]?.expiryTime, };}No googleapis dependency. No OAuth token management. gplay handles the service-account auth internally from the file at GPLAY_SERVICE_ACCOUNT.
Go example
Section titled “Go example”package purchases
import ( "context" "encoding/json" "os/exec")
type Verified struct { Active bool `json:"active"` ProductID string `json:"productId"` ExpiresAt string `json:"expiresAt"`}
type v2 struct { State string `json:"subscriptionState"` LineItems []struct { ProductID string `json:"productId"` ExpiryTime string `json:"expiryTime"` } `json:"lineItems"`}
func Verify(ctx context.Context, pkg, token string) (Verified, error) { out, err := exec.CommandContext(ctx, "gplay", "purchases", "subscriptionsv2", "get", "--package", pkg, "--token", token, ).Output() if err != nil { return Verified{}, err } var p v2 if err := json.Unmarshal(out, &p); err != nil { return Verified{}, err } v := Verified{Active: p.State == "SUBSCRIPTION_STATE_ACTIVE"} if len(p.LineItems) > 0 { v.ProductID = p.LineItems[0].ProductID v.ExpiresAt = p.LineItems[0].ExpiryTime } return v, nil}Python example
Section titled “Python example”import jsonimport subprocess
def verify_subscription(package: str, token: str) -> dict: out = subprocess.check_output([ "gplay", "purchases", "subscriptionsv2", "get", "--package", package, "--token", token, ]) purchase = json.loads(out) return { "active": purchase["subscriptionState"] == "SUBSCRIPTION_STATE_ACTIVE", "product_id": purchase["lineItems"][0]["productId"] if purchase.get("lineItems") else None, "expires_at": purchase["lineItems"][0].get("expiryTime") if purchase.get("lineItems") else None, }Same command, three languages, same shape.
One-time products
Section titled “One-time products”For consumables and non-consumables:
gplay purchases products get \ --package com.example.app \ --product-id premium_upgrade \ --token "$PURCHASE_TOKEN"You get back purchaseState (0 = purchased, 1 = canceled, 2 = pending), consumptionState, and orderId. Same JSON shape, same backend integration.
Acknowledging purchases
Section titled “Acknowledging purchases”Google requires purchases to be acknowledged within 3 days or they’re auto-refunded. gplay handles both flavors:
# Subscriptionsgplay purchases subscriptions acknowledge \ --package com.example.app \ --subscription-id monthly_pro \ --token "$PURCHASE_TOKEN"
# Consumable productsgplay purchases products consume \ --package com.example.app \ --product-id gems_pack_100 \ --token "$PURCHASE_TOKEN"Both are idempotent — safe to retry.
Voided purchases (refunds, chargebacks)
Section titled “Voided purchases (refunds, chargebacks)”To sweep for refunds you missed:
gplay purchases voided list \ --package com.example.app \ --start-time 2026-07-01T00:00:00Z \ --paginate--paginate fetches every page automatically. Run it as an hourly cron and revoke entitlements when a purchaseToken shows up.
Deployment
Section titled “Deployment”In your backend service or container, put the service-account JSON at a known path and set:
export GPLAY_SERVICE_ACCOUNT=/secrets/play-sa.jsonexport GPLAY_PACKAGE=com.example.app # optional defaultexport GPLAY_NO_UPDATE=1 # disable the update check in prodexport GPLAY_TIMEOUT=30s # tighter timeout for API pathsThat’s the whole setup. No OAuth flow, no refresh tokens, no client library.
Why this ends up simpler than the SDK path
Section titled “Why this ends up simpler than the SDK path”- Language-agnostic. Your backend team keeps its stack; the CLI is the shared interface.
- Static binary. 12 MB, no runtime dependency, works in
alpine,distroless, Lambda custom runtime, wherever. - Deterministic JSON. The output shape is stable — no library version drift.
--dry-runfor tests. Sanity-check request payloads in staging without hitting Google.- Same tool for the rest of Play Console. The same CLI that verifies purchases also uploads builds, checks vitals, and downloads reports. One dependency, six APIs.
Get started
Section titled “Get started”brew install tamtom/tap/gplaygplay setup --autogplay purchases subscriptionsv2 get --package com.example.app --token TEST_TOKENFull purchase reference at /reference/purchases/. If you’re validating high volume, install the purchase-verification skill and your AI agent will scaffold the backend integration for you.