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

Characters

This page explains how the character a player plays is defined, created and changed in Open Play. Read it if you want to keep character state (attributes, resources, XP, level, abilities, flags) on the server and change it with cheat-proof commands.

What it does

The character module (characters) keeps player-bound character state in one trustworthy place. State changes only through commands, and every change is recorded as an immutable event. This lets you read back a character’s state at any point in time and undo a faulty change with a new event.

If the module is disabled, every endpoint on this page returns 404 with {"code":"module_disabled","module":"characters"}. Enable the module on the Game settings › Modules page.

Where it lives in the panel

What you want to doPanel page
Define a character template, a trait and a level curveCharacter templates
List characters, open one and see its state and historyCharacters, Character profile
See a player’s account and characters togetherPlayers, Player profile
Write event-driven automationGame rules

Note

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

Data model (in plain words)

  • Trait: A reusable bundle of fields. For example, a “warrior” trait brings strength and health fields. A template combines several traits.
  • Template (definition): A character type identified by a key (warrior). It says which traits it uses, the defaults of the fields, how the character is created (defaults, point_buy, roll) and which level curve it follows. A template becomes a new version on every publish; existing characters stay bound to the version they were born with.
  • Level curve: Describes how many XP a level corresponds to. The level is not stored; it is derived from cumulative XP, so changing the curve does not require migrating character data. There are three kinds: table (a list of thresholds), piecewise (XP per level range) and formula (a quadratic formula, with max_level).
  • Character: An instance born from a template. You can write your game’s own player ID into external_id; two characters in the same environment cannot have the same external_id.
  • State: attributes (trait values), resources (consumed resources such as health/mana), progression (level, xp, highest_level), abilities, flags (yes/no flags) and the free-form custom field.
  • Version (version): A number that grows with every command on the character. It is used to prevent concurrent writes (see below).

Numbers are integers (i64); there are no decimals. Percentages are written in basis points, 10000 = 100%. Details: Numbers and encoding.

Calls your game makes

Character endpoints are called with an API key; a player session (cookie or session token) is not valid on these endpoints. Request header:

Authorization: Bearer crx_live_<prefix>_<secret>

The key type matters (see Concepts): a server key runs every command, a client key can only run the set_flag command. Do not embed a server key in your game.

Method and pathKeyWhat it does
POST /v1/definitionsany (see note below)Saves a template as a new version
GET /v1/definitions/{key}anyReads the current template and the resolved schema
POST /v1/traitsany (note)Saves a trait
POST /v1/level-curvesany (note)Saves a level curve
GET /v1/level-curves/{key}anyReads a curve
POST /v1/charactersanyCreates a character from a template
GET /v1/charactersanyLists characters (definition, cursor, limit)
GET /v1/characters/{id}anyReads a character with its state
DELETE /v1/characters/{id}anyDeletes a character (soft delete, 204)
POST /v1/characters/{id}/commandsdepends on the commandRuns a command
GET /v1/characters/{id}/statsanyReturns effective values and the calculation breakdown
GET /v1/characters/{id}/eventsanyEvent log (cursor, limit)
GET /v1/characters/{id}/timelineanyHuman-readable timeline
GET /v1/characters/{id}/state?at=anyReconstructs the state at a past moment

Identity and module rules and error bodies: Errors. Pagination: Pagination (limit 1–200, default 50; when next_cursor is null, it is the last page).

Note

Error bodies on these endpoints are plain text (text/plain). Only module_disabled and account_banned return JSON.

Warning

Known limitation: The endpoints that write templates, traits, level curves and rules, and the endpoints that read or delete characters, do not look at the key type; a valid client key can call them too. A client key is scoped to the environment, not to the player: with a key embedded in the game, any character in that environment can be read or deleted. So make management calls and character deletion only from your own server, and apply an ownership check on your server to any character ID that comes from a player. A gate by key type does not exist yet.

Creating a character

POST /v1/characters
Authorization: Bearer crx_live_<prefix>_<secret>
Content-Type: application/json

{ "definition": "warrior", "external_id": "player-1234" }
{
  "id": "0192f0f2-5b1a-7c3e-9d11-4a5b6c7d8e9f",
  "definition": "warrior",
  "definition_version": 3,
  "external_id": "player-1234",
  "version": 1,
  "level": 1,
  "state": {
    "attributes": { "strength": 10 },
    "resources": { "hp": 100 },
    "progression": { "level": 1, "xp": 0, "highest_level": 1, "granted_rewards": [] },
    "abilities": [],
    "flags": {},
    "custom": {}
  },
  "created_at": "2026-11-03T09:00:00Z"
}

The body can also contain allocation (for point-buy creation: field → points spent), seed (required for dice-roll creation, a deterministic seed) and custom (free-form JSON, at most 64 KB). The mode is defined in the template; the request only carries the input the mode asks for.

Warning

The create and delete endpoints do not accept an idempotency key and are not retried automatically. A second create with the same external_id returns 409; this also prevents duplicate characters. After a network error, do not blindly resend; first look the character up by external_id.

Running a command

POST /v1/characters/{id}/commands
Authorization: Bearer crx_live_<prefix>_<secret>
Content-Type: application/json
Idempotency-Key: quest-17-player-1234
If-Match: 1

{ "type": "add_xp", "amount": 250 }
{
  "id": "0192f0f2-5b1a-7c3e-9d11-4a5b6c7d8e9f",
  "version": 2,
  "state": { "...": "current state" },
  "events": [
    { "id": "…", "seq": 2, "type": "xp_added", "payload": { "...": "…" }, "created_at": "2026-11-03T09:01:00Z" }
  ],
  "replayed": false
}

The command type is chosen with the type field:

typeBody fieldsKey
set_flagkey, value (bool)client or server
set_attributekey, value (integer)server
add_resourcekey, delta (integer, can be negative)server
add_modifierstat, operation (flat_add, flat_sub, percent_add, percent_mul, clamp), value, priority, source_kind (item, buff, skill, curse, quest), source_id, expires_atserver
remove_modifiersource_kind, source_idserver
add_xpamountserver
rollbackto_versionserver

If you send any command other than set_flag with a client key, you get 403 and your idempotency key is not burned.

One command can produce several events: for example, if add_xp causes a level up, the response contains xp_added and level_up together; defined rules produce additional events as well.

Concurrency and replay safety

  • Idempotency-Key (optional, 1–200 characters): If the same key and the same body arrive a second time, the command does not run again; the first response is returned with replayed: true. The same key with a different body is 422. The key is “burned” only when the command succeeds; a rejected command does not consume the key. The same key in flight at the same time gets 409.
  • If-Match (optional): The version value you read, as 3 or "3". If the character is not at that version, the command is rejected with 409 and nothing changes. * and the weak W/ form are not accepted (400). If-Match is not processed on the rollback command.

General rules: Idempotency and concurrency.

Rollback

The rollback command does not delete history. It writes a new event (character_rolled_back) that returns the character to the state at to_version; the event log is append-only.

Events and webhooks

Character events (flag_set, attribute_set, resource_changed, modifier_added, modifiers_removed, xp_added, level_up, ability_unlocked, event_emitted, character_rolled_back) can also be delivered as webhooks. The envelope contains event_id, type, character_id, environment_id, seq and data. Registration and verification: Webhooks.

Limits

  • The custom field is at most 64 KB; it cannot contain decimal numbers.
  • limit on list endpoints is 1–200.
  • A level cannot be below 1; the maximum is 65535.
  • GET …/state?at= returns 400 for a future or negative time. at is a Unix millisecond timestamp or an RFC 3339 time.

Common errors

SymptomCauseFix
403 permission deniedA command other than set_flag with a client keySend the command from your server with a server key
409 version conflict: expected 3, actual 4If-Match is staleRe-read the character and resend with the new version
409 already existsThe same external_id already existsFind the existing character with GET /v1/characters
422 idempotency mismatchThe same key, a different bodyGenerate a new key; use the same key only for network retries
400 If-Match tam sürüm numarası olmalı* or a weak tag was sentSend only the number
404 + module_disabledThe characters module is disabledEnable the module

Note

The 400 message text is returned by the server in Turkish, exactly as shown; match on the status code, not on the text.

Working example

The example below creates a character and safely grants XP. The template (warrior) must already be defined, from the panel or with POST /v1/definitions.

/** Güvenilir backend örneği: karakter oluşturur, XP verir. Server anahtarı yalnız sunucu ortamından gelir. */
import { CrxClient, CrxError } from "@ferdiunal/open-play-ts";

/** Oyuncunun karakterini açar ve ilk XP'yi güvenle (idempotent) verir. */
export async function createAndReward(baseUrl: string, serverApiKey: string, playerId: string): Promise<bigint> {
  const crx = new CrxClient({ baseUrl, apiKey: serverApiKey });

  // 1) Karakteri tanımdan oluştur. `external_id` oyunun kendi oyuncu kimliğidir.
  const character = await crx.createCharacter({
    body: { definition: "warrior", external_id: playerId },
  });

  // 2) XP ver: aynı Idempotency-Key ile tekrar denemek ikinci XP vermez.
  //    If-Match, karakterin okuduğunuz sürümüdür; araya başka komut girdiyse 409 döner.
  try {
    const result = await crx.executeCommand({
      path: { id: character.id },
      body: { type: "add_xp", amount: 250n },
      headers: {
        "Idempotency-Key": `quest-1-${playerId}`,
        "If-Match": character.version.toString(),
      },
    });
    return result.version;
  } catch (error) {
    if (error instanceof CrxError && error.status === 409) {
      // Sürüm çakışması: karakteri yeniden okuyup komutu yeni sürümle gönderin.
      throw new Error("Karakter bu arada değişti; yeniden okuyup tekrar deneyin.");
    }
    throw error;
  }
}