Skip to content
Docs · Storefront

Storefront and offers

What fetch_storefront() returns in the PowerUp Godot monetization SDK, how PowerUpOfferButton draws an offer, and the display rules it keeps.

Updated

The storefront is the one call that tells your game what a player can buy right now. In the PowerUp Godot monetization SDK, PowerUp.fetch_storefront() returns every live offer with its price, what it grants, whether this player can buy it and, if not, why. Your game draws it. This page covers the payload, the two nodes that draw it for you, and the display rules the SDK won't break.

Fetch it

var r: Dictionary = await PowerUp.fetch_storefront()
if r.ok:
    _render(r.storefront)

The result is ok plus storefront. The SDK keeps the last one in PowerUp.last_storefront and fires storefront_loaded, or storefront_failed with the reason. Fetch it each time the store opens: when you publish a new version, the game picks it up on its next request.

An offer is listed when it's active, inside its start and end times, sold on this platform, and grants only items this build can show (PowerUp checks your appVersion). A season pass offer is listed only while its pass is running, and No Ads only while an interstitial or banner it would remove is live.

The payload

FieldWhat it holds
offersEvery live offer, subscriptions included, in the order set in the console
featuredThe first lobby_featured offer the player can still buy, or null
subscriptionsEvery live subscription. premium is the first of them
noAdsThe No Ads offer and whether the player has it, or null
freeOffersFree rewards on a cooldown, with nextAvailableAt and claimsLeftToday
adOffersRewarded ads that pay coins, with how many are left today
spendLimitenforced, monthlyUsd, spentUsd and remainingUsd
ageGateWhether the age question is still needed, and the player's age band
purchaseBlockedNull, or why this player can't buy anything right now
game, playerThe game's name and environment, and the player asking
serverNow, configVersionThe server's time, and the published version this came from

featured, premium and noAds.offer are offers that also appear in offers. Group the list yourself by kind (bundle, pack, item, pass or subscription) and by placement (store, lobby_featured or contextual).

An offer

Here's a coin bundle from the seeded buggs.io demo, recorded in the sandbox and trimmed a little:

{
  "key": "coins_1500",
  "name": "Chest of Coins",
  "kind": "bundle",
  "placement": "store",
  "priceUsd": 9.99,
  "displayPrice": "$9.99",
  "rail": "stripe",
  "productId": "coins_1500",
  "basePlanId": null,
  "storeType": "consumable",
  "grants": { "coins": 1500, "bonusCoins": 0, "items": [] },
  "reference": { "multiplier": 1.2, "baselineKey": "coins_120", "verified": true },
  "startsAt": null,
  "endsAt": null,
  "owned": false,
  "purchasesLeft": null,
  "subscription": null,
  "disclosures": { "price": true, "currencyRateUsd": 0.00825, "renewalTerms": false, "cancelPath": false, "perks": false },
  "purchasable": true,
  "notPurchasableReason": null
}
FieldMeaning
key, name, descriptionThe offer's id, and the words to show
kind, placementWhat it is, and where it belongs
priceUsd, displayPriceThe list price in US dollars, and that price as text
railHow it's paid on this platform: stripe on web and desktop, apple on iOS, google on Android
productId, basePlanIdThe id it's sold under in that store, and a Play subscription's base plan
storeTypeconsumable, non_consumable, auto_renewable or non_renewing
grantscoins, bonusCoins (a subscriber's bonus on coin packs) and items
referenceThe value badge's multiplier, the baselineKey it's measured against, and whether it's verified
startsAt, endsAtThe sale window, set on the server. Null means open
owned, purchasesLeftWhether the player already has it, and how many more they can buy (null for no limit)
subscriptionperiod, periodLabel, renews, cancelHow, trialDays and perks
disclosuresWhat the card must show: price, currencyRateUsd, renewalTerms, cancelPath, perks
purchasable, notPurchasableReasonWhether this player can buy it now, and if not, why

notPurchasableReason is one of not_available, owned, purchase_limit_reached, age_gate_required, parental_consent_required or spend_limit_reached, checked in that order.

The display rules

The SDK draws offers through PowerUpDisclosure, a set of pure static functions. PowerUpOfferButton applies all of these rules, and a card you build yourself gets them by calling the same functions:

  • A value badge needs proof. "1.2× value" only renders when the server checked the bundle against a real baseline offer (reference.verified). A subscriber's Premium bonus never counts toward it. On iOS and Android, store_badge_text() also hides it when the store's own prices put the ratio more than 2% off.
  • Countdowns need a real end. A countdown only runs to the server's endsAt, measured on the server's clock. No end time, no countdown. An offer past its end stays disabled even if an old payload still says it's for sale.
  • No price, no button. A card the server didn't give a price shows "Price unavailable" and can't be pressed.
  • Coins show what they cost. A coin amount comes with its real-money value beside it, not in a tooltip: "1,200 coins (≈ $9.90)".
  • Subscriptions carry their terms. A subscription card lists its price per period, whether it renews, how to cancel and any trial. If the price, the period, the renewal terms or the way to cancel is missing, the card can't be bought and says "Subscription terms missing".
  • Blocks are shown. When a player can't buy something, the card says why ("Owned", "Monthly spending limit reached", "Confirm your birth year to buy") instead of failing quietly.

The publish checks hold your config to the same rules before it ships, so these guards are a second line in the client: a slip in your UI code can't bring back a badge or a countdown the server didn't back.

The functions you'll use most:

FunctionReturns
display_price(offer, localized = "")The price to show, or "" when the card has none. A store's localized string wins
currency_line(offer)"1,200 coins (≈ $9.90)"
bonus_line(offer)"+240 Premium bonus"
badge_text(offer)"1.15× value", or ""
seconds_remaining(offer, now_unix)Seconds until endsAt, or -1
subscription_lines(offer, localized = "")Price per period, renewal, cancel path and trial, or an empty list when a term is missing
perk_lines(offer)A subscription's perks, one line each
blocking_reason(offer, localized = "")Why the card can't be bought, or ""
blocking_text(reason), blocking_short(reason)The words for a notPurchasableReason, full and button-sized
spend_limit_notice(storefront)"Monthly limit: $4.99 of $50.00 used", when a limit applies
PowerUp Console · sandbox
The PowerUp console's store card preview: an offer drawn the way the game shows it, with its checks beside it.
The console previews each card the way the SDK will draw it, next to its checks.

PowerUpOfferButton

PowerUpOfferButton is a regular Godot Button that writes its own text from those rules:

var button := PowerUpOfferButton.new()
button.configure(offer)
button.offer_pressed.connect(_on_offer_pressed)
%OfferGrid.add_child(button)

The card reads, top to bottom: the name (with "[1.2x verified]" when the badge may show), the price or "Price unavailable", the coin line, any Premium bonus, the countdown ("Ends in 2d 4h") or "Offer ended", a subscription's terms and perks, and the reason it's blocked. It disables itself when the offer can't be bought and puts the reason in its tooltip. offer_pressed(offer) only fires for an offer that can be bought.

The countdown runs on the server's clock while the PowerUp autoload is running. Because it's a Button, your theme styles it like any other. The SDK sets its text, a 220-pixel minimum width, left alignment and word wrap, and dims it while it's disabled.

Store prices on phones

On iOS and Android, show the store's own localized price. The button draws the server's US dollar price, so on phones build the card from PowerUpDisclosure and pass the store's string as localized:

# store_prices: product id → what Play Billing or StoreKit returned for it
# (the formatted string, or a Dictionary with it and the amount)
func _fill_card(offer: Dictionary, offers: Array, store_prices: Dictionary) -> void:
    if not PowerUpDisclosure.store_has_product(offer, store_prices):
        hide()   # the store didn't return it, so it can't be bought here
        return
    var local := PowerUpDisclosure.store_price(offer, store_prices)
    %Price.text = PowerUpDisclosure.display_price(offer, local)
    %Terms.text = "\n".join(PowerUpDisclosure.subscription_lines(offer, local))
    %Badge.text = PowerUpDisclosure.store_badge_text(offer, offers, store_prices)
    %Buy.disabled = PowerUpDisclosure.blocking_reason(offer, local) != ""

PowerUpStorefront

PowerUpStorefront is a whole store screen: a VBoxContainer that fetches the storefront, builds a grid of offer buttons and shows the spend-limit notice. Add it to a scene and call load_offers(). It rebuilds the grid each time, so calling it again is safe.

MemberWhat it does
columnsGrid columns. Default 3
show_spend_limitShow the monthly limit notice. On by default
load_offers()Fetch the storefront and draw it
offer_purchased(result)A purchase went through, and the grid redraws
offer_failed(reason)A purchase was refused, with the server's reason

Its buy path calls PowerUp.purchase(), which only works with sandbox keys. That makes it a quick way to try your store while you set things up. For real money, build the grid from PowerUpOfferButton and send offer_pressed to the checkout for the offer's rail, as payments shows.

Free offers, coin prices and limits

Not everything in a store costs money:

  • Free offers in freeOffers are claimed with PowerUp.claim_free_offer(key). A claim too soon answers cooldown or daily_cap, with details.nextAvailableAt; count down to it with PowerUp.seconds_until().
  • Items for coins come from your catalog (PowerUp.fetch_catalog(), where a coin-buyable item has price.coins) and are bought with PowerUp.buy_item(item_key, expected_price). Pass the price the player saw. If it changed, the server answers price_changed with the new price in details.priceCoins instead of charging a different amount. The coin wallet page covers the wallet behind it.
  • Spending limits. When spendLimit.enforced is true, the player is a child or teen with a monthly limit: show PowerUp.spend_limit_notice() near the buy buttons.
  • Blocked stores. When purchaseBlocked is age_gate_required, ask the age question first (getting started shows how). parental_consent_required means a child whose parent hasn't approved purchases: show the store read-only.

Questions

Why doesn't my bundle show a value badge?

The badge only shows when the server checked the bundle against a real baseline offer in the same storefront and marked it verified. On iOS and Android, PowerUpDisclosure.store_badge_text() also hides it when the store's own prices move the ratio more than 2% from the verified one.

Why is an offer missing from the storefront?

It's outside its sale window, not sold on this platform, grants an item this build can't show, belongs to a season pass that isn't running, or hasn't been published yet. Players only see published versions; preview the draft with a sandbox key and the draftConfig option.

Does the storefront show prices in the player's currency?

The server's prices are US dollars. On iOS and Android, show the store's localized price with PowerUpDisclosure.store_price(). Web and desktop checkouts go through Stripe in US dollars, so the server's displayPrice is the price there.

Can I restyle PowerUpOfferButton?

Yes. It's a regular Godot Button, so themes and overrides apply. Its text comes from the display rules. For a different layout, build your own card from the PowerUpDisclosure functions.

See it running on a real game.

We’ll walk you through the console with buggs.io’s setup.