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

Inventory and items

This page describes how to define your game’s items (potions, weapons, cosmetics…), give them to players, consume them, transfer them and place them in the inventory. Read it if you want inventory state to be held safely on the server.

What it does

The items module (items) manages two separate things:

  • Item definition: A kind such as “Small health potion”. Its key, kind, whether it is stackable, whether it is tradable and the NPC buy/sell price live here.
  • Item instance: The real item in a player’s hands. It carries the definition version it was born with, its owner, its quantity and its customization data (metadata).

An inventory is the placement of an owner’s items. The inventory only holds which item is in which slot; quantity and customization live on the item instance.

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

Where it lives in the panel

What you want to doPanel page
Edit item definitions and game data (the built-in item group)Content
Attach a 3D model to an item3D models, Model profile
See a player’s inventory and walletPlayer profile
Sell an item in the storeStore

Note

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

Data model (in plain words)

  • Kind (kind): consumable, equipment, material, cosmetic, vip_card.
  • Stackable (stackable) and max_stack: For a stackable item, instances from the same definition version with empty metadata merge into a single row. If stackable: true, max_stack is required (between 1 and 2147483647).
  • tradeable: If false, the item cannot be transferred to another owner (422). The default is true.
  • npc_buy_price, npc_sell_price: An integer in the smallest unit. If null, buying/selling with a merchant is closed.
  • Version pinning: Each instance carries the definition version it was born with (definition_version). Changing a definition later does not change the rules of old instances.
  • Owner (owner): The form <kind>:<uuid>; the kind can be player, character, clan or system. Example: player:0192f0f2-5b1a-7c3e-9d11-4a5b6c7d8e9f.
  • Inventory: Opened by itself on the first command (default capacity 32). The capacity is the upper bound on the number of occupied slots; a slot number can be any number 0 or greater. Reading an inventory that has never been opened returns 404 and does not open it.
  • version: Both the item instance and the inventory have their own version; it grows with every change and is used with If-Match.

Calls your game makes

Item and inventory endpoints are called with an API key:

Authorization: Bearer crx_live_<prefix>_<secret>

The player side offers two read endpoints; they are called with a player session (a cookie or session token, with the x-crx-env: <environment uuid> header):

Method and pathCredentialWhat it does
POST /v1/item-definitionsserver keySaves a definition as a new version (game data can also be written with the content field)
GET /v1/item-definitions/{key}API keyReads the current definition version
POST /v1/itemsserver keyCreates (mints) an item
GET /v1/items?owner=player:<uuid>API keyLists the owner’s items (cursor, limit)
GET /v1/items/{id}API keyReads a single instance
POST /v1/items/{id}/consumeAPI keyConsumes quantity
POST /v1/items/{id}/destroyserver keyDestroys the item
POST /v1/items/{id}/transferAPI keyTransfers ownership
GET /v1/inventories/{owner}API keyReads the inventory and the occupied slots
POST /v1/inventories/{owner}/moveAPI keyPlaces or moves an item into a slot
POST /v1/inventories/{owner}/splitAPI keySplits a quantity off a stack into a new slot
POST /v1/inventories/{owner}/stackAPI keyMerges two stacks
POST /v1/inventories/{owner}/equipAPI keyPuts an item into an equip slot
POST /v1/inventories/{owner}/unequipAPI keyMoves an equipped item down to another slot
GET /v1/player/inventoryplayer sessionThe player’s own inventory (read-only)
GET /v1/player/itemsplayer sessionThe player’s own items (read-only)
GET /v1/catalog/items, GET /v1/catalog/items/{key}server key or player sessionThe item catalog (below)

Note

The TypeScript SDK has no typed client for items and inventory; these endpoints are called with raw REST. There is no SDK helper yet for reading with a player session (/v1/player/*) either. Coverage table: TypeScript SDK.

Warning

Known limitation: The consume, transfer, move, split, stack, equip and unequip endpoints do not look at the key type; a valid client key can call them too, and owner or the item ID is given freely in the request. A client key is scoped to the environment, not to the player. Verify on your own server that a player touches only their own items; against the risk of cheating, make these calls from the game server. mint and destroy work only with a server key.

Minting an item

POST /v1/items
Authorization: Bearer crx_live_<prefix>_<secret>
Content-Type: application/json
Idempotency-Key: quest-17-potion-player-1234

{
  "definition_key": "health_potion",
  "owner": "player:0192f0f2-5b1a-7c3e-9d11-4a5b6c7d8e9f",
  "quantity": 3,
  "source": "quest"
}
{
  "instance": {
    "id": "0192f1aa-1111-7222-8333-944455556666",
    "definition_id": "0192f0aa-0000-7000-8000-000000000001",
    "definition_version": 2,
    "owner_type": "player",
    "owner_id": "0192f0f2-5b1a-7c3e-9d11-4a5b6c7d8e9f",
    "quantity": 3,
    "metadata": {},
    "version": 1,
    "created_event_id": "0192f1aa-2222-7333-8444-955566667777",
    "created_at": "2026-11-03T09:00:00Z",
    "updated_at": "2026-11-03T09:00:00Z"
  },
  "event": { "type": "item_instance_created" },
  "replayed": false
}
  • quantity is between 1 and 2147483647.
  • source can be mint, craft, purchase, quest or grant; if not given, mint.
  • If metadata is non-empty, the item does not stack and is always a single row.
  • For a stackable definition, if the owner already holds a stack of the same version with empty metadata, no new row is opened and the quantity is added to that stack. If max_stack is exceeded, 422.

Consuming, destroying, transferring

EndpointBodyNote
POST /v1/items/{id}/consume{"amount": 1}amount is between 1 and the current quantity; exceeding it is 422. If the quantity drops to zero, the row is deleted and the next GET returns 404. The item kind is not checked.
POST /v1/items/{id}/destroy{"amount": 1}Server key only. The same rule applies; past events are not deleted.
POST /v1/items/{id}/transfer{"to_owner": "player:<uuid>"}422 if tradeable: false.

Idempotency-Key is required on all three of these endpoints (400 if absent). If-Match is optional and is the item instance’s version value.

Inventory placement

Placement commands have the same shape:

POST /v1/inventories/player:0192f0f2-5b1a-7c3e-9d11-4a5b6c7d8e9f/move
Authorization: Bearer crx_live_<prefix>_<secret>
Content-Type: application/json
Idempotency-Key: move-1
If-Match: 4

{ "item_instance_id": "0192f1aa-1111-7222-8333-944455556666", "to_slot": 5 }
EndpointBody
moveitem_instance_id, to_slot
equip, unequipitem_instance_id, to_slot
splitfrom_instance_id, amount, to_slot
stackfrom_instance_id, to_instance_id

The response has the form { "inventory": {…}, "slots": [{ "slot_idx": 5, "item_instance_id": "…", "created_at": "…" }], "event": {…}, "replayed": false }.

Rules:

  • Placing into an occupied slot is 422. When placing a new item, exceeding the capacity is also 422; moving an already placed item does not consume capacity.
  • If-Match is the inventory’s version. If you send If-Match on the first open (while the inventory does not exist yet), you get 400.
  • If the same item is already placed in another inventory, 409 (double-placement lock).
  • split: amount is between 1 and one less than the source quantity; the item must be stackable. The new instance’s ID is returned in the event in the response (new_instance_id).
  • stack: both stacks must be placed; they must be of the same definition version and have empty metadata.
  • equip: if the definition’s metadata has an equip_condition (a CEL expression), equipping depends on this condition. If the condition or its evaluation errors, equipping is rejected (422). In the condition the character context is empty for now, and now and seed are a constant 0.

Item catalog

The game client reads the item definition, the typed data rows and the model address from the catalog. There are two audiences: a player sees only rows with player visibility; a server key sees all rows, with each row’s visibility and with content_revision. Responses carry an ETag; with If-None-Match, 304 is returned if nothing changed. Details and a watching example: Content groups and Scenes, assets and 3D models.

Versioning and replay safety

  • Idempotency-Key is required on mint, consume, destroy, transfer and all inventory commands. If the same key and the same body arrive a second time, the operation does not run again; the response returns with replayed: true. The same key with a different body is 422. The key is burned only on success.
  • If-Match prevents concurrent writes (a mismatch is 409).
  • General rules: Idempotency and concurrency.

Events

Item events (item_instance_created, item_ownership_changed, item_consumed, item_stacks_merged, item_stack_split) can be used in rule automation and quest objectives. There is no item event among the event types you can select as a filter for a webhook endpoint. An endpoint registered without a filter (empty event_types) receives all events and so receives item events too; the envelope format of these events is not a documented contract, so do not rely on it. If you need to announce an item change to the outside, read it from your own server. Selectable types: Webhook events.

Limits

  • limit is 1–200 on list endpoints; pagination is cursor-based.
  • The default inventory capacity is 32 occupied slots.
  • quantity, amount and prices are integers; there are no decimals.

Common errors

SymptomCauseFix
400 (no key)The Idempotency-Key header is missingSend a key on every write
403mint/destroy/definition registration with a client keyUse a server key
404 (inventory)The inventory was never openedThe first inventory command (for example move) opens it; a read does not
409If-Match is stale or the item is in another inventoryRe-read and try again
422 insufficient quantityamount is greater than the current quantityReduce the quantity
422 tradetradeable: falseEnable trading on the definition
422 slot occupiedThe target slot is occupiedChoose an empty slot

Working example

An example that gives a potion and consumes it with raw REST (it uses fetch because the SDK has no typed client):

/** Envanter ve eşya uçları için SDK'da tipli istemci yoktur; ham REST ile çağrılır (sunucu anahtarı). */

/** Küçük yardımcı: Bearer + Idempotency-Key ile JSON POST. */
async function post(baseUrl: string, key: string, path: string, idempotencyKey: string, body: unknown, ifMatch?: number): Promise<unknown> {
  const headers: Record<string, string> = {
    Authorization: `Bearer ${key}`,
    "Content-Type": "application/json",
    "Idempotency-Key": idempotencyKey,
  };
  if (ifMatch !== undefined) headers["If-Match"] = String(ifMatch);
  const response = await fetch(`${baseUrl}${path}`, { method: "POST", headers, body: JSON.stringify(body) });
  if (!response.ok) throw new Error(`${path}: HTTP ${response.status} ${await response.text()}`);
  return response.json();
}

/** Oyuncuya bir iksir verir (mint, yalnız server anahtarı) ve 1 adedini tüketir. */
export async function grantAndDrink(baseUrl: string, serverKey: string, playerId: string): Promise<void> {
  const minted = (await post(baseUrl, serverKey, "/v1/items", `mint-${playerId}-potion-1`, {
    definition_key: "health_potion",
    owner: `player:${playerId}`,
    quantity: 3,
  })) as { instance: { id: string; version: number } };

  await post(
    baseUrl,
    serverKey,
    `/v1/items/${minted.instance.id}/consume`,
    `drink-${minted.instance.id}-1`,
    { amount: 1 },
    minted.instance.version, // If-Match: okuduğunuz sürüm; uyuşmazlık 409
  );
}