Skip to main content
Services around the game

Not translated yet — showing English.

Catalog & Commerce

Items, prices, wallets, storefronts, purchases, entitlements. Real store integrations where the platforms allow them (Stripe, App Store, Google Play, Steam, Xbox); scheduled, audience-targeted storefronts; and a purchase flow whose every step is hookable.

When to use it

  • You sell things, for real money through Stripe, App Store, Google Play, Steam or Xbox, or for wallet currency.
  • Storefronts must resolve per player: schedule, audience and price computed server-side, never eligibility math in the client.
  • Pricing rules belong in one testable hook: discounts, repricing and vetoes run before any charge.
  • Receipts must be replay-proof, and a refund must revoke the entitlement through the same events the grant used.
  • Skip it when items are never sold. Rewards still land through commerce's one Grant with origin reward (Leaderboards cycle chests arrive that way), so even a shopless game keeps a single auditable grant ledger.

Who does what

ActorOn this page
playerbrowses storefronts, purchases, manages wallet, redeems codes
sellerconfigures catalog, prices and storefront schedules
backend-servicevalidates receipts; reprices or grants via purchase hooks

At a glance

Get the main storefront, already resolved for this player, and purchase from the wallet
// client — the storefront arrives already resolved for this player
var front = await playserv.Commerce.Storefront("main");
var order = await playserv.Commerce.Purchase(front.Items.First(), pay: Pay.Wallet("gems"));
// client — the storefront arrives already resolved for this player
const front = await playserv.commerce.storefront('main');
const order = await playserv.commerce.purchase(front.items[0], { pay: Pay.wallet('gems') });
# client — the storefront arrives already resolved for this player
front = await playserv.commerce.storefront("main")
order = await playserv.commerce.purchase(front.items[0], pay=Pay.wallet("gems"))
Coming soon — Go

Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.

// client — the storefront arrives already resolved for this player
Client->Commerce->Storefronts->Select().Then(
    TPSOnResult<TPSPage<FPSStorefront>>::CreateWeakLambda(this, [this](const TPSResult<TPSPage<FPSStorefront>>& Result)
    {
        if (!Result.HasValue()) { return; }
        const FPSStorefront& Front = Result.Value().Rows[0];
        Client->Commerce->Orders->Create(FPSIdempotencyKey(CartId), Front.Items[0], FPSPay::Wallet(TEXT("gems")));
    }));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
Unity C# is the same C# API here — runs as-is in Unity against the generated types
// client — the storefront arrives already resolved for this player
var front = await playserv.Commerce.Storefront("main");
var order = await playserv.Commerce.Purchase(front.Items.First(), pay: Pay.Wallet("gems"));
Two purchase hooks: before reprices the first buy, after grants the item
[Before(Commerce.Purchase)]                    // veto or reprice
public static Verdict FirstBuyDiscount(PurchaseIntent p) =>
    p.Player.Purchases == 0 ? Hook.Continue(p.WithPrice(p.Price * 0.5m)) : Hook.Continue(p);

[After(Commerce.Purchase)]                     // grant — side effects only
public static Task Grant(Purchase done) =>
    done.Player.Inventory.Grant(done.Item, done.Count);
// veto or reprice
export const firstBuyDiscount = before(Commerce.purchase, (p: PurchaseIntent) =>
  p.player.purchases === 0 ? Hook.continue(p.withPrice(p.price * 0.5)) : Hook.continue(p));

// grant — side effects only
export const grant = after(Commerce.purchase, (done: Purchase) =>
  done.player.inventory.grant(done.item, done.count));
@before(commerce.purchase)                     # veto or reprice
def first_buy_discount(p: PurchaseIntent) -> Verdict:
    return Hook.continue_(p.with_price(p.price * 0.5)) if p.player.purchases == 0 else Hook.continue_(p)

@after(commerce.purchase)                      # grant — side effects only
async def grant(done: Purchase):
    await done.player.inventory.grant(done.item, done.count)
Coming soon — Go

Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.

Runs off the engine

A hook is a cloud function: it executes on the platform, not in the engine. Write it in C#, TypeScript or Python — Unreal subscribes to the purchased / entitlement-changed events. Engine-authored functions are planned: Blueprint graphs, and C++ (translated to a platform-validated script target). Both are analyzed and pushed like any declaration; execution stays on the platform.

Runs off the engine

A hook is a cloud function: it executes on the platform, not in the engine. Write it in C#, TypeScript or Python — Unity subscribes to the purchased / entitlement-changed events.

The model

What a catalog item declares.

DeclaresWhat it is
keyit is authored content, addressed by a key so a rename in code is a rename
kindconsumable — it is spent; or durable — owned once
pricesa price is a monetary quantity: an integer in minor units plus a currency code, never a float. An item may carry several — game currency and real currency both
external identifier per providerone slot per provider, declared, because a store knows the item by its own id
what it points atoptionally an entity of any declared kind, and buying the item then grants ownership of that entity

What composition is allowed to be.

What it is
what is purchasablea catalog item, never an arbitrary entity, because a purchase needs someone who grants the right and answers for the refund
two levels, no thirda bundle is a catalog item made of items; a storefront is a set of offers, and an offer points at an item and may override its price and a bundle's contents
a territorial priceis expressed by a storefront rather than on the item

What a storefront declares.

DeclaresWhat it is
offersthe set, each pointing at an item
schedulein wall-clock time, always UTC: when the window opens and closes
audiencea predicate, not a list of players — so the audience is a rule that keeps being true rather than a snapshot

A storefront whose audience a player does not fall into does not exist for that player.

An order's states.

FromTo
createdawaiting payment
awaiting paymentpaid · declined · expired
paidgranted
paid or grantedrefunded
AlwaysWhat it is
the priceis fixed in the order at the moment it is created, so a price change afterwards cannot alter what was agreed
awaiting paymenthas a declared deadline, declared per provider, because they differ
grantingis separate from payment: paid and granted are different states: money arriving and the thing appearing are two facts, and conflating them hides which one failed
an entitlementcarries its origin — a purchase, a promo code, a reward, a gift — so "where did this come from" is answerable a year later
a consumable entitlementaccumulates: it changes by an increment with an idempotency key, never by overwriting what was read
ownershipis an owner predicate: an entitlement belongs to a player by the same mechanism as any owned row
the catalogis declared in code and reaches the panel under the seed ownership mode by default: code creates what is absent, and a designer's edits survive the next push
provider secretslive in the operator plane, never in the declaration, and never in a repository
a provider's capabilitiesare declared: whether it has a usable API at all, and what it can do — so a catalogue does not promise a flow the store cannot serve

A refund can arrive at any time

A refund is an external transition: it arrives without any request from the game. What happens to what was granted is declared: there are three answers and no default.

Each extension point names the type it hands the hook: a purchase intent before the purchase — player, offer, provider, price — and the purchase itself after. A hook never receives an untyped bag.

Errors

  • Outside the audience answers not found, and repeating is pointless. Outside the schedule also answers not found, but is worth repeating once the window opens.
  • The provider is unavailable and the provider declined the payment are deliberately different answers: the first is unavailable and retryable with backoff, the second a conflict that retrying will not fix.
  • An invalid receipt is a validation refusal; a receipt already consumed by another order or another player is a conflict — that is what makes replay useless.
  • The price changed between reading the storefront and buying is a precondition failure: re-read and decide again, rather than being charged the new price silently.
  • Insufficient game currency is a conflict, not forbidden: the permission to buy is held, the balance isn't enough. Worth repeating after topping up.
  • A durable entitlement already held is a conflict.
  • The region or age does not permit the purchase answers forbidden, and repeating is pointless.
  • The order's deadline has passed is a conflict: create a new order.
  • The spending limit exhausted answers as a conflict or a rate limit depending on which limit it was, and it states when the limit resets.

Limits

The numbers arrive with the platform limits chapter; what happens at each ceiling is fixed now.

  • The catalog's size: publishing another item is refused as a conflict.
  • Storefronts per project: creation is refused.
  • Offers in a storefront: an addition is refused; the storefront is never truncated silently.
  • The lifetime of an order awaiting payment: a transition to expired, with an event.
  • The rate of purchase attempts: a rate limit with a deadline.
  • The spending limit per period: a conflict that states when the limit resets.
  • Retention of orders: past the declared period an order is no longer readable, rather than vanishing without explanation.
  • Entitlements per player: a grant is refused, and those already granted are never evicted.
  • A price's precision is not a limit but a type: an integer in minor units.