PLATFORM COMPLIANCE
Reporting External Transactions to Google Play
When you sell through Xsolla on storefronts where Google permits external transactions, Google still requires you to report those transactions. This page explains what Google expects, and how to build that report from the data Xsolla already sends you.
Who reports what
Reporting is part of your integration, not something Xsolla does on your behalf.
| Handled by | |
|---|---|
| Processing the payment, tax calculation, refunds | Xsolla |
| Notifying your backend of each order | Xsolla (webhooks) |
| Obtaining Google's external transaction token in your app | You |
| Filing the report with Google | You, from your own server |
Google Play's reporting API is authenticated with your own service-account credentials, tied to your app's listing in Play Console, so no payment provider can file on your behalf. What Xsolla provides is the transaction data the report is built from.
When this applies
Google's programs differ by region, and the list has grown. Check which ones apply to where you ship before building anything.
| Program | Applies in | Reporting required |
|---|---|---|
| External content links & alternative billing | United States | Not yet — starting October 1, 20261 |
| Billing choice | United Kingdom, European Economic Area | Yes |
| External offers | European Economic Area | Yes |
| External payments | Japan | Yes |
| User Choice Billing | 35+ countries, including the EEA, UK, Australia, Brazil, Indonesia, Japan, and South Africa | Yes |
Rates, deadlines and eligibility change often, and they differ per program and region. This page deliberately avoids repeating Google's numbers. Always check Google's own pages for current terms: User Choice Billing ↗, Billing choice (UK/EEA) ↗, External offers (EEA) ↗, External content links & alternative billing (US) ↗, and External payments (Japan) ↗.
Each program is enrolled separately in Play Console and comes with its own updated Developer Distribution Agreement terms. Sort that out before you write code, since the Billing Library APIs below report the program as unavailable without it.
To enable external transactions only for the storefronts that permit them, see How to detect Google Play storefront (Android)?.
The flow end to end
- Your app checks program availability and asks the Play Billing Library for an external transaction token.
- Your app sends that token to your backend, which stores it against the customer.
- The player buys through Xsolla as normal — in-app for User Choice Billing and alternative billing, or after a link-out for external offers, external content links, external payments, and billing choice.
- Xsolla sends your backend an
order_paidwebhook with the financial detail of that order. - Your backend pairs the token with the order data and reports the transaction to Google — most programs expect this within a fixed window measured in hours, not days, far tighter than a monthly cadence, so build the job to run per webhook rather than batching. See Google's reporting guide ↗ for the exact deadline per program.
- Refunds and chargebacks arrive as
order_canceledand are reported to Google as refunds against the original transaction.
Getting the token
Tokens come from the Play Billing Library, not from the Xsolla SDK. Which surface you call depends on the program.
enableUserChoiceBilling is the surface for User Choice Billing. External offers, external content links, external payments, and billing choice use a newer, unified surface instead — isBillingProgramAvailableAsync, createBillingProgramReportingDetailsAsync, and launchExternalLink. Alternative-billing-only integrations (no choice screen shown) use a third method, createAlternativeBillingOnlyReportingDetailsAsync. Check Google's alternative billing overview ↗ for which surface and Billing Library version your program needs.
Even within that unified surface the four programs aren't interchangeable. The link-out example below is written for external offers and works unchanged for external content links. External payments swaps LaunchExternalLinkParams for DeveloperBillingOptionParams, and billing choice additionally needs setDeveloperBillingType plus a check of getBillingChoiceAvailabilityDetails() on the availability response before it may link out.
- User Choice Billing
- External offers / content links
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.PurchasesUpdatedListener;
// Application context, and Google Play Billing's own purchase-update listener.
final Context context = // ...
final PurchasesUpdatedListener purchasesUpdatedListener = // ...
final BillingClient googlePlayBillingClient = BillingClient.newBuilder(context)
.setListener(purchasesUpdatedListener)
.enableUserChoiceBilling(userChoiceDetails -> {
// Called when the player picks your alternative over Google Play Billing.
final String token = userChoiceDetails.getExternalTransactionToken();
// Send the token to your own backend and store it before continuing.
sendTokenToYourServer(token);
// ...then start your Xsolla purchase, attaching the token as shown below.
})
.build();
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClient.BillingProgram;
import com.android.billingclient.api.BillingClient.BillingResponseCode;
import com.android.billingclient.api.BillingProgramReportingDetailsParams;
import com.android.billingclient.api.LaunchExternalLinkParams;
// Application context, the activity to launch the external link from, and the
// Xsolla Pay Station or Web Shop URL to send the player to.
final Context context = // ...
final Activity activity = // ...
final String checkoutUrl = // ...
// Use EXTERNAL_CONTENT_LINK instead for external content links.
final int billingProgram = BillingProgram.EXTERNAL_OFFER;
final BillingClient googlePlayBillingClient = BillingClient.newBuilder(context)
.enableBillingProgram(billingProgram)
.build();
googlePlayBillingClient.isBillingProgramAvailableAsync(billingProgram, (billingResult, availability) -> {
if (billingResult.getResponseCode() != BillingResponseCode.OK) return;
final BillingProgramReportingDetailsParams params = BillingProgramReportingDetailsParams.newBuilder()
.setBillingProgram(billingProgram)
.build();
googlePlayBillingClient.createBillingProgramReportingDetailsAsync(params, (reportingResult, details) -> {
if (reportingResult.getResponseCode() != BillingResponseCode.OK) return;
final String token = details.getExternalTransactionToken();
// Send the token to your own backend and store it before continuing.
sendTokenToYourServer(token);
final LaunchExternalLinkParams linkParams = LaunchExternalLinkParams.newBuilder()
.setBillingProgram(billingProgram)
.setLinkUri(Uri.parse(checkoutUrl))
// A checkout page is a digital content offer. LINK_TO_APP_DOWNLOAD is
// billed per install instead of per transaction, and its reports require
// installedAppPackage and installedAppCategory.
.setLinkType(LaunchExternalLinkParams.LinkType.LINK_TO_DIGITAL_CONTENT_OFFER)
.setLaunchMode(LaunchExternalLinkParams.LaunchMode.LAUNCH_IN_EXTERNAL_BROWSER_OR_APP)
.build();
googlePlayBillingClient.launchExternalLink(activity, linkParams, launchResult -> {
// The player leaves the app here and finishes the purchase on your own
// site (Pay Station or Web Shop). See "Correlating the token" below.
});
});
});
The token returned by either surface is an opaque string — externalTransactionToken — that Google correlates against the report you file later. Which token types apply, when to request them, and how long they stay valid all vary by region and program. See Google's reporting guide ↗ for the current rules.
Request a new token immediately before each purchase or link-out — don't cache and reuse one across transactions. Only the first payment of a recurring subscription carries a token; renewals are reported against initialExternalTransactionId instead. See Google's reporting guide ↗ for the exact rules per program.
Correlating the token with your Xsolla order
How you tie the token to the order depends on which path the purchase took.
Purchases that run in-app — User Choice Billing and alternative billing — go through the Xsolla SDK, so pass the token straight into BillingFlowParams.Builder.setExternalTransactionToken when you launch the flow, right alongside the product you're selling:
import com.xsolla.android.mobile.BillingFlowParams;
import com.xsolla.android.mobile.ProductDetails;
// token is the one your app just got from the Play Billing Library above, and
// mBillingClient the com.xsolla.android.mobile.BillingClient you initialized earlier.
final ProductDetails productDetails = // ...
final String token = // ...
final Activity activity = // ...
mBillingClient.launchBillingFlow(activity, BillingFlowParams
.newBuilder()
.setProductDetailsParamsList(Arrays.asList(
BillingFlowParams.ProductDetailsParams
.newBuilder()
.setProductDetails(productDetails)
.build()
))
.setExternalTransactionToken(token)
.build()
);
See Launch a billing flow for the rest of BillingFlowParams.Builder's settings.
Purchases that link out — external offers, external content links, external payments, and billing choice — leave the app before the player pays, so there's no Xsolla SDK call to attach the token to. Correlate it yourself instead: key the stored token to the buyer's user ID, and pair it with the order when the webhook arrives. Google also suggests carrying the token in the link URI as a query parameter, which hands your checkout page the same correlation key — but a token arriving that way still doesn't reach the Xsolla order, so your backend stays the place where the two are joined.
Once the token is attached through setExternalTransactionToken, Xsolla builds a ucb_report object for the order — a ready-built Google externalTransactions request body (originalPreTaxAmount, originalTaxAmount, transactionTime, userTaxAddress, and the token itself under oneTimeTransaction.externalTransactionToken) — and attaches it to the order webhook. On the combined order_paid / order_canceled model, it nests under billing.ucb_report; on the legacy payment / refund webhooks, it's at the top level. Either way, report from ucb_report directly instead of reconstructing the fields yourself.
Building the report from webhook data
Most of what a Google transaction report needs is already assembled for you in ucb_report — billing.ucb_report on order_paid / order_canceled, or top-level ucb_report on the legacy payment / refund model (see above). If you need to build it yourself, the same data lives in the order_paid payload under billing.payment_details. Here's where each field comes from either way.
| Google transaction field | Where it comes from |
|---|---|
externalTransactionId | You generate it. Persist it, refunds and renewals reference it |
externalTransactionToken | The token from the Play Billing Library, forwarded through your Xsolla order (see setExternalTransactionToken above) |
transactionTime | billing.transaction.payment_date, converted to RFC 3339 |
originalPreTaxAmount | billing.payment_details.payment.amount minus originalTaxAmount below, converted to micros |
originalTaxAmount | billing.payment_details.vat.amount outside the US and Canada, sales_tax.amount in the US and Canada. See the note below |
userTaxAddress.regionCode | user.country — already the two-letter code Google expects, no conversion needed |
Transaction type (oneTimeTransaction / recurringTransaction / otherRecurringProduct) | Presence and type of billing.purchase.subscription |
externalSubscription.subscriptionType | RECURRING or PREPAID, from billing.purchase.subscription |
billing.payment_details.payout is your share after Xsolla's fees. Google's report expects the amount the player was charged, so originalPreTaxAmount and originalTaxAmount must derive from billing.payment_details.payment. Using payout under-reports your transactions.
vat isn't EU-only despite its name — it's the general non-US/Canada consumption-tax field, so it also carries JCT in Japan and GST elsewhere covered by these programs.
Google requires userTaxAddress.administrativeArea in addition to regionCode for transactions in India specifically — it's optional everywhere else, and the field only accepts Google's fixed list of Indian states and union territories. user.country doesn't carry that level of detail, so if you're assembling the report yourself from the fields above, source the player's state from your own data for Indian transactions. ucb_report (see above) already includes it correctly, since Xsolla holds the player's state internally for tax purposes even though user.country doesn't expose it.
Amounts and rounding
Webhook amounts are decimal values, while Google expects integer micros — millionths of the currency unit, not the minor units (cents) Apple's report uses. Convert with a decimal type rather than a float, and derive originalPreTaxAmount by subtracting the tax you reported rather than recomputing it from a percentage, so the two figures reconcile.
Google requires $0 transactions from free trials to be reported the same as paid ones. Don't filter these out of your reporting job because billing.payment_details.payment.amount is zero.
Refunds
order_canceled covers both refunds and chargebacks. Report both with refundexternaltransaction, referencing the original externalTransactionId — for subscriptions, the externalTransactionId of the specific recurrence being refunded. Deduplicate on order.id, since webhooks are retried and a repeated delivery must not produce a second refund report.
Unlike Apple's report, you don't need to track a running net yourself: Google recomputes currentPreTaxAmount and currentTaxAmount automatically once a refund is reported against a transaction.
What Xsolla can't tell you
Some of Google's requirements sit outside anything Xsolla observes. Your backend has to cover them:
- Tokens that produced no purchase. Google expects these reported too. There's no order and therefore no webhook, so your token store is the only record.
- App installs completed through a link-out. The US external content links program and the EEA external offers program also require reporting successful installs, not just purchases, using the
externalOfferDetailsblock (installedAppPackage,installedAppCategory). Installs aren't purchases, so Xsolla has no visibility into them at all — your own attribution tracking has to supply this. - Transaction identity.
externalTransactionIdvalues are yours to generate and persist. Without them, refunds and renewals can't reference the transaction they belong to.
Testing
License testers reported through the Play Billing Library are marked with testPurchase on Google's side and aren't invoiced. Xsolla test orders are marked separately — order.mode: "sandbox" on order_paid / order_canceled, or transaction.dry_run: 1 on the legacy payment / refund webhooks — keep these straight, since neither implies the other.
Further reading
- Google Play Developer API — externaltransactions ↗ — the reporting endpoints and transaction schema
- Reporting transactions from outside Google Play's billing system ↗
- Alternative billing overview ↗
- Xsolla webhooks — payload reference and signature verification
- How to detect Google Play storefront (Android)?
Footnotes
-
The one program date this page commits to, since it's the date Google has published for when it starts collecting fees and requiring reports on U.S. external transactions. Reconfirm on Google's External Content Links Program page ↗ before building a deadline around it — it's exactly the kind of detail this page otherwise avoids stating outright. ↩