Store Get Webhook
The store.get webhook retrieves a list of items available for a player in the Game Hub store. Aghanim calls your server on every store visit and renders the store based on your response in real time.
This webhook is triggered in the following cases:
- A player logs in to the game hub (pre-fetch).
- A player opens the store.
- A player makes a purchase.


Integration modes
store.get can operate at three levels. Choose the one that matches your setup — deeper is not always better.
| Mode | SKU setup | What you control |
|---|---|---|
| Layer 1 | Stored in Aghanim dashboard | Which SKUs to show and in what order per player |
| Layer 2 | Stored in Aghanim dashboard | Override specific fields: price, name, image, bundle contents |
| Layer 3 | Fully dynamic — nothing stored in Aghanim | All fields sent in real time on every request |
Layer 1 is the lightest integration and the recommended starting point. SKUs and their visual configuration live in Aghanim; you return a personalized list and display order per player. All native Aghanim features work fully at this level.
Layer 2 is the right choice when the same SKU needs different parameters depending on user context — for example, progression-based pricing, purchase-history-aware bundle contents, or user-specific images. SKUs still exist in Aghanim; you only override the fields that need to change per player.
Layer 3 gives you full control and is suited for teams that already have a fully built LiveOps backend managing all monetization logic at the individual user level. The key signal is velocity: offers need to reflect in-game state that changes within hours. At this level, price and name are required for every item since Aghanim has no fallback configuration. Note that Layer 3 turns your infrastructure into a single point of failure for the hub — any backend degradation directly impacts store availability. See Limitations before choosing this mode.
When not to use store.get at all. If your goal is simply to mirror your in-game catalog on the hub, store.get is not the right tool. The hub's value comes from exclusivity and additional value for the player — not from replicating what's already available in-game. In practice, the top 10–15% of SKUs drive 80–90% of web shop revenue. A well-configured set of offers outperforms a full catalog.
Anonymous users. When is_anonymous: true in event_data, return 200 OK with an empty items array. Do not return 4xx or 5xx — this causes errors in the hub.
Requirements
To use the store.get webhook from Aghanim, configure your webhook server as follows:
- HTTPS endpoint, accepting POST webhook requests.
- Listen for events generated and signed by Aghanim.
- Respond with
2xxstatus codes to signal successful processing, and4xxor5xxfor denial or errors. - Respond within 500ms. Slower responses degrade the hub experience.
Configuration
Below are function templates for an endpoint that processes store.get webhooks, returning a personalized item list for each player:
- Python
- Ruby
- Node.js
- Go
import fastapi, hashlib, hmac, json, typing
from fastapi.responses import JSONResponse
app = fastapi.FastAPI()
@app.post("/webhook")
async def webhook(request: fastapi.Request) -> dict[str, typing.Any]:
secret_key = "<YOUR_S2S_KEY>" # Replace with your actual webhook secret key
raw_payload = await request.body()
payload = raw_payload.decode()
timestamp = request.headers["x-aghanim-signature-timestamp"]
received_signature = request.headers["x-aghanim-signature"]
if not verify_signature(secret_key, payload, timestamp, received_signature):
raise fastapi.HTTPException(status_code=403, detail="Invalid signature")
data = json.loads(payload)
event_type = data["event_type"]
event_data = data["event_data"]
if event_type == "store.get":
if event_data.get("is_anonymous"):
return {"items": []}
items = get_store_items(event_data["player_id"])
return {"items": items}
raise fastapi.HTTPException(status_code=400, detail="Unknown event type")
def verify_signature(secret_key: str, payload: str, timestamp: str, received_signature: str) -> bool:
signature_data = f"{timestamp}.{payload}"
computed_hash = hmac.new(secret_key.encode(), signature_data.encode(), hashlib.sha256)
computed_signature = computed_hash.hexdigest()
return hmac.compare_digest(computed_signature, received_signature)
def get_store_items(player_id: str) -> list[dict]:
# Placeholder logic for fetching store items for this player.
# In a real application, this function would interact with your database or merchandising system.
return [
{
"sku": "starter_bundle",
"price": 999,
"name": "Starter Bundle",
"card_type": "featured",
"category_slugs": ["special-offers"],
"nested_items": [
{
"sku": "coins",
"name": "Coins",
"quantity": 500,
"image_url": "https://cdn.example.com/coins.png",
}
],
}
]
require 'sinatra'
require 'json'
require 'openssl'
post '/webhook' do
secret_key = "<YOUR_S2S_KEY>" # Replace with your actual webhook secret key
payload = request.body.read
timestamp = request.env["HTTP_X_AGHANIM_SIGNATURE_TIMESTAMP"]
received_signature = request.env["HTTP_X_AGHANIM_SIGNATURE"]
unless verify_signature(secret_key, payload, timestamp, received_signature)
halt 403, "Invalid signature"
end
data = JSON.parse(payload)
event_type = data["event_type"]
event_data = data["event_data"]
if event_type == "store.get"
if event_data["is_anonymous"]
return { items: [] }.to_json
end
items = get_store_items(event_data["player_id"])
return { items: items }.to_json
end
halt 400, "Unknown event type"
end
def verify_signature(secret_key, payload, timestamp, received_signature)
signature_data = "#{timestamp}.#{payload}"
computed_signature = OpenSSL::HMAC.hexdigest('sha256', secret_key, signature_data)
OpenSSL.secure_compare(computed_signature, received_signature)
end
def get_store_items(player_id)
# Placeholder logic for fetching store items for this player.
# In a real application, this function would interact with your database or merchandising system.
[
{
sku: "starter_bundle",
price: 999,
name: "Starter Bundle",
card_type: "featured",
category_slugs: ["special-offers"],
nested_items: [
{
sku: "coins",
name: "Coins",
quantity: 500,
image_url: "https://cdn.example.com/coins.png"
}
]
}
]
end
const express = require('express');
const crypto = require('crypto');
const app = express();
app.post('/webhook', express.raw({ type: "*/*" }), async (req, res) => {
const secretKey = '<YOUR_S2S_KEY>'; // Replace with your actual webhook secret key
const rawPayload = req.body;
const timestamp = req.headers['x-aghanim-signature-timestamp'];
const receivedSignature = req.headers['x-aghanim-signature'];
if (!verifySignature(secretKey, rawPayload, timestamp, receivedSignature)) {
return res.status(403).send('Invalid signature');
}
const payload = JSON.parse(req.body);
const { event_type, event_data } = payload;
if (event_type === 'store.get') {
if (event_data.is_anonymous) {
return res.json({ items: [] });
}
const items = getStoreItems(event_data.player_id);
return res.json({ items });
}
return res.status(400).send('Unknown event type');
});
function verifySignature(secretKey, payload, timestamp, receivedSignature) {
const signatureData = `${timestamp}.${payload}`;
const computedSignature = crypto
.createHmac('sha256', secretKey)
.update(signatureData)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(computedSignature), Buffer.from(receivedSignature));
}
function getStoreItems(playerId) {
// Placeholder logic for fetching store items for this player.
// In a real application, this function would interact with your database or merchandising system.
return [
{
sku: 'starter_bundle',
price: 999,
name: 'Starter Bundle',
card_type: 'featured',
category_slugs: ['special-offers'],
nested_items: [
{
sku: 'coins',
name: 'Coins',
quantity: 500,
image_url: 'https://cdn.example.com/coins.png',
},
],
},
];
}
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
func webhookHandler(w http.ResponseWriter, r *http.Request) {
secretKey := "<YOUR_S2S_KEY>" // Replace with your actual webhook secret key
rawPayload, _ := ioutil.ReadAll(r.Body)
payload := string(rawPayload)
timestamp := r.Header.Get("X-Aghanim-Signature-Timestamp")
receivedSignature := r.Header.Get("X-Aghanim-Signature")
if !verifySignature(secretKey, payload, timestamp, receivedSignature) {
http.Error(w, "Invalid signature", http.StatusForbidden)
return
}
var data map[string]interface{}
if err := json.Unmarshal(rawPayload, &data); err != nil {
http.Error(w, "Invalid payload", http.StatusBadRequest)
return
}
eventType := data["event_type"].(string)
eventData := data["event_data"].(map[string]interface{})
if eventType == "store.get" {
if isAnonymous, _ := eventData["is_anonymous"].(bool); isAnonymous {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{"items": []interface{}{}})
return
}
playerID, _ := eventData["player_id"].(string)
items := getStoreItems(playerID)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{"items": items})
return
}
http.Error(w, "Unknown event type", http.StatusBadRequest)
}
func verifySignature(secretKey, payload, timestamp, receivedSignature string) bool {
signatureData := fmt.Sprintf("%s.%s", timestamp, payload)
mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write([]byte(signatureData))
computedSignature := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(computedSignature), []byte(receivedSignature))
}
func getStoreItems(playerID string) []map[string]interface{} {
// Placeholder logic for fetching store items for this player.
// In a real application, this function would interact with your database or merchandising system.
return []map[string]interface{}{
{
"sku": "starter_bundle",
"price": 999,
"name": "Starter Bundle",
"card_type": "featured",
"category_slugs": []string{"special-offers"},
"nested_items": []map[string]interface{}{
{
"sku": "coins",
"name": "Coins",
"quantity": 500,
"image_url": "https://cdn.example.com/coins.png",
},
},
},
}
}
- Develop a function for the
store.getwebhook processing. - Make your endpoint available.
- Register your endpoint within Aghanim account → Game → Webhooks → Add webhook by choosing the
store.getevent type.
Alternatively, you can register your endpoint within Aghanim using the Create Webhook API method.
Trigger values
| Value | Description |
|---|---|
hub.login | When the player opens the game hub. Pre-fetches the store on login. |
hub.store.open | When the store is opened. |
hub.purchase | When the player clicks the buy button — to verify the item is still available. |
order.captured | Just before payment processing begins — to verify the item is still available. |
test | When using the "Send test event" in the Dashboard. |
See the full events and triggers matrix for how store.get relates to other event types.
Request schema
Below is an example of an store.get webhook request:
- HTTP
- cURL
POST /your/webhook/uri HTTP/1.1
Content-Type: application/json
Host: your-webhook-endpoint.com
User-Agent: Aghanim/0.1.0
X-Aghanim-Signature: 2e45ed4dede5e09506717490655d2f78e96d4261040ef48cc623a780bda38812
X-Aghanim-Signature-Timestamp: 1725548450
{
"event_type": "store.get",
"event_data": {
"player_id": "2D2R-OP3C",
"is_anonymous": false,
"placement_keys": null,
"category_slugs": null,
"current_page_path": "/store",
"locale": "en"
},
"event_time": 1725548450,
"event_id": "whevt_eCacGbJVbvToOgzjXUgOCitkQE",
"idempotency_key": "idmpt_aXRlb...JkX2VFS",
"request_id": "d1593e9c-c291-4004-8846-6679c2e5810b",
"sandbox": false,
"trigger": "hub.store.open",
"transaction_id": "whtx_eCacGbJVbvT",
"context": null,
"game_id": "gm_exTAyxPsVwh"
}
curl "https://your-webhook-endpoint.com/your/webhook/uri" \
-X POST \
-H "Content-Type: application/json" \
-H "User-Agent: Aghanim/0.1.0" \
-H "X-Aghanim-Signature: 2e45ed4dede5e09506717490655d2f78e96d4261040ef48cc623a780bda38812" \
-H "X-Aghanim-Signature-Timestamp: 1725548450" \
-d '{
"event_type": "store.get",
"event_data": {
"player_id": "2D2R-OP3C",
"is_anonymous": false,
"placement_keys": null,
"category_slugs": null,
"current_page_path": "/store",
"locale": "en"
},
"event_time": 1725548450,
"event_id": "whevt_eCacGbJVbvToOgzjXUgOCitkQE",
"idempotency_key": "idmpt_aXRlb...JkX2VFS",
"request_id": "d1593e9c-c291-4004-8846-6679c2e5810b",
"sandbox": false,
"trigger": "hub.store.open",
"transaction_id": "whtx_eCacGbJVbvT",
"context": null,
"game_id": "gm_exTAyxPsVwh"
}'
The Event schema
| Key | Type | Description |
|---|---|---|
event_id | string | Unique Event ID generated by Aghanim. |
game_id | string | Your game ID in the Aghanim system. |
event_type | string | The type of the event, store.get in this case. |
event_time | number | Event date in Unix epoch time. |
event_data | EventData | Contains the event-specific data, with possible keys for inherited objects. |
idempotency_key | string | Ensures webhook actions are executed only once, even if retried. |
request_id | string|null | If the event was triggered by an API request, the request ID is included. |
sandbox | boolean | Indicates whether the event was sent from the sandbox game environment. |
trigger | string|null | The trigger that caused the event to be sent. |
transaction_id | string | The transaction ID generated by Aghanim. This ID may be the same for multiple events emitted within the same transaction. |
context | EventContext|null | Contextual information about the event. |
The EventContext schema
| Key | Type | Description |
|---|---|---|
order | OrderContext|null | Order information associated with the event if applicable. |
player | PlayerContext|null | (Optional) Player information. To add this, enable "Add player context" in the webhook settings. |
The EventData schema
| Key | Type | Description |
|---|---|---|
player_id | string | The unique Player ID chosen for player authentication. |
is_anonymous | boolean | Indicates whether the current user is unauthenticated (anonymous). |
placement_keys | string[]|null | List of placement keys from where store.get was requested. |
category_slugs | string[]|null | List of category slugs from where store.get was requested. |
current_page_path | string|null | Path of the currently opened page, relative to the game hub root (for example, /store/offers). |
locale | string | The current player's locale code (ISO 639-1). Find the full list of possible locales in Locales. |
Response schema
Return 200 OK with the following JSON payload.
| Key | Type | Description | Required? |
|---|---|---|---|
items | [Item|BundleItem] | Items available for the player in the store. | No |
rolling_offers | RollingOffer[] | Rolling offers available for the player. | No |
The Item schema
Use Item for standalone products (currencies, equipment, consumables).
| Key | Type | Description | Required? |
|---|---|---|---|
sku | string | Unique SKU identifier. For Layer 1 and 2: must match a SKU in the Aghanim dashboard. For Layer 3 (fully dynamic): price and name are also required. | Yes |
price | number | Price in USD cents. Required for Layer 3. | No |
name | string | Item name. Required for Layer 3. | No |
is_stackable | boolean | Whether the item can be purchased multiple times and stacks in the player's inventory. | No |
quantity | number | Item quantity. | No |
description | string | Item description. | No |
image_url | string | Main image URL. | No |
background_image_url | string | Background image URL. | No |
background_image_color | string | Background color. | No |
image_url_featured | string | Image URL when displayed as a featured item. | No |
card_background_image_url | string | Background image URL of the item card. | No |
category_slugs | string[] | Category slugs that determine where the item appears in the store. Must match categories configured in the Aghanim dashboard. | No |
start_at | number | Unix timestamp when the item becomes available. | No |
end_at | number | Unix timestamp when the item expires. | No |
max_purchases | number | Maximum number of purchases allowed. Once reached, the item disappears from the store. | No |
current_purchases | number | Current number of purchases by this player. | No |
price_template_id | string | ID of the price template for country-specific pricing. Use the internal ID from /v1/price_templates (e.g. ptm_eGhmGhfwjVL), not the template name. | No |
custom_badge | string | Custom badge text. | No |
bonus_items | WebhookItemBonus[]|Item[] | Bonus items included with this item. | No |
bonus_badge | string | Badge text describing the bonus. | No |
bonus_percent | number | Bonus percent applied to the first stackable item in nested_items. | No |
bonus_fixed | number | Fixed bonus value applied to the first stackable item in nested_items. Overrides bonus_percent if set. | No |
reward_points_fixed | number | Fixed number of Reward Points awarded on purchase. Overrides reward_points_percent if set. | No |
reward_points_percent | number | Percentage of Reward Points awarded on purchase. | No |
metadata | object | Key-value pairs for additional data. | No |
view_option | ItemViewOption | Store appearance: default, in_title. | No |
card_type | StoreCardType | Card display type: default, featured. | No |
free_claims | FreeClaims | Settings to make the item claimable for free. | No |
show_disabled_by_max_purchases | boolean | Whether to show the item as disabled after max purchases is reached. | No |
The BundleItem schema
Use BundleItem for products that contain multiple items. Identical to Item with the addition of nested_items.
| Key | Type | Description | Required? |
|---|---|---|---|
sku | string | Unique SKU identifier. For Layer 3: price and name are also required. | Yes |
price | number | Price in USD cents. Required for Layer 3. | No |
name | string | Bundle name. Required for Layer 3. | No |
nested_items | NestedItem[] | Items included in the bundle. | No |
description | string | Bundle description. | No |
image_url | string | Bundle image URL. | No |
background_image_url | string | Background image URL. | No |
background_image_color | string | Background color. | No |
image_url_featured | string | Image URL when displayed as featured. | No |
category_slugs | string[] | Category slugs for store placement. Must match categories in the Aghanim dashboard. | No |
start_at | number | Unix timestamp when the bundle becomes available. | No |
end_at | number | Unix timestamp when the bundle expires. | No |
max_purchases | number | Maximum purchases allowed. | No |
price_template_id | string | Internal ID of the price template (e.g. ptm_eGhmGhfwjVL). | No |
custom_badge | string | Custom badge text. | No |
bonus_items | WebhookItemBonus[]|Item[] | Bonus items included with the bundle. | No |
bonus_badge | string | Badge text describing the bonus. | No |
bonus_percent | number | Bonus percent applied to the first stackable item in nested_items. | No |
bonus_fixed | number | Fixed bonus value applied to the first stackable item in nested_items. Overrides bonus_percent if set. | No |
reward_points_fixed | number | Fixed Reward Points awarded on purchase. Overrides reward_points_percent if set. | No |
reward_points_percent | number | Percentage of Reward Points awarded on purchase. | No |
metadata | object | Key-value pairs for additional data. | No |
view_option | ItemViewOption | Store appearance: default, in_title. | No |
card_type | StoreCardType | Card type: default, featured. | No |
free_claims | FreeClaims | Settings to make the bundle claimable for free. | No |
show_disabled_by_max_purchases | boolean | Show as disabled after max purchases is reached. | No |
The NestedItem schema
Items inside a bundle. name and image_url are technically optional but required in practice for correct display — without name, the bundle renders without nested items in the hub.
| Key | Type | Description | Required? |
|---|---|---|---|
sku | string | Item SKU matching on both the game and Aghanim sides. | Yes |
name | string | Item name, used for display inside the bundle card. Required in practice — bundle renders without nested items if omitted. | Effectively yes |
quantity | number | Item quantity. Relevant for stackable items. | No |
image_url | string | Item image URL, used for display inside the bundle card. Required in practice for correct visual rendering. | Effectively yes |
background_image_url | string | Background image for this item inside the bundle card. | No |
is_featured | boolean | Whether this item is featured in the bundle list. | No |
metadata | object | Key-value pairs for additional data. | No |
The WebhookItemBonus schema
| Key | Type | Description | Required? |
|---|---|---|---|
sku | string | Item SKU matching on both the game and Aghanim sides. | Yes |
quantity | number | Quantity of the bonus item. Relevant for stackable items. | No |
The FreeClaims schema
| Key | Type | Description | Required? |
|---|---|---|---|
enabled | boolean | Whether the item can be claimed for free. | Yes |
max_claims | number | Maximum number of times the item can be claimed within the period. | Yes |
period | Period | Period window used to reset max_claims. If omitted, the limit does not reset. | No |
exceeded_claims_behavior | ExceededClaimsBehavior | Behavior after max claims is reached: hide — removes from store; disable_with_timer — keeps visible but disabled with countdown. | No |
The Period schema
| Key | Type | Description | Required? |
|---|---|---|---|
unit | PeriodType | Period type: month, week, day, hour. | Yes |
duration | number | Duration in the specified units (e.g. duration: 2, unit: day = two-day period). | Yes |
The RollingOffer schema
Rolling offers require a dedicated block placed on a hub page. The placement_key must match the key of that block.
| Key | Type | Description | Required? |
|---|---|---|---|
key | string | Unique offer key used to track purchase or claim state. | Yes |
placement_key | string | Key of the hub block where the rolling offer will be displayed. | Yes |
name | string | Rolling offer title. | Yes |
description | string | Rolling offer description. | Yes |
rolling_items | RollingItem[] | Items included in the rolling offer. | Yes |
background_image_url | string | Background image URL. | No |
background_size | string | Background image size: contain, repeat, cover. | No |
expire_at | number | Expiration Unix timestamp. The hub shows a countdown and hides the offer when reached. | No |
The RollingItem schema
| Key | Type | Description | Required? |
|---|---|---|---|
sku | string | Unique SKU identifier for the dynamic bundle item. | Yes |
quantity | number | Item quantity. Relevant for stackable items. | No |
is_free_item | boolean | Whether the item can be claimed for free. | No |
Example responses
Layer 1 — return SKU list only
{
"items": [
{ "sku": "crystals" },
{ "sku": "shield" },
{ "sku": "starter_bundle" }
]
}
Layer 2 — override specific fields
{
"items": [
{
"sku": "starter_bundle",
"price": 999,
"nested_items": [
{ "sku": "crystals", "quantity": 200 },
{ "sku": "shield", "quantity": 1 }
]
}
]
}
Layer 3 — fully dynamic
{
"items": [
{
"sku": "fly_bundle",
"price": 2000,
"name": "Fly Bundle",
"description": "Fly Bundle Description",
"image_url": "https://example.com/fly-bundle.png",
"card_type": "featured",
"card_background_image_url": "https://example.com/bg.png",
"category_slugs": ["special-offers"],
"start_at": 1630000000,
"end_at": 1635000000,
"max_purchases": 1,
"nested_items": [
{
"sku": "crystals",
"name": "Crystals",
"quantity": 100,
"image_url": "https://example.com/crystals.png"
},
{
"sku": "shield",
"name": "Shield",
"quantity": 1,
"image_url": "https://example.com/shield.png"
}
]
}
]
}
Anonymous user
{
"items": []
}
Limitations
Layer 2 and Layer 3 have compatibility gaps with several Aghanim-native features.
| Feature | Layer 1 | Layer 2 | Layer 3 |
|---|---|---|---|
| Abandoned Cart retargeting | ✅ | ⚠️ Limited | ❌ |
| Insufficient Funds campaigns | ✅ | ⚠️ Limited | ❌ |
| Loyalty Program | ✅ | ❌ | ❌ |
| Piggy Bank | ✅ | ❌ | ❌ |
| Sequential Offers | ✅ | ❌ | ❌ |
| Dynamic discounts / bonus layering | ✅ | ⚠️ Limited | ⚠️ Limited |
If you rely on these features, use Layer 1 or a hybrid approach (static SKUs in Aghanim + Layer 1 personalization).
Need help? Contact our integration team at integration@aghanim.com
Need help?
Contact our integration team at integration@aghanim.com