Loyalty API
The public read-only loyalty API: fetch a shop's tiers and milestones, and read a per-player loyalty snapshot, from your backend. Endpoint reference, auth, and response shapes.
Stash exposes a read-only public loyalty API so your backend can read the same program configuration and per-player state that drives the loyalty hub and the loyalty webhooks. Use it to render tier and milestone information in your own surfaces, or to reconcile a player's XP and tier on demand.
This is an ingress integration: your backend calls Stash. It is read-only. All program state (XP balances, tiers, claim history) is still owned by Stash and mutated only through purchases, refunds, and claims. See the API Reference for how ingress endpoints are authenticated and called.
These endpoints are published in the hosted api.stash.gg SDK swagger, so the full request/response schema and a try-it console appear under API Reference once the swagger is regenerated. This page is the loyalty-specific overview. Endpoint paths and field names below are consistent with the loyalty webhook payload; where they differ from the shipped swagger, the swagger is authoritative.
Authentication
Call these endpoints from your backend only, with your shop's server API key, exactly like every other ingress endpoint. The key is shop-scoped, so the tiers, milestones, and snapshots returned belong to the shop the key authenticates. Never call them from a game client or expose the key.
Endpoints
GET /sdk/server/loyalty/program
Returns the shop's loyalty program configuration, nested under activeCampaign: the live campaign, its tiers in ascending startPoints order, each tier's points-per-dollar earn rate, and the milestones inside each tier. This is the published, player-facing configuration, not the Studio draft.
{
"activeCampaign": {
"campaignId": "spring_2026",
"tiers": [
{
"tierId": "bronze",
"name": "Bronze",
"startPoints": 0,
"pointsPerDollar": 5,
"iconUrl": "https://cdn.stash.gg/loyalty/bronze.png",
"pointsMultiplierPermille": 1000,
"milestones": [
{
"milestoneId": "milestone_uuid",
"name": "Welcome Bonus",
"pointsNeeded": 100,
"rewards": [
{ "itemId": "gold-coins", "quantity": 500, "type": "LOYALTY_REWARD_TYPE_IN_GAME_CURRENCY" },
{ "itemId": "gems", "quantity": 50, "type": "LOYALTY_REWARD_TYPE_IN_GAME_CURRENCY" },
{ "itemId": "starter-sword", "quantity": 1, "type": "LOYALTY_REWARD_TYPE_SKU_ITEM" }
]
}
]
},
{
"tierId": "silver",
"name": "Silver",
"startPoints": 1000,
"pointsPerDollar": 10,
"iconUrl": "https://cdn.stash.gg/loyalty/silver.png",
"pointsMultiplierPermille": 2000,
"milestones": []
}
]
}
}Prop
Type
The activeCampaign object is a Campaign: its campaignId and its tiers, in ascending startPoints order. Each tier carries:
Prop
Type
Each milestone carries milestoneId, display name, pointsNeeded (the Loyalty points balance that unlocks it), and a rewards list. See Tiers and Milestones & rewards.
Each reward is { itemId, quantity, type }. type is a typed enum identifying the reward kind. This API response emits the prefixed enum value names: LOYALTY_REWARD_TYPE_IN_GAME_CURRENCY, LOYALTY_REWARD_TYPE_LOYALTY_CURRENCY, LOYALTY_REWARD_TYPE_LOYALTY_POINTS, LOYALTY_REWARD_TYPE_SKU_ITEM, plus LOYALTY_REWARD_TYPE_UNSPECIFIED for the zero/unknown value. For a SKU reward, itemId is the catalog product's SKU (external ID); for currency and points rewards, itemId is the reward type ID. rewards is always a list: a milestone can carry multiple currency rewards and multiple catalog product (SKU) rewards, and the API returns all of them.
The type values here differ from the webhook form. This API response emits the fully prefixed enum names (LOYALTY_REWARD_TYPE_..., standard protojson enum-by-name serialization), while the loyalty webhooks and snapshot payloads emit the bare token (IN_GAME_CURRENCY, SKU_ITEM, and so on). They map one-to-one; strip the LOYALTY_REWARD_TYPE_ prefix to line them up.
Key your logic on tierId and milestoneId, never on the display name. Names are editable after publish and are localized.
GET /sdk/server/loyalty/snapshot?playerId={externalAccountId}
Returns the current loyalty snapshot (type PlayerLoyaltyState) for one player: the same object your backend receives on loyalty webhooks and purchase requests, read on demand. Use it to reconcile XP and tier without waiting for the next event.
{
"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" },
{ "itemId": "starter-sword", "quantity": 1, "type": "SKU_ITEM" }
]
}
}
}The loyalty object is the shared PlayerLoyaltyState snapshot documented in Loyalty webhooks. nextMilestone is a Milestone object carrying the upcoming milestone's milestoneId and its full rewards list. Reward type values in this snapshot use the bare token form (IN_GAME_CURRENCY, LOYALTY_CURRENCY, LOYALTY_POINTS, SKU_ITEM), matching the webhooks, not the prefixed LOYALTY_REWARD_TYPE_... form used by the GetLoyalty config response above. Because a snapshot read is not tied to a specific event, the event-only fields (pointsDelta and previousTierId) are not returned. The numeric fields are non-negative uint32; tier ID fields are optional and omitted when unset. When the player has no XP in the live campaign, the snapshot reflects the first tier: totalPoints 0, currentTierId the first tier, and pointsMultiplierPermille 1000.
The snapshot omits its body when the shop has no published, enabled program and live campaign, the same presence rule as the webhook payload. Treat a missing snapshot as "no loyalty".
When to use it
- Render tiers and milestones in your own UI. Read the program once and cache it; it changes only when you publish in Studio.
- Reconcile a player on demand. Read a snapshot to recover XP and tier if you missed or are replaying a webhook.
- Do not use it to grant rewards. Grants are driven by the loyalty webhooks (or synchronous milestone delivery). This API is read-only context, never a grant trigger.
How is this guide?