Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Store and in-app purchases

This page describes the store showcase the player sees, the flow of buying with game money and the verification of in-app purchases (IAP) made with real money. Read it if you want to build a showcase, sell products and complete payment safely on the server.

What it does

The store module (store) ties three things together:

  • Showcase (page) and product (listing): The catalog an operator sets up in the panel and that is shown to the player.
  • Payment: A product is sold either with game money (currency: through a merchant, money in exchange for an item) or with real money (iap: an App Store or Google Play receipt).
  • Reservation: A purchase first reserves an entitlement (stock and the per-player limit), then completes the payment in one transaction. Payment and delivery either both happen or neither does.

If the module is disabled, the endpoints return 404 with {"code":"module_disabled","module":"store"}. Enable the module on the Game settings › Modules page.

Where it lives in the panel

What you want to doPanel page
Create showcases and products; set price, stock, visibility window and per-player limitStore, Product
Define the currency for a priceCurrencies
Define the item to be soldContent

In the panel, IAP products and receipts are only listed. For attaching an IAP product to the store and for the platform credentials (App Store / Google Play), see Limits and known limitations.

Note

Panel help pages are not translated yet; the panel links above point to the Turkish source.

Data model (in plain words)

  • Showcase (page): key (lowercase letters, digits, _, -; at most 64), title, sort_order and visibility. Showcase fields such as the description, the banner image, the product title, the badge and the image are entered in the panel; the server-key REST draft does not carry them.
  • Product (listing): Attached to a showcase. The payment form is currency (merchant + item definition + currency + price_amount) or iap (an IAP product ID). limit_per_player (at least 1) is the lifetime per-player quantity limit.
  • Visibility: active, a date window (starts_at/ends_at, Unix ms) and an optional CEL visibility_rule (a player-specific condition; compiled at registration, at most 8192 bytes). The player showcase is recomputed on every read: the condition, the time and the remaining limit are evaluated against the state at that moment.
  • Catalog version (version): Every change made to a showcase increases the catalog version; on management commands If-Match is this version.
  • Product version: The version field of the product the player sees; it is sent back as expected_version on purchase.
  • Reservation: Its status is reserved, processing, completed, cancelled. reserved and processing do not count as payment.
  • Price: An integer in the smallest unit; it comes with display_decimals (0–4) in the showcase. Build the display with integer/string operations; do not convert it to a decimal number.

Calls your game makes

There are two credential types:

  • Player session (cookie or session token + x-crx-env: <environment uuid>): reads the showcase and buys on its own behalf. With a cookie session, mutations require the CSRF double-submit (x-csrf-token) and the Origin check; see Sessions and CSRF.
  • Server key (Authorization: Bearer crx_live_<prefix>_<secret>): catalog management, player preview, reservation and payment completion.
Method and pathCredentialWhat it does
GET /v1/store/pages[?page=<key>&at=now]player sessionThe showcases the player can see; page narrows to one showcase
POST /v1/player/store/listings/{listing_id}/buyplayer sessionThe player buys on their own behalf
GET /v1/store/catalogserver keyThe raw management catalog (visibility is not filtered)
GET /v1/store/preview/{player_id}[?at=now]server keyPreviews the showcase a player would see
POST /v1/store/pages, PUT/DELETE /v1/store/pages/{id}, POST /v1/store/pages/reorderserver keyShowcase management
POST /v1/store/pages/{page_id}/listings, PUT/DELETE /v1/store/listings/{id}, POST …/listings/reorderserver keyProduct management
POST /v1/store/listings/{listing_id}/reserveserver keyReserves an entitlement for a player (not a payment)
POST /v1/store/reservations/{id}/checkout/currencyserver keyCompletes payment with game money
POST /v1/store/reservations/{id}/checkout/iapserver keyCompletes payment with a verified IAP
POST /v1/store/reservations/{id}/cancelserver keyCancels a reserved reservation (not a refund)
POST /v1/iap/verifyAPI keyVerifies an IAP receipt and grants the reward
POST /v1/webhooks/iap/{platform}/{environment_id}no key (signature)Receives the platform’s refund notification

