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 do | Panel page |
|---|---|
| Define a character template, a trait and a level curve | Character templates |
| List characters, open one and see its state and history | Characters, Character profile |
| See a player’s account and characters together | Players, Player profile |
| Write event-driven automation | Game 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) andformula(a quadratic formula, withmax_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 sameexternal_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-formcustomfield. - 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 path | Key | What it does |
|---|---|---|
POST /v1/definitions | any (see note below) | Saves a template as a new version |
GET /v1/definitions/{key} | any | Reads the current template and the resolved schema |
POST /v1/traits | any (note) | Saves a trait |
POST /v1/level-curves | any (note) | Saves a level curve |
GET /v1/level-curves/{key} | any | Reads a curve |
POST /v1/characters | any | Creates a character from a template |
GET /v1/characters | any | Lists characters (definition, cursor, limit) |
GET /v1/characters/{id} | any | Reads a character with its state |
DELETE /v1/characters/{id} | any | Deletes a character (soft delete, 204) |
POST /v1/characters/{id}/commands | depends on the command | Runs a command |
GET /v1/characters/{id}/stats | any | Returns effective values and the calculation breakdown |
GET /v1/characters/{id}/events | any | Event log (cursor, limit) |
GET /v1/characters/{id}/timeline | any | Human-readable timeline |
GET /v1/characters/{id}/state?at= | any | Reconstructs 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). Onlymodule_disabledandaccount_bannedreturn 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_idreturns409; this also prevents duplicate characters. After a network error, do not blindly resend; first look the character up byexternal_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:
type | Body fields | Key |
|---|---|---|
set_flag | key, value (bool) | client or server |
set_attribute | key, value (integer) | server |
add_resource | key, delta (integer, can be negative) | server |
add_modifier | stat, operation (flat_add, flat_sub, percent_add, percent_mul, clamp), value, priority, source_kind (item, buff, skill, curse, quest), source_id, expires_at | server |
remove_modifier | source_kind, source_id | server |
add_xp | amount | server |
rollback | to_version | server |
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 withreplayed: true. The same key with a different body is422. 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 gets409.If-Match(optional): Theversionvalue you read, as3or"3". If the character is not at that version, the command is rejected with409and nothing changes.*and the weakW/form are not accepted (400).If-Matchis not processed on therollbackcommand.
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
customfield is at most 64 KB; it cannot contain decimal numbers. limiton list endpoints is 1–200.- A level cannot be below 1; the maximum is 65535.
GET …/state?at=returns400for a future or negative time.atis a Unix millisecond timestamp or an RFC 3339 time.
Common errors
| Symptom | Cause | Fix |
|---|---|---|
403 permission denied | A command other than set_flag with a client key | Send the command from your server with a server key |
409 version conflict: expected 3, actual 4 | If-Match is stale | Re-read the character and resend with the new version |
409 already exists | The same external_id already exists | Find the existing character with GET /v1/characters |
422 idempotency mismatch | The same key, a different body | Generate 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 sent | Send only the number |
404 + module_disabled | The characters module is disabled | Enable the module |
Note
The
400message 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;
}
}