Skip to main content

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, refundsXsolla
Notifying your backend of each orderXsolla (webhooks)
Obtaining Google's external transaction token in your appYou
Filing the report with GoogleYou, 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.

ProgramApplies inReporting required
External content links & alternative billingUnited StatesNot yet — starting October 1, 20261
Billing choiceUnited Kingdom, European Economic AreaYes
External offersEuropean Economic AreaYes
External paymentsJapanYes
User Choice Billing35+ countries, including the EEA, UK, Australia, Brazil, Indonesia, Japan, and South AfricaYes
note

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

important

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

  1. Your app checks program availability and asks the Play Billing Library for an external transaction token.
  2. Your app sends that token to your backend, which stores it against the customer.
  3. 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.
  4. Xsolla sends your backend an order_paid webhook with the financial detail of that order.
  5. 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.
  6. Refunds and chargebacks arrive as order_canceled and 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.

The API differs per 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.

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();

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.

Generate a fresh token per transaction

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_reportbilling.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 fieldWhere it comes from
externalTransactionIdYou generate it. Persist it, refunds and renewals reference it
externalTransactionTokenThe token from the Play Billing Library, forwarded through your Xsolla order (see setExternalTransactionToken above)
transactionTimebilling.transaction.payment_date, converted to RFC 3339
originalPreTaxAmountbilling.payment_details.payment.amount minus originalTaxAmount below, converted to micros
originalTaxAmountbilling.payment_details.vat.amount outside the US and Canada, sales_tax.amount in the US and Canada. See the note below
userTaxAddress.regionCodeuser.country — already the two-letter code Google expects, no conversion needed
Transaction type (oneTimeTransaction / recurringTransaction / otherRecurringProduct)Presence and type of billing.purchase.subscription
externalSubscription.subscriptionTypeRECURRING or PREPAID, from billing.purchase.subscription
Report what the player paid, not what you receive

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.

India needs a state code

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.

Free trials count

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 externalOfferDetails block (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. externalTransactionId values 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

Footnotes

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