Error envelopes: errors from the management and reservation endpoints are text/plain; the player purchase endpoint returns JSON with a typed code (below). General list: Errors.

Reading the showcase

GET /v1/store/pages?page=garage
x-crx-env: 0192f000-0000-7000-8000-00000000e001
{
  "version": 12,
  "pages": [
    {
      "id": "…", "key": "garage", "title": "Garage",
      "listings": [
        {
          "id": "0192f111-…", "version": 3,
          "title": "Fast Car", "badge": "sale",
          "price": { "currency_key": "gold", "currency_name": "Gold", "amount": 1250, "display_decimals": 0, "icon_url": null },
          "item": { "key": "fast_car", "name": "Fast Car" },
          "remaining": 40
        }
      ]
    }
  ]
}

The response carries Cache-Control: private, no-store. A page key that is not visible or unknown returns 404. The player identity, the time and the CEL result are not taken from the client; adding player_id or any other field to the query returns 400.

A player’s purchase

POST /v1/player/store/listings/0192f111-…/buy
x-crx-env: 0192f000-0000-7000-8000-00000000e001
x-csrf-token: <csrf>
Idempotency-Key: buy-player-1234-0001
Content-Type: application/json

{ "quantity": 1, "expected_version": 3 }
{
  "reservation_id": "…",
  "payment_reference": "…",
  "item_instance_ids": ["…"],
  "balance_after": 750,
  "replayed": false
}

The body contains only quantity (positive) and expected_version; a player ID, a price or a payment claim is not accepted, and an unknown field returns 400. The identity comes from the session. Reservation and charging are in a single transaction. Idempotency-Key is required: the same key and the same body repeat the durable result with replayed: true and make no second charge; the same key with a different body is 422. An IAP product cannot be bought on this endpoint (422).

Rejected purchases carry a typed JSON code, {"code": "...", "message": "..."}:

codeHTTPMeaning
not_visible404The product is not visible to this player or does not exist
listing_changed409The product changed while you were looking (expected_version is stale); refresh the showcase
out_of_stock422Out of stock
limit_reached422The per-player limit is full
insufficient_funds422The balance is insufficient
inventory_full422The inventory is full; the operation is rolled back completely

Other errors (401, 403 CSRF/Origin, 400, 429 budget, 500) follow the general rules.

Server-side flow: reservation and payment

In cases where the game server manages the payment (for example, when your server receives the real-money receipt), use two steps:

POST /v1/store/listings/{listing_id}/reserve
Authorization: Bearer crx_live_<prefix>_<secret>
Idempotency-Key: reserve-player-1234-0001
If-Match: 12
Content-Type: application/json

{ "player_id": "0192f0f2-…", "quantity": 1 }

The response has the form { catalog_version, entity_id, event_id, event, reservation, replayed }; reservation.status is reserved. Then:

POST /v1/store/reservations/{id}/checkout/currency
Authorization: Bearer crx_live_<prefix>_<secret>
Content-Type: application/json

{ "player_id": "0192f0f2-…" }
{ "reservation_id": "…", "payment_reference": "…", "kind": "currency", "replayed": false }

Idempotency-Key and If-Match are not sent on the checkout endpoints (400 if sent); the durable reservation ID is the anchor of replay safety. If there is an error when paying with a currency, the operation is rolled back and the reservation stays reserved; you can retry or cancel. If the outcome of an IAP payment is uncertain, the reservation becomes processing and the player limit is preserved; retry with the same credential. In an IAP payment quantity is 1.

Idempotency-Key is required on reserve and the catalog management commands; If-Match is the catalog version and is optional (a mismatch is 409). A failed command does not burn the key.

IAP verification

POST /v1/iap/verify
Authorization: Bearer crx_live_<prefix>_<secret>
Idempotency-Key: iap-player-1234-tx-abc
Content-Type: application/json

{ "platform": "app_store", "product_sku": "gems_500", "credential": "<transactionId>", "player_id": "0192f0f2-…" }
{ "receipt_id": "…", "status": "granted", "product_sku": "gems_500", "transaction_id": "…", "grants": [ … ], "replayed": false }
  • platform: app_store or google_play. credential: the transactionId for the App Store, the purchaseToken for Google Play. The server queries the platform itself; the client is not trusted.
  • Verifying the same transaction twice does not grant a second reward (200, replayed: true).
  • status can be verified, granted, revoked or failed; the happy path is granted.
  • Errors: a forged receipt 403; the product is not in the catalog or the transaction is not recognized on the platform 404; a product_sku mismatch, a platform that is not configured or a supply cap overrun 422; another request in flight with the same key 409.
  • Refund notification: App Store (ASN V2) and Google Play (RTDN) notifications arrive at POST /v1/webhooks/iap/{platform}/{environment_id}; the endpoint is keyless and authentication is the signature. On a refund of a known receipt the reward is taken back with reversing entries and the receipt becomes revoked; cases that can only be partially reversed are marked flagged (the default policy; it does not touch the consumed part). Non-refund notifications and unknown transactions get 200 and are ignored.

Versioning and replay safety

  • Player purchase: expected_version (the product version) + Idempotency-Key.
  • Catalog management and reservation: Idempotency-Key required, If-Match = the catalog version.
  • Payment completion: the durable reservation ID; no header.
  • General rules: Idempotency and concurrency.

Events and webhooks

Store events can be delivered as webhooks: store_page_created, store_page_updated, store_page_deleted, store_pages_reordered, store_listing_created, store_listing_updated, store_listing_deleted, store_listings_reordered, store_purchase_initiated, store_payment_started, store_purchase_completed, store_reservation_cancelled. The envelope contains event_id, environment_id, type, seq, now_ms, actor_api_key_id and data. Registration and verification: Webhooks.

Limits and known limitations

  • A showcase key is ^[a-z0-9_-]{1,64}$; at most 2000 entries in a reorder request.
  • An IAP product is not supported on the player purchase endpoint; IAP products are bought with checkout/iap or iap/verify.
  • IAP product registration: In the panel, IAP products and receipts are read-only, and there is no public HTTP endpoint yet to save a product. For now, adding an IAP product is done through the Open Play operator.
  • Platform credentials: The App Store and Google Play verification credentials are defined by the operator at deployment. If they are not defined, iap/verify and checkout/iap return 422.
  • Read the showcase when the player opens or refreshes it; changes are recomputed at read time.

Common errors

SymptomCauseFix
409 listing_changedThe product version you read in the showcase went staleRe-read the showcase and try with the new version
404 not_visibleThe product is not visible to the player (window, condition, inactive)Refresh the showcase; check the condition in the panel
422 insufficient_fundsThe balance is insufficientShow the balance; give money through a reward or IAP
400 (unknown field)An extra field was added to the purchase bodySend only quantity and expected_version
400 (checkout)Idempotency-Key/If-Match was sentRemove these headers on the checkout endpoints
403 (purchase)The CSRF pair or the Origin was rejectedRefresh the CSRF token; see Sessions and CSRF
403 (iap/verify)A forged receipt or one with an invalid signatureGet the receipt again from the platform
422 (iap)The platform is not configured or product_sku does not matchContact the operator; check the sku

Working example

An example that reads the showcase and buys the first product is on the Economy page; it shows a separate message per error code (storeBuyErrorCode). The TypeScript SDK provides the player.store.catalog() and player.store.buy() helpers; if Idempotency-Key is not given, it generates one per call, and after a network interruption you must supply the same key yourself.