Skip to Content
BillingGoogle Play

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.get endpoint. 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_account JSON, with client_email and private_key fields).
  • 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

  1. Open the Billing page in the PickLayer console.

  2. Scroll to External Integrations and click Add integration.

  3. Set Provider to Google Play. Give it a Display name (free-text label, e.g. Android production).

  4. 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, and package_name. The camelCase aliases clientEmail, privateKey, and packageName are also accepted, as is the standard service-account JSON downloaded from Google Cloud (which already contains client_email and private_key).
    • package_name is 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_uri overrides the default OAuth2 token endpoint (https://oauth2.googleapis.com/token). You almost never need to set this.
    • Embed \n in the PEM string; do not paste raw line breaks inside the JSON value.
  5. Set a Webhook secret. This is required for the /external-checkout/google endpoint. 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/google must include an X-Google-Signature header containing HMAC-SHA256(secret, JSON.stringify(body)) in hex; missing or mismatched headers are rejected with 401.

  6. Set Status to Active and click Create. Credentials are stored encrypted at rest and used only to validate billing events.

  7. 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

  1. 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.

  2. Optionally pass intentId as the obfuscatedAccountId on the BillingFlowParams. PickLayer does not read obfuscatedAccountId from the purchase payload (the verifier never sees the client-side Purchase object), so this is just a server-side lookup convenience for your own bookkeeping. The authoritative intentId is the one your app POSTs to PickLayer in step 5.

  3. The user taps the purchase button. Your app calls BillingClient.launchBillingFlow(activity, BillingFlowParams.newBuilder().setProductDetailsParamsList(...).build()).

  4. The PurchasesUpdatedListener callback returns a Purchase carrying:

    • purchaseToken (string) — opaque token, the value PickLayer verifies.
    • products (list of strings) — the subscription product IDs.
    • orderId (string, optional) — Google’s order identifier.
  5. 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-Signature server-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.

  6. PickLayer:

    • Validates the HMAC against the integration’s webhookSecret.
    • Looks up the funding intent by intentId and 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 to https://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 to funding_status = 'funded'.
  7. 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.

  8. Your app polls GET /v1/billing/readiness (or watches your usual entitlement state) to confirm activation. Expect funded: true once verification completes.

Endpoint reference

POST /external-checkout/google

Public endpoint — no Authorization header. The HMAC X-Google-Signature is the auth.

Headers

HeaderRequiredNotes
Content-Type: application/jsonyes
X-Google-SignatureyesHMAC-SHA256(webhookSecret, JSON.stringify(body)) in hex. Fail-closed: missing header, missing integration secret, or signature mismatch all return 401.

Request body

FieldTypeRequiredNotes
intentIdstring (UUID)yesThe PlanFundingIntent ID created earlier.
productIdstringyesThe Play subscription product ID (e.g. picklayer_pro_monthly).
purchaseTokenstringyesThe opaque purchaseToken from the Purchase object.
packageNamestringnoOverrides 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

StatusWhen
400Body missing intentId, productId, or purchaseToken, or the funding intent has no externalCheckoutIntegrationId.
401Missing X-Google-Signature header, integration has no webhookSecret configured, or HMAC mismatch.
404intentId does not match any funding intent, or the integration referenced by the intent no longer exists.
500Google’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 purchaseToken they 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

SymptomLikely cause
401 Missing x-google-signature headerYour 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 integrationThe integration has no webhookSecret set. Open the integration in the admin UI and add one.
401 Invalid webhook signatureHMAC 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 requiredOne of the three required body fields is empty.
404 Funding intent not foundThe 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 configurationThe 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 incompleteThe 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 daysYour 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.
Last updated on