Real-time Catalog
Learn how to expose REST API endpoints on your game backend to power personalized web shops with real-time offers. This guide covers the Catalog, Player, and Purchase APIs with protobuf-based type-safe integration.
The Stash v1 WebShop APIs enable game studios to power dynamic, player-personalized web shops with special offers. These protobuf-based APIs provide a robust, type-safe integration path that accelerates development while ensuring reliability at scale.
Getting Started
You can integrate the v1 APIs in two ways. Both produce the same endpoints; pick whichever fits your stack, then follow the Integration Steps below to roll it out.
Clone the proto repository and generate type-safe clients automatically:
# Add the public-api repo as a submodule in your backend repo
git submodule add https://github.com/stashgg/public-api.git
# Or just clone it locally
git clone https://github.com/stashgg/public-api.git
# Generate clients using the included script
cd public-api
./gen.shThe gen.sh script generates clients for Go, TypeScript, and OpenAPI specs. Adapt it for your preferred language (C#, Java, Python, etc.) using standard protobuf tooling.
No documentation required: The generated clients provide full type definitions, so your IDE's autocomplete guides the integration.
Integration Steps
Obtain API credentials
Your Stash engineering partner will generate an Egress API key for you in Stash Studio. Set your Game Backend URL in Stash Studio to match where the APIs will be hosted. Custom API endpoint paths can be configured if needed.
Generate clients or review the API reference
Use the proto repository to generate type-safe clients, or review the API Reference for detailed endpoint documentation.
Implement the required endpoints
At minimum, implement these endpoints on your game backend:
Required:
- GetCatalog: Returns the product catalog
- ConfirmPayment: Powers both WebShop and StashPay
Register and cancel (on by default, optional per shop):
Stash calls these two as a pair. Implement both, or ask your Stash engineering partner to turn them off for your shop. See Register and cancel are optional.
- RegisterPayment: Reserves inventory across all game/shop clients
- CancelPayment: Releases inventory reservation locks
Optional:
- GetOfferDetails: Show extra details like drop rates for legal compliance
- GetPlayer: Show player-specific details like avatar, level, currency
Test with test credentials
Test your integration with your studio-test.stash.gg credentials before going live.
API Overview
The v1 APIs are organized into three services, exposed as REST endpoints. Stash sends requests to your Game Studio's backend servers:
All endpoints use HMAC authentication for secure server-to-server communication.
JSON wire format
Stash reads your responses leniently:
- Field names: camelCase as shown in the API reference (
productId) or snake_case (product_id). Both are accepted, also within one response. - Enum values: the value's name (
"CONTENT_ITEM_TYPE_WEB_STORE_BONUS") or its number (1). An enum name Stash does not recognize is ignored, and the field keeps its default value. - Unknown fields: ignored. Extra fields in your response do not cause an error.
Catalog API
Retrieve dynamic, player-specific product catalogs.
GetCatalog
GET /api/v1/catalog: returns the complete product catalog for a given platform, region, language, and playerId. The response is structured as sections (grids or carousels) containing PurchasableItem, OfferChainItem, or NonPurchasableItem entries.
For the full request parameters and response schema, see the GetCatalog API Reference.
GetOfferDetails
GET /api/v1/catalog/offer: returns detailed offer information including drop rates for loot boxes, required for legal compliance in many jurisdictions.
For the full request parameters and response schema, see the GetOfferDetails API Reference.
Purchase API
Handle the complete purchase lifecycle with three endpoints.
Purchase Flow
For where the player's money is at each step, why ConfirmPayment is the point where your backend can still decline a purchase, and which checks only your backend can enforce, see Payment Flow.
Register and cancel are optional
RegisterPayment and CancelPayment are on or off together. By default Stash calls RegisterPayment before ConfirmPayment, and CancelPayment when a registered web shop cart purchase does not complete.
If your backend does not need a reserve step, ask your Stash engineering partner to turn both off for your shop. Stash then calls neither, and ConfirmPayment is the first request that carries the purchase's transaction ID. Stash can also turn them off only for checkout link purchases (Stash Pay and Buy Now), so web shop carts keep them.
RegisterPayment
POST /api/v1/purchase/register (Step 1): registers a purchase intent. Your backend should reserve inventory for the player and respond with one of the inline status codes (e.g. SUCCESS_REGISTERED, FAILED_INSUFFICIENT_INVENTORY, FAILED_PURCHASE_LIMIT_EXCEEDED, FAILED_INVALID_PRICE).
For the full list of status codes and the complete request/response schema, see the RegisterPayment API Reference.
ConfirmPayment
POST /api/v1/purchase/confirm (Step 2A): confirms and completes the purchase after successful payment. Your backend grants items to the player and acknowledges the transaction.
Unified Integration: This endpoint is the same API used for StashPay integrations. Implement once to power both your WebShop and StashPay checkout links.
Declining a confirm. To refuse the purchase, respond with an HTTP 4xx status and a body that carries a decline object naming a PurchaseConfirmationStatus, and omit results. Stash never charges the player for a declined confirm. Where the checkout supports it, Stash uses the status to pick the message the player sees (for example PURCHASE_CONFIRMATION_STATUS_FAILED_INSUFFICIENT_INVENTORY, PURCHASE_CONFIRMATION_STATUS_FAILED_PURCHASE_LIMIT_EXCEEDED, PURCHASE_CONFIRMATION_STATUS_FAILED_INVALID_PRICE); otherwise the player sees a generic failure message. A 4xx without a decline still declines the purchase, but carries no structured reason.
{
"decline": {
"status": "PURCHASE_CONFIRMATION_STATUS_FAILED_PURCHASE_LIMIT_EXCEEDED",
"productId": "gold_1000",
"message": "player has reached the purchase limit for this item"
}
}Use PURCHASE_CONFIRMATION_STATUS_FAILED_PACK_LIMIT_EXCEEDED when the purchase allowance shared by every item of a purchase pack is exhausted, and PURCHASE_CONFIRMATION_STATUS_FAILED_PURCHASE_LIMIT_EXCEEDED when only the item's own limit is. Return exactly one of results or decline; a body carrying both is treated as declined.
For the full request/response schema (including the decline object, its status values, and optional fields like extraInGameCurrency, extraLoyaltyPoints, and emailMarketingOptIn), see the ConfirmPayment API Reference.
CancelPayment
POST /api/v1/purchase/cancel (Step 2B, failure path): cancels a pending purchase. Your backend should release reserved inventory so the player can retry.
See the CancelPayment API Reference for the full request/response schema.
Loyalty snapshot on purchase requests
When the shop runs a live loyalty program, the RegisterPayment and ConfirmPayment request bodies carry a loyalty snapshot (type PlayerLoyaltyState): the same object your backend already receives on loyalty webhooks. This gives your backend the player's XP balance, tier, and tier-relative earn multiplier at purchase time, in-band with the register/confirm calls, so it can apply tier-aware logic (for example bonus sizing) without a separate lookup.
"loyalty": {
"campaignId": "spring_2026",
"totalPoints": 1250,
"currentTierId": "silver",
"pointsMultiplierPermille": 2000,
"pointsToNextTier": 750,
"pointsToNextMilestone": 250,
"nextMilestone": {
"milestoneId": "milestone_uuid",
"rewards": [
{ "itemId": "gold-coins", "quantity": 500, "type": "IN_GAME_CURRENCY" }
]
}
}- The snapshot reflects state as of the request. On
RegisterPaymentit is pre-purchase; onConfirmPaymentit reflects the XP the confirmed purchase awards. - It is present only when the shop has a published, enabled loyalty program and a live campaign. Otherwise the field is omitted and the request body is byte-identical to a non-loyalty shop's.
- Treat it as read-only context. It never replaces the authoritative loyalty state your backend receives on the loyalty webhooks; grant XP-linked effects from the webhook, not from this snapshot.
The snapshot fields match the shared PlayerLoyaltyState payload documented in Loyalty webhooks. The signed pointsDelta and previousTierId are event-specific and meaningful only on webhooks; the purchase-request snapshot otherwise carries the same fields, including the balance, tier, progression, and nextMilestone. When the purchase's XP gain crossed one or more milestone thresholds, the snapshot also carries unlockedMilestones, with the same semantics and de-duplication guidance as on the webhook payload.
Player API
Retrieve player profile information for personalized experiences.
GetPlayer
GET /api/v1/player: returns player profile data (name, avatar, language, in-game level, in-game currency) for display in the web shop. Stash calls this endpoint with the playerId you supplied to the catalog request.
See the GetPlayer API Reference for the full request/response schema.
Key Data Types
Catalog Items
Each catalog item is exactly one of five types (a protobuf oneof; the JSON key is in parentheses):
| Type | Use Case |
|---|---|
PurchasableItem (product) | Standard products and bundle offers with pricing |
PurchasePack (pack) | One offer sold at several prices; see Purchase packs |
OfferChainItem (chain) | Progressive offers that unlock sequentially with CLAIMED / UNLOCKED / LOCKED link statuses |
NonPurchasableItem (info) | Banners, promotions, and informational displays |
FreeItem (free) | Free-to-claim gifts with a claim status, an optional refreshAt window, and claim tracking |
For the full field-level structure of each type, including pricing, contents, expiration, attributes, and offer chain link details, see the GetCatalog API Reference.
Stash skips an item whose type it does not recognize and renders the rest of the catalog.
Prices
Each PurchasableItem carries one price, set in one of two ways:
price | What Stash does |
|---|---|
amount (currency, cents) | Shows and charges this price as sent. No Stash Studio price is looked up for the item. |
priceId | Looks up the price imported in Stash Studio for that ID and the player's platform and region. See Studio price lookup. |
| Omitted | The same Studio lookup, using the item's productId. |
When you send amount:
centsis in the currency's CLDR minor units:199inUSDis $1.99, and300inJPYis ¥300.centsmust be greater than0. To give an item away, use aFreeItem.- Use one currency for all priced items in a response. Stash shows one currency per shop page and leaves out priced items in any other currency.
Studio price lookup
Stash looks up an imported price in the currency that is primary for the player's region, and takes the first match in this order:
- The player's region and platform.
- The player's region, from a file imported for all platforms (Universal in Studio).
USinUSD, the player's platform.USinUSD, Universal.
If none of these exists, the item is left out of the catalog. A missing country therefore shows the US price rather than hiding the item, and a row imported for a country in a currency other than its primary one is never matched. When no platform is resolved, only Universal rows match.
To return a different price per platform or country, choose the price from the request's platform and region:
platformcomes from the Unity SDK session, then from the platform stored on the player's account (theuser.platformyour backend sends on Approve custom login), then from the browser's user agent (iPhone, iPad and iPod are iOS; Android is Android). A desktop browser has no platform unless the account has one stored, so senduser.platformwhen you link the player if prices differ by platform.regionis the most recent region your backend sent for the player, asuser.regionCodeon Approve custom login or Generate authenticated URL when the player signs in. When you send none, it is the country of the player's IP address, orUSwhen that is unknown. It is not the country of the player's app store account.
Purchase packs
Use a pack when one offer is sold at several prices and the player buys one of them, for example the same reward at three price points. Each price point is one entry in the pack's items (a PurchasePackItem).
Declare the values the whole offer shares once, on the pack:
| Field | Meaning |
|---|---|
id | Required. Your stable identifier for the offer, unique within the response. A pack without an id is left out of the catalog. |
items | Required. The offer's price points, in display order. Each one has its own productId and price, and optional contents. |
maxPurchasable | Optional. The player's remaining purchases of this offer. Stash applies it to every item in the pack. |
expiration | Optional. After this time no item in the pack is available for purchase, except a purchase already in checkout within a checkout grace period agreed for your shop. |
refreshAt | Optional. When the offer's allowance next resets, for display such as a countdown. Purchase checks never use it. |
A PurchasePackItem has no maxPurchasable, expiration or refreshAt of its own, so the pack's values govern every item in it.
- Buy the whole offer. To sell one item that buys everything in the offer in a single transaction, give it
type: PURCHASE_PACK_ITEM_TYPE_WHOLE_PURCHASE_PACKand list the combined rewards in itscontents. Use this type on at most one item per pack. - Remaining count. Compute
maxPurchasableper player and send it fresh in every response. Stash keeps no purchase count of its own: it checks each purchase against the value from your catalog. - Purchase calls. For an item bought from a pack,
RegisterPayment,ConfirmPaymentandCancelPaymentcarry the pack'sidaspurchasePackIdon that item.productIdstill identifies the price point. - Unavailable offers. To withdraw an offer or a price point, leave it out of the response.
See PurchasePack in the API reference for every field.
Free items and claim identity
A FreeItem is claimable at no cost. When a player claims one, Stash records the redemption and emits a FREE_ITEM_REDEEMED webhook. Two fields govern how claims are tracked:
status: your response is authoritative for whether the gift is offered (AVAILABLE,CLAIMED, orLOCKED). After a recorded claim, Stash overridesAVAILABLEtoCLAIMEDon catalog reads so a reload cannot show a consumed gift as claimable. It only ever applies that one flip: aCLAIMEDorLOCKEDstatus you send is never changed, and Stash never producesAVAILABLE. The override ends at the item'srefreshAt; from then on your status alone decides the next period.claimId(optional): a stable claim identity, separate fromitemId. Free items that share aclaimIdare the same claim for a claim period: claiming any one of them consumes the claim for all of them. When absent,itemIdis the claim identity, which is the existing behavior; integrations that do not send it change nothing.
Use claimId when the item you serve varies per player, for example a daily gift whose itemId differs by loyalty tier. Keep the claimId stable across the variants and the claim still de-duplicates as one gift per period, whichever variant a player sees.
Rules to follow:
- Return at most one item per
claimIdper response; additional items sharing one may be discarded. A locked preview of a later reward must carry a distinctclaimId, or none at all. - A repeat claim inside the same window returns an idempotent success: no duplicate
FREE_ITEM_REDEEMEDwebhook, and no second Loyalty XP credit for gifts that grant XP. - On the
FREE_ITEM_REDEEMEDpayload,itemIdis the item that was granted;claimIdis the claim it consumed and appears only when it differs fromitemId.
Localization
All user-facing text supports localization. Each text field is a LocalizableText. Send defaultText in every one, since it is the fallback, and add at most one of:
localizationKey: Stash-side lookup in our localization system (requires sharing localization keys with Stash up front).localizedText: Pre-resolved text in the player's language, computed by your backend.
Pick whichever fits your localization pipeline; you can mix the two across different fields in the same response.
Stash shows localizedText when you send it. For a localizationKey, it shows the translation for the player's language, or defaultText when the key has no translation. With neither, it shows defaultText.
Web Store Bonuses
Give players extra content for buying on the web:
| Field | What it does |
|---|---|
bonusItems on a product, or on an item in a pack | The bonuses that come with the item, each a type, an id and an amount. On purchase, Stash sends them to your ConfirmPayment. |
contents[].bonusQuantity | A bonus amount shown next to a content item's quantity, for example +25. It is an amount, not a percentage. Display only: Stash does not send it to ConfirmPayment, and when the item also sets bonusItems, those decide what is granted. |
contents[].type: CONTENT_ITEM_TYPE_WEB_STORE_BONUS | Shows a content item as a web store bonus. |
Grant bonuses from ConfirmPayment. The bonusItems amounts on the ConfirmPayment request are the amounts to grant for the bonus IDs they name. Do not also grant your offer's own bonus for an ID listed there, or the player receives it twice.
Drop Rate Compliance
For loot boxes and randomized rewards, the API supports drop rate disclosure required for legal compliance in many jurisdictions.
Enabling Drop Rate Display
To show drop rates or offer details in a popup:
-
Add a badge with the show details action: Include a badge on the offer with
action: BADGE_ACTION_SHOW_OFFER_DETAILS. This signals to the frontend to display an info button that calls the GetOfferDetails API and opens the corresponding modal. -
Enable per-item details (optional): For individual content items that should show their own reward/drop rate details, include
clickAction: CONTENT_ITEM_CLICK_ACTION_SHOW_DETAILS_BY_IDon thatContentItem.
Example Catalog.PurchasableItem Response
{
"attributes": {
"badge": {
"text": { "defaultText": "Loot Box" },
"action": "BADGE_ACTION_SHOW_OFFER_DETAILS"
}
},
"contents": [
{
"name": "Legendary Hero",
"guid": "xxx",
"image": "https://...",
"quantity": "1",
"dropRate": "2.5%",
"clickAction": "CONTENT_ITEM_CLICK_ACTION_SHOW_DETAILS_BY_ID"
},
{
"name": "Common Item",
"quantity": "1",
"dropRate": "97.5%"
}
]
}If using clickAction, the guid must be supplied (can't be auto-generated)
to cross-reference with the GetOfferDetails API response.
Caching
Stash caches the catalog response your server returns. This reduces load on your catalog server and improves shop load times for players.
Caching is on for all Real-time Catalog integrations. If you wish to disable it temporarily for testing purposes, let your engineering partner at Stash know.
How long are responses cached?
The effective cache lifetime is:
min(5 minutes, earliest upcoming timestamp in your response)Stash inspects these timestamps in your response and caps the cache at the earliest upcoming one:
expirationon aproduct,chainorpackitemrefreshAton aproduct,packorfreeitemdisplayExpirationon a sectionnextResetAton a trigger, for scheduled resets such as a midnight-UTC daily offer rollover
Stash only ever shortens the window from these signals; it never serves a cached response past the point they indicate.
The default cap is 5 minutes when your response contains no timestamp signals.
Recommended pattern for timed catalog resets: set one of the timestamps above equal to your planned reset time. Stash re-fetches exactly at that boundary, so there is no perceptible delay when the catalog changes.
Immediate invalidation after player actions
Stash clears a player's cached catalog immediately after every non-read action:
- Purchases
- Free-gift redemptions
- Loyalty-milestone claims (where integrated)
The next shop load re-fetches live from your server. No special annotation or forced reload is needed. This is automatic.
Always-fresh Stash-side data
Only the raw response from your catalog API is cached. Data that Stash computes (prices, bonuses, offer-chain and purchase-progression state, injected free offers) is recomputed on every request and is never cached. Changes you make to prices or bonuses in Stash Studio are reflected immediately regardless of the cache window.
Force a catalog refresh
Most integrations never need this. The cache window is short, and Stash already clears a player's cached catalog automatically after the actions above.
If something changes outside the web shop and you want the catalog view in the Web Shop to reflect it right away, call Force refresh catalog from your backend. Pass a player_id to refresh a single player, or omit it to refresh every player of your shop. It applies only to shops with a Real-time Catalog integration; for any other integration it is a no-op that still returns success.
This endpoint is optional. Stash checks every Web Shop purchase against the catalog it holds for the player, and that can be a cached response, up to the window in How long are responses cached?. An item's expiration is compared with the current time when the purchase completes, so an expired offer is refused even while a cached view still shows it. A maxPurchasable count is only as current as the cached response, so keep enforcing your purchase limits when you confirm a purchase. Call this endpoint when a change outside the Web Shop, such as an in-game purchase, should reach the catalog view and the counts Stash checks before the cache window ends.
Never omit the player_id unless you know exactly what you are doing. Forcing a catalog refresh for every single player can result in a much slower Web Shop experience for all players across the board and a spike of API calls to your external catalog API server.
| Scenario | Call ForceRefreshCatalog? |
|---|---|
| A player made a purchase in-game (outside the web shop) that changes what they should see | ✅ Yes (optional); refresh so the catalog view in the Web Shop updates right away |
| Your catalog changed outside a normal player action and you want it visible before the cache expires | ✅ Yes (optional); shortens the visible staleness window |
| A player completed a purchase in the web shop | ❌ No; the cache is cleared automatically |
| A player redeemed a free gift or claimed a loyalty milestone | ❌ No; the cache is cleared automatically |
| You changed prices or bonuses in Stash Studio | ❌ No; Stash-computed data is never cached and updates immediately |
| Your shop does not use a Real-time Catalog integration | ❌ No; the call is a no-op |
Frequently asked questions
Does Stash cache the response my server returns?
Yes. Stash caches your catalog/offer response for a short, bounded window. This reduces load on your catalog server and speeds up the shop for players.
What is the worst-case staleness? How long is my data cached?
min(5 minutes, earliest upcoming timestamp in your response), counting only the timestamps listed in How long are responses cached?. Without any of those timestamps in your response, responses may be served for up to 5 minutes.
How do I make a catalog change appear at a known time, such as a daily reset?
Include the planned reset time as one of the timestamps that shorten the cache, for example a trigger's nextResetAt or an item's expiration. Stash re-fetches exactly at that boundary with no delay.
What happens after a player makes a purchase?
The player's cached catalog is cleared immediately on every purchase, free-gift redemption, and loyalty-milestone claim. The next shop visit re-fetches live from your server. This is automatic. No special annotation is needed.
Do I need to send Cache-Control HTTP headers?
No. Stash does not use HTTP Cache-Control headers for this. Control freshness via refresh and expiration timestamps in your catalog payload instead.
Can the cache show a player an offer they already purchased?
Not after a Web Shop purchase: Stash clears the player's cached catalog as soon as it completes. A purchase made outside the Web Shop, such as in-game, shows when the cache window ends or after you force a catalog refresh. Until then Stash still refuses an offer whose expiration has passed, but it checks the quantity against the cached maxPurchasable, so enforce your purchase limits when you confirm a purchase. Accurate expiration and refresh timestamps in your response shorten the window.
My catalog has offers with different refresh times. How should I structure the timestamps?
Stash caps the cache lifetime at the earliest upcoming timestamp among those that shorten the cache, so send accurate values on every item and section. Ideally align all refreshes to a single consistent reset time so the whole catalog expires together.
Can caching be turned off while I test catalog changes?
Yes. Contact your Stash engineering partner and they will disable caching for your test shop, so every request hits your server live.
What are the benefits of caching?
Faster shop loads for players, fewer requests to your catalog server, and improved reliability if your server is briefly slow or unavailable.
Authentication
Stash authenticates outgoing requests to your game backend using a versioned HMAC-SHA256 signature in the x-stash-hmac-signature header:
x-stash-hmac-signature: v1;<appId>;<unixMillisTimestamp>;<base64-hmac>| Field | Description |
|---|---|
v1 | Protocol version |
<appId> | Your immutable App ID (Studio → Project Settings → General) |
<unixMillisTimestamp> | Request timestamp in Unix milliseconds |
<base64-hmac> | Standard base64-encoded HMAC-SHA256 signature |
The signature covers "<unixMillisTimestamp>." + <body>, where <body> is the exact bytes Stash transmits: empty string for GET requests, raw request body for POST requests. The HMAC key is your egress secret base64-decoded: the value shown in Studio → Project Settings → API Keys is base64-encoded; decode it before computing the HMAC (e.g. Buffer.from(secret, 'base64')). Reject requests where <unixMillisTimestamp> is more than 5 minutes from your server clock.
Verifying the signature
For Node.js, Python, and Go implementation code, see API Keys → HMAC Verification.
New shops (created on or after 2026-08-15) receive only x-stash-hmac-signature. The legacy unversioned stash-hmac-signature header is no longer emitted for them. New integrations should verify x-stash-hmac-signature only.
If you've implemented HMAC authentication and your game backend is still
responding to Stash's API requests with 3xx, 4xx, or 5xx error codes, you may
need to allowlist test-api.stash.gg and api.stash.gg on your cloud network
firewall.
Configuration
To configure your Real-time Catalog endpoint in Stash Studio:
Navigate to game settings
In Stash Studio, navigate to your game settings.
Open App Backend
Go to Project Settings → App Backend
Set endpoint URL
Set your Game Backend URL to match where the Catalog & Purchase APIs will be hosted.
Configure authentication
Your Stash engineering partner can configure custom API endpoint paths if needed.
Debugging & Logs
Stash Studio provides comprehensive logging and debugging tools for your Real-time Catalog. Use these tools to monitor your catalog endpoint and troubleshoot issues:
- Check Stash Studio logs for endpoint response times
- Verify your endpoint returns valid JSON
- Ensure all required fields are present
- Test with your studio-test credentials before going live
How is this guide?
Managed Catalog
Learn how to create and publish products, configure Free Gift offers, and schedule product availability in the Stash Webshop managed catalog.
Payment Flow
Understand where the money moves in a Real-time Catalog purchase, why ConfirmPayment is required rather than optional, and which checks only your backend can enforce.