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 do | Panel page |
|---|---|
| Edit item definitions and game data (the built-in item group) | Content |
| Attach a 3D model to an item | 3D models, Model profile |
| See a player’s inventory and wallet | Player profile |
| Sell an item in the store | Store |
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) andmax_stack: For a stackable item, instances from the same definition version with emptymetadatamerge into a single row. Ifstackable: true,max_stackis required (between 1 and2147483647). tradeable: Iffalse, the item cannot be transferred to another owner (422). The default istrue.npc_buy_price,npc_sell_price: An integer in the smallest unit. Ifnull, 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 beplayer,character,clanorsystem. 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
404and does not open it. version: Both the item instance and the inventory have their own version; it grows with every change and is used withIf-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 path | Credential | What it does |
|---|---|---|
POST /v1/item-definitions | server key | Saves a definition as a new version (game data can also be written with the content field) |
GET /v1/item-definitions/{key} | API key | Reads the current definition version |
POST /v1/items | server key | Creates (mints) an item |
GET /v1/items?owner=player:<uuid> | API key | Lists the owner’s items (cursor, limit) |
GET /v1/items/{id} | API key | Reads a single instance |
POST /v1/items/{id}/consume | API key | Consumes quantity |
POST /v1/items/{id}/destroy | server key | Destroys the item |
POST /v1/items/{id}/transfer | API key | Transfers ownership |
GET /v1/inventories/{owner} | API key | Reads the inventory and the occupied slots |
POST /v1/inventories/{owner}/move | API key | Places or moves an item into a slot |
POST /v1/inventories/{owner}/split | API key | Splits a quantity off a stack into a new slot |
POST /v1/inventories/{owner}/stack | API key | Merges two stacks |
POST /v1/inventories/{owner}/equip | API key | Puts an item into an equip slot |
POST /v1/inventories/{owner}/unequip | API key | Moves an equipped item down to another slot |
GET /v1/player/inventory | player session | The player’s own inventory (read-only) |
GET /v1/player/items | player session | The player’s own items (read-only) |
GET /v1/catalog/items, GET /v1/catalog/items/{key} | server key or player session | The 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,equipandunequipendpoints do not look at the key type; a valid client key can call them too, andowneror 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.mintanddestroywork 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
}
quantityis between 1 and2147483647.sourcecan bemint,craft,purchase,questorgrant; if not given,mint.- If
metadatais 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. Ifmax_stackis exceeded,422.
Consuming, destroying, transferring
| Endpoint | Body | Note |
|---|---|---|
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 }
| Endpoint | Body |
|---|---|
move | item_instance_id, to_slot |
equip, unequip | item_instance_id, to_slot |
split | from_instance_id, amount, to_slot |
stack | from_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 also422; moving an already placed item does not consume capacity. If-Matchis the inventory’s version. If you sendIf-Matchon the first open (while the inventory does not exist yet), you get400.- If the same item is already placed in another inventory,
409(double-placement lock). split:amountis 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 emptymetadata.equip: if the definition’smetadatahas anequip_condition(a CEL expression), equipping depends on this condition. If the condition or its evaluation errors, equipping is rejected (422). In the condition thecharactercontext is empty for now, andnowandseedare a constant0.
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-Keyis 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 withreplayed: true. The same key with a different body is422. The key is burned only on success.If-Matchprevents concurrent writes (a mismatch is409).- 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
limitis 1–200 on list endpoints; pagination is cursor-based.- The default inventory capacity is 32 occupied slots.
quantity,amountand prices are integers; there are no decimals.
Common errors
| Symptom | Cause | Fix |
|---|---|---|
400 (no key) | The Idempotency-Key header is missing | Send a key on every write |
403 | mint/destroy/definition registration with a client key | Use a server key |
404 (inventory) | The inventory was never opened | The first inventory command (for example move) opens it; a read does not |
409 | If-Match is stale or the item is in another inventory | Re-read and try again |
422 insufficient quantity | amount is greater than the current quantity | Reduce the quantity |
422 trade | tradeable: false | Enable trading on the definition |
422 slot occupied | The target slot is occupied | Choose 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
);
}