Google Play
PickLayer accepts Google Play purchase tokens from your Android app and uses them to fund a billing plan. There is no separate payment processor in the loop: your app makes the purchase through the Google Play Billing Library, posts the resulting purchaseToken to PickLayer, and PickLayer activates the user’s plan after calling the Google Play Developer API to confirm the purchase under the service account you configure.
For Stripe-based subscriptions on web, see Stripe. For iOS in-app purchases, see Apple App Store. For the cross-cutting usage and quota model, see the billing overview.
Subscriptions only. The current verifier calls the Google Play Developer API
purchases.subscriptions.getendpoint. One-time products (purchases.products.get) are not yet wired up — purchase tokens for managed products will fail verification with a 404 from Google. Configure your in-app products as auto-renewing subscriptions for now.
Prerequisites
- A Google Play Console account with the app published to at least the internal test track (purchase tokens cannot be generated against an unpublished app).
- One or more subscription products configured under Monetization → Products → Subscriptions in Play Console. Note the product ID (e.g.
picklayer_pro_monthly); this is the value your app passes to PickLayer. - A Google Cloud service account with the Google Play Android Developer API enabled in the same Google Cloud project. Download the service account key as a JSON file (this is the standard
service_accountJSON, withclient_emailandprivate_keyfields). - The service account email granted access in Play Console under Users and permissions → Invite new users, with the View financial data and Manage orders and subscriptions permissions for the relevant app. Account propagation can take up to 24 hours.
- Your app’s package name (e.g.
com.example.myapp) — this is what the verifier uses to scope the API call.
Setup in the PickLayer admin UI
-
Open the Billing page in the PickLayer console.
-
Scroll to External Integrations and click Add integration.
-
Set Provider to Google Play. Give it a Display name (free-text label, e.g.
Android production). -
Paste the service account key JSON into the Credentials JSON / token field, then add the package name to the same JSON object:
{ "type": "service_account", "project_id": "my-play-project", "client_email": "bot@my-play-project.iam.gserviceaccount.com", "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----", "package_name": "com.example.myapp" }- The verifier reads
client_email,private_key, andpackage_name. The camelCase aliasesclientEmail,privateKey, andpackageNameare also accepted, as is the standard service-account JSON downloaded from Google Cloud (which already containsclient_emailandprivate_key). package_nameis read from the credentials JSON unless the request explicitly provides one — see the endpoint reference. We recommend setting it on the integration so the field can’t be tampered with from the client.- Optional:
token_urioverrides the default OAuth2 token endpoint (https://oauth2.googleapis.com/token). You almost never need to set this. - Embed
\nin the PEM string; do not paste raw line breaks inside the JSON value.
- The verifier reads
-
Set a Webhook secret. This is required for the
/external-checkout/googleendpoint. Unlike Apple — which has the JWS signature as a second auth layer — Google purchase tokens carry no signature, so the HMAC is the only request-level auth and is enforced fail-closed. Every request to/external-checkout/googlemust include anX-Google-Signatureheader containingHMAC-SHA256(secret, JSON.stringify(body))in hex; missing or mismatched headers are rejected with401. -
Set Status to Active and click Create. Credentials are stored encrypted at rest and used only to validate billing events.
-
On a Billing Plan, attach the integration by adding a funding mode that references this integration. The plan’s funding mode is what links a Google Play purchase token to a specific plan.
Android-side flow
-
Before kicking off the Play purchase, your app creates a plan funding intent through the standard PickLayer billing API and obtains an
intentId(UUID). Stash this — you will pass it back when you POST the purchase token. -
Optionally pass
intentIdas theobfuscatedAccountIdon theBillingFlowParams. PickLayer does not readobfuscatedAccountIdfrom the purchase payload (the verifier never sees the client-sidePurchaseobject), so this is just a server-side lookup convenience for your own bookkeeping. The authoritativeintentIdis the one your app POSTs to PickLayer in step 5. -
The user taps the purchase button. Your app calls
BillingClient.launchBillingFlow(activity, BillingFlowParams.newBuilder().setProductDetailsParamsList(...).build()). -
The
PurchasesUpdatedListenercallback returns aPurchasecarrying:purchaseToken(string) — opaque token, the value PickLayer verifies.products(list of strings) — the subscription product IDs.orderId(string, optional) — Google’s order identifier.
-
Your app POSTs the purchase to PickLayer:
POST https://<your-picklayer-host>/external-checkout/google Content-Type: application/json X-Google-Signature: <hmac-sha256-hex> { "intentId": "<plan-funding-intent-uuid>", "productId": "picklayer_pro_monthly", "purchaseToken": "<purchase.purchaseToken>" }Compute
X-Google-Signatureserver-side in your backend, not in the Android client — embedding the integration webhook secret in a shippable APK defeats the point. The typical pattern: your Android app POSTs to your own backend, your backend signs and forwards to PickLayer. -
PickLayer:
- Validates the HMAC against the integration’s
webhookSecret. - Looks up the funding intent by
intentIdand resolves the plan and organization from it. - Mints an OAuth2 access token by signing a JWT (
RS256) with the service account’s private key, scoped tohttps://www.googleapis.com/auth/androidpublisher. - Calls
GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{productId}/tokens/{purchaseToken}to fetch the authoritative purchase record. - Marks the funding intent
verified, credits the user’s plan wallet, and flips the user-plan assignment tofunding_status = 'funded'.
- Validates the HMAC against the integration’s
-
Your app must acknowledge the purchase via
BillingClient.acknowledgePurchase(AcknowledgePurchaseParams.newBuilder().setPurchaseToken(...).build(), listener)within 3 days, or Google Play will automatically refund the purchase. PickLayer does not acknowledge the purchase for you — acknowledgement is a client-side responsibility tied to your Play Billing Library integration. -
Your app polls
GET /v1/billing/readiness(or watches your usual entitlement state) to confirm activation. Expectfunded: trueonce verification completes.
Endpoint reference
POST /external-checkout/google
Public endpoint — no Authorization header. The HMAC X-Google-Signature is the auth.
Headers
| Header | Required | Notes |
|---|---|---|
Content-Type: application/json | yes | |
X-Google-Signature | yes | HMAC-SHA256(webhookSecret, JSON.stringify(body)) in hex. Fail-closed: missing header, missing integration secret, or signature mismatch all return 401. |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
intentId | string (UUID) | yes | The PlanFundingIntent ID created earlier. |
productId | string | yes | The Play subscription product ID (e.g. picklayer_pro_monthly). |
purchaseToken | string | yes | The opaque purchaseToken from the Purchase object. |
packageName | string | no | Overrides the package_name from the integration credentials. Optional; we recommend leaving it unset and storing the canonical value on the integration. |
The providerTransactionId recorded on the funding ledger is the purchaseToken.
Successful response (200)
{
"code": 200,
"type": "success",
"message": "Google checkout verified",
"data": {}
}Error responses
| Status | When |
|---|---|
400 | Body missing intentId, productId, or purchaseToken, or the funding intent has no externalCheckoutIntegrationId. |
401 | Missing X-Google-Signature header, integration has no webhookSecret configured, or HMAC mismatch. |
404 | intentId does not match any funding intent, or the integration referenced by the intent no longer exists. |
500 | Google’s OAuth2 token exchange failed, the Android Publisher API returned a non-2xx response (most commonly 404 from Google for an unknown purchase token, package name mismatch, or a one-time product), or an internal error occurred during settlement. |
POST /billing/external-integrations/:integrationId/providers/google_play/verify
Authenticated builder-side variant. Same verification logic, but called from your trusted backend with the standard PickLayer auth instead of HMAC. Use this when your backend already has the purchase token and you’d rather not deal with HMAC signing. Body fields: orgId, planId, intentId, provider: "google_play", productId, purchaseToken, optional packageName, optional amountUsd / amountPicodollars / currency / metadata.
Sandbox vs production
Google Play does not have a separate sandbox endpoint the way Apple does. The Android Publisher API hits the same hostname for test and live purchases, and the parseIntegrationCredentials reader has no environment field. Testing works through Play Console’s license testers instead:
- Add the tester’s Google account under Setup → License testing in Play Console.
- License testers can purchase any in-app subscription without being charged across all tracks (internal, closed, open, production). The
purchaseTokenthey generate is verified against the same Android Publisher API endpoint as a real purchase. - For one-shot scripted runs in CI, use a dedicated tester Google account rather than rotating your own.
If you need to keep test and production purchases on separate billing plans, create two integrations pointing at the same service account but bind them to different funding modes — the discriminator is which integration the funding intent’s externalCheckoutIntegrationId resolves to, not anything inside the Google response.
Troubleshooting
| Symptom | Likely cause |
|---|---|
401 Missing x-google-signature header | Your request didn’t include X-Google-Signature. Compute and send the HMAC (see endpoint reference). The header is required even when the integration looks otherwise correct. |
401 Webhook secret is not configured for this integration | The integration has no webhookSecret set. Open the integration in the admin UI and add one. |
401 Invalid webhook signature | HMAC mismatch. The signature is HMAC-SHA256(webhookSecret, JSON.stringify(body)) in hex — make sure your serialization exactly matches the bytes you sent (no pretty-printing, no key reordering after signing). |
400 Intent ID, product ID, and purchase token are required | One of the three required body fields is empty. |
404 Funding intent not found | The intentId doesn’t match any row. Either the intent expired and was cleaned up, or you used a stale intent — create a fresh intent for each purchase. |
400 Intent is missing Google integration configuration | The funding intent has no externalCheckoutIntegrationId. The funding mode wasn’t bound to an external integration when the intent was created. |
500 Google Play credentials are incomplete | The credentials JSON is missing client_email, private_key, or package_name. |
500 Failed to obtain Google access token: 401 ... | The JWT PickLayer signed for Google’s OAuth2 endpoint was rejected. Usually the service account’s private key is wrong, the service account has been deleted, or the key has been rotated. |
500 Google verification failed: 404 ... | Google has no record of this (packageName, productId, purchaseToken) triple. Common causes: the package name on the integration doesn’t match the app the purchase came from; the product ID was a one-time managed product (only subscriptions are supported); the purchase token was for an internal test on a different app; or the service account doesn’t have View financial data in Play Console and Google is hiding the purchase. |
500 Google verification failed: 401 ... | The OAuth token was minted but Android Publisher rejected it. Usually the service account hasn’t been granted access in Play Console (or was granted access less than 24 hours ago and propagation is incomplete). |
| Purchase verifies but Play auto-refunds it after 3 days | Your Android client never called acknowledgePurchase. PickLayer does not do this for you; it must happen client-side as part of your Play Billing Library integration. |