Skip to content

Search the documentation by title, section, or page text.

Validate Google Play Store Purchase

POST https://api.botsi.com/v2/purchases/play-store/validate

Call 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. To give a returning user back the access their subscriptions carry, use Restore Google Play Subscriptions.

Request body#

ParameterTypeRequiredDescription
profileIdstringRequiredBotsi profile ID from Create a Profile
productIdstringRequiredMust match the Play Store Product ID configured in Botsi. Use sourceProductId from the Products response
tokenstringRequiredThe token uniquely identifying this purchase for an item and user pair
paywallSessionIdstringOptionalThe paywallSessionId from the Fetch Paywall response. When sent, the purchase is attributed from the token. See below
placementIdstringOptionalThe Placement used to fetch the Paywall
paywallIdnumberOptionalThe internal Paywall ID, from data.id on the Fetch Paywall response
abTestIdnumberOptionalThe A/B test that produced the Paywall
isExperimentbooleanOptionalThe value Fetch Paywall returned
aiPricingModelIdnumberOptionalThe model ID Fetch Paywall returned
environmentstringOptionalproduction for live apps, sandbox for testing. Read the warning below
isSubscriptionbooleanOptionalWhether the purchase is a subscription
subscriptionOfferDetailsobjectOne of the twoFor a subscription. From ProductDetails.getSubscriptionOfferDetails(), usually the first item
oneTimePurchaseOfferDetailsobjectOne of the twoFor a one-time purchase

isExperiment and aiPricingModelId are optional on the v2 API, though the SDK surface requires them. Omitting them still validates the purchase, but read Attributing the purchase to a Paywall before you do.

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.

subscriptionOfferDetails#

ParameterTypeDescription
basePlanIdstringThe base plan ID for the subscription Product
offerIdstringThe offer ID, if the Product has an offer configured in the store
pricingPhasesarrayThe pricing phases for the subscription

Each entry in pricingPhases:

ParameterTypeDescription
priceAmountMicrosnumberPrice in micro-units. 1,000,000 micro-units is one unit of currency
currencyCodestringISO 4217 currency code
billingPeriodstringThe billing period, in ISO 8601 format
recurrenceModenumberThe recurrence mode of the phase
billingCycleCountnumberHow many cycles the period applies for

oneTimePurchaseOfferDetails#

ParameterTypeDescription
priceAmountMicrosnumberPrice in micros, for example 9990000 for $9.99
currencyCodestringISO 4217 code, for example USD

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
        }
      ]
    }
  }'

Attributing the purchase to a Paywall#

Send the paywallSessionId from the Fetch Paywall response. Botsi reads the Paywall, Placement, A/B test, and AI Pricing Model from the token, and credits the revenue to the experiment that produced it. This is the preferred way to attribute a purchase.

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",
    "productId": "monthly",
    "paywallSessionId": "v1.eyJhIjo0MDIxLCJ3Ijo5MDUsInAiOiJvbmJvYXJkaW5nIiwiaSI6MTc4NTMxMjAwMH0.QmzR1w",
    "environment": "production",
    "profileId": "55bbd5f3-6de8-4d3b-8a55-006e6af6e3dd",
    "subscriptionOfferDetails": {
      "basePlanId": "1month-sub",
      "pricingPhases": [
        {
          "priceAmountMicros": 359990000,
          "currencyCode": "UAH",
          "billingPeriod": "P1M",
          "recurrenceMode": 1,
          "billingCycleCount": 0
        }
      ]
    }
  }'
  • The token wins. When paywallSessionId is sent, paywallId, placementId, abTestId, aiPricingModelId, and isExperiment all come from the token. Values you send for those fields are ignored, not merged.
  • Fields the token doesn't carry are cleared. They aren't filled in from the request.
  • There's no age limit. You can validate a purchase long after the Paywall was fetched, unlike a paywall_shown event, which expires after 24 hours.
  • profileId still identifies the buyer. The token attributes the sale, but it doesn't choose the Profile.

An invalid token fails the whole request

A paywallSessionId that isn't valid returns 400. Botsi never falls back to the explicit attribution fields, so the purchase isn't validated until you send a valid token or drop it.

Without a token, the explicit fields work exactly as before. Pass paywallId, isExperiment, and aiPricingModelId as Fetch Paywall returned them, as the examples above do. Omit both the token and the fields and the purchase still validates, but it detaches from the experiment that produced it, so the model gets no credit for the revenue it earned.

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. Every field is described on Get a Profile. This response is the same object, without custom. The shape matches Validate Apple Store Purchase, so a single handler can process either store's response.