Validate Google Play Store Purchase
POST https://api.botsi.com/v2/purchases/play-store/validateCall this after a purchase completes in your app. It confirms the transaction with Google Play and records the revenue against the Profile and the Paywall that produced it.
Identify the user with profileId.
Request body#
isExperiment and aiPricingModelId are optional on the v2 API, though the SDK surface requires them.
Send environment explicitly. It has no default.
A missing environment is treated as non-production, which silently drops the revenue event. The call still succeeds, so nothing surfaces until the revenue is missing from your reports.
Send production for live apps and sandbox for testing.
Send one offer object, never both
subscriptionOfferDetails and oneTimePurchaseOfferDetails are mutually exclusive. Which one you send depends on whether you are validating a subscription or a one-time purchase.
Like the Apple endpoint, this one does not accept paywallSessionId. Keep passing paywallId, isExperiment, and aiPricingModelId.
subscriptionOfferDetails#
Each entry in pricingPhases:
oneTimePurchaseOfferDetails#
A missing zero-priced phase turns a trial into revenue
Botsi identifies a free trial by a pricingPhases element with priceAmountMicros set to 0, alongside its usual billingPeriod, recurrenceMode, and billingCycleCount.
Leave that phase out and the event is read as a paid subscription, which inflates your revenue statistics with money nobody paid.
Example requests#
Which offer object you send depends on what the user bought. A subscription with an introductory free trial carries two pricing phases: the zero-priced trial, then the phase that charges once it ends.
curl -X POST "https://api.botsi.com/v2/purchases/play-store/validate" \
-H "Authorization: $BOTSI_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"token": "algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5",
"placementId": "fresh_placement",
"productId": "monthly",
"paywallId": 569,
"isExperiment": true,
"aiPricingModelId": 32,
"environment": "production",
"profileId": "55bbd5f3-6de8-4d3b-8a55-006e6af6e3dd",
"subscriptionOfferDetails": {
"basePlanId": "1month-sub",
"pricingPhases": [
{
"priceAmountMicros": 359990000,
"currencyCode": "UAH",
"billingPeriod": "P1M",
"recurrenceMode": 1,
"billingCycleCount": 0
}
]
}
}'curl -X POST "https://api.botsi.com/v2/purchases/play-store/validate" \
-H "Authorization: $BOTSI_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"token": "algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5",
"placementId": "fresh_placement",
"productId": "monthly",
"paywallId": 569,
"isExperiment": true,
"aiPricingModelId": 32,
"environment": "production",
"profileId": "55bbd5f3-6de8-4d3b-8a55-006e6af6e3dd",
"subscriptionOfferDetails": {
"basePlanId": "1month-sub",
"offerId": "freetrial-7d",
"pricingPhases": [
{
"priceAmountMicros": 0,
"currencyCode": "UAH",
"billingPeriod": "P1W",
"recurrenceMode": 2,
"billingCycleCount": 1
},
{
"priceAmountMicros": 359990000,
"currencyCode": "UAH",
"billingPeriod": "P1M",
"recurrenceMode": 1,
"billingCycleCount": 0
}
]
}
}'curl -X POST "https://api.botsi.com/v2/purchases/play-store/validate" \
-H "Authorization: $BOTSI_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"token": "algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5",
"placementId": "fresh_placement",
"productId": "lifetime_unlock",
"paywallId": 569,
"isExperiment": true,
"aiPricingModelId": 32,
"environment": "production",
"profileId": "55bbd5f3-6de8-4d3b-8a55-006e6af6e3dd",
"oneTimePurchaseOfferDetails": {
"priceAmountMicros": 9990000,
"currencyCode": "USD"
}
}'Finding the token#
The purchase token comes from getPurchaseToken() in the Google Play Billing Library. Google's own documentation on verifying purchases before granting entitlements covers where it appears in the billing flow.
Response#
A successful validation returns the updated Profile, including its Access Levels, subscriptions, and total revenue. Read state and accessLevels to decide what the user now has access to.
{
"ok": true,
"data": {
"profileId": "55bbd5f3-6de8-4d3b-8a55-006e6af6e3dd",
"appUserId": "user-123",
"state": "subscribed",
"totalRevenueUsd": 8.72,
"accessLevels": {},
"subscriptions": {},
"nonSubscriptions": {}
}
}accessLevels, subscriptions, and nonSubscriptions are keyed objects, empty when the Profile has no entries of that kind. The shape matches Validate Apple Store Purchase, so a single handler can process either store's response.