Skip to main content

ADDENDUM

Inventory API vs. Events API

Under the hood, the Android SDK sources purchase data from one of two Xsolla backends: Inventory API or Events API.

The inventory records every purchase. Events are recorded only once Events API is enabled for the project in Publisher Account — and from that point they are recorded regardless of which mode the SDK itself runs in. That asymmetry is what makes switching a live project non-trivial: see Migrating a live project.

Selecting a mode

Two independent settings decide it, both off by default: the project-level toggle in Publisher Account, and an Events config passed to the SDK (Config.Integration.Xsolla.Events, supplied via .withEvents(...)).

They govern different things. The Publisher Account toggle decides whether events are recorded at all. The SDK config decides which backend the client restores from — on its own, without consulting the project's toggle.

Events API in
Publisher Account
Events API in
SDK config
Inventory
populated
Events
populated
SDK restores from
Inventory API
Events API — but nothing is ever recorded, so restoration comes back empty
Inventory API — meanwhile events accumulate unread
Events API

The inventory is populated either way. Events only exist once the project toggle is on — and once it is, they are recorded whether the client reads them or not. Row three is where that matters: the project accumulates a full event history the client has never seen, which is what Migrating a live project has to deal with.

What each mode gives you

Neither mode requires you to run a server of your own. What differs is what a queried purchase carries:

Inventory APIEvents API
Order and invoice IDsNot stored — the SDK synthesizes both locallyStored permanently, and returned in full
Purchase timestampNonegetPurchaseTime() is always 0Real timestamp of the original payment
GranularityAggregated per SKU — quantities, not transactionsOne record per payment
Bundle purchases, including virtual currency packagesNot restorable — only what the bundle grantedReplay as the bundle's content items

Field-by-field behavior of a restored Purchase in each mode is documented in Purchase flow → Restored purchases.

Enabling Events API for a project disables that project's webhooks — the two cannot run at the same time. If your server handles payments, or splits that work with the client, that arrangement ends when the project switches over.

Why inventory-backed purchases can't be matched against anything

An inventory query returns holdings, not transactions. Five separate one-unit purchases of sku_0 come back as a single entry with a quantity of 5:

To match a purchase you'd needInventory API returns
Order IDA value synthesized locally by the SDK
Invoice IDThe same synthesized value
TimestampNothing (0)
One record per paymentNothing — only an aggregate quantity per SKU

By default the SDK does split that aggregate into five separate Purchase objects, one per unit, and withCollapseRestoredMultiUnitPurchases turns that off in favor of a single Purchase carrying the quantity (see Units per restored purchase). Either shape is a presentation choice — splitting adds no identifying information, so it doesn't make matching possible.

Migrating a live project

New projects should start on Events API and will never hit any of this. What follows applies only to live projects with existing purchase history.

Two problems collide on the first launch after the client switches to Events API:

  • Events API replays history. It has been recording purchases for as long as the Publisher Account toggle has been on — including the entire period the client ran inventory-backed and never read them. All of it arrives at once.
  • Inventory API may still owe the player goods. Purchases can sit unconsumed because the payment completed but the app crashed, was updated, or was killed mid-flow. Those still need to be awarded.

The clean fix would be to match the two sets and silently settle — consume without awarding — everything already handled on the inventory side. That isn't possible: the event records are complete, and the inventory records carry nothing to anchor them to (see above).

Approach A — client-side migration build

Drain the inventory before reading a single event:

  1. Ship a build that initializes the SDK in Inventory API mode on launch.
  2. Query outstanding purchases, consume them, and award them.
  3. Write a persistent marker recording that the inventory has been drained and must never be read again.
  4. Re-initialize the SDK with the Events API configuration.
  5. Discard — consume without awarding — every event dated before the drain in step 2.
ProsCons
Runs entirely on the device, needing no coordination on the Xsolla side, and settles what the inventory actually owes before any event is read.Only reaches players who install the migration build. The marker is lost on reinstall or cleared app data, so the drain can run twice. Step 5 cuts on a timestamp rather than an identity match, so purchases close to the cutoff can be double-awarded or dropped.

Approach B — server-side reconciliation

A reconciliation performed on the Xsolla side, settling the overlap without involving the client. Whether it fits your project depends on its history and scale — raise it with your integration manager.

ProsCons
Reaches players who never install a migration build, which Approach A cannot.Has to be arranged per project, so it can't be scheduled from your side. A single pass also covers nothing that happens after it runs.

Planning around the residual

Approach A is the part you control, and it's worth shipping regardless. However the migration runs, expect a residue of players who miss a purchase or receive one twice, and have a support and compensation path ready before the switch.