HomeDeveloper docsPublic API reference
Project reference 14 min read

Public API reference

Python API, browser domain API, SDK operations, payloads and error contracts.

Documentation index · Português · Module guide

Gravewright has three related interfaces. They use the same native permission checks, but have different version markers, signatures, and result shapes:

InterfaceEntry pointCurrent markerIntended caller
Pythonfrom api import actors, ...api.API_VERSION == "1.0"Trusted server integrations with initialized Django
Browser domain APIwindow.gravewright, or a module context's apiapi.version == "1.1.0"Native frontend code and browser extensions
Original module operationsMount block's host.call(name, payload, options)SDK 1.0.0Installed, active browser modules

The Python facade re-exports selected service functions; it does not discover plugins or register Django applications. The browser APIs send authenticated requests to those services. The SDK registry is narrower and has explicit JSON input/output schemas. Do not interchange api.actors.update(...) and host.call("actor.update", ...) payloads without checking their respective contracts. The project does not currently ship an npm SDK, OpenAPI specification, or automatic compatibility release checker. Treat the version markers as descriptions of this checkout, not a promise covering undocumented internals. See licensing for extension licensing.

Browser domain API

The base template loads frontend-api.js. Once loaded, use the table-bound API on a game page, or explicitly select a table on other pages:

javascript
const table = gravewright.forTable("<campaign-uuid>");
const actors = await table.actors.list();
const result = await table.dice.roll("1d20+3", { label: "Check" });

Replace the UUID with a campaign the authenticated user belongs to. Dice rolls publish a chat message; dice.roll is not a read-only calculator. A module should use its supplied api instead of obtaining a fresh global handle, so its requests remain tied to activation and mounting.

forTable(tableId, {sceneId}) fixes a scene. A conflicting sceneId/mapId in later payloads fails with stale_context. An unbound scene on the current game page can default to the selected map; integrations requiring stable scene identity should bind it explicitly. Global account/campaign/admin/module adapters work without selecting a table, while table operations require a table context.

Methods and payloads

The authoritative method-to-command mapping is contracts/frontend.json; domain-api.js supplies convenience methods. Payload validation and permission projection belong to the native service for each domain. This contract is a method registry, not a full payload schema. Representative payloads and result assertions live in frontend API tests, table domain tests, and each app's tests.

Every domain below has state(payload = {}, options) and the listed command methods, normally (payload = {}, options):

DomainCommand methods
actorscreate, update, delete, setPermissions, saveSheet, createFolder, updateFolder, deleteFolder, deleteAsset
tokensplace, move, remove, duplicate, setHidden, setVision, configure, addCondition, removeCondition
mapsupdate, move, delete, activate, updateObjects, createFolder, updateFolder, deleteFolder
journalscreate, update, delete, move, setAccess, present, createFolder, updateFolder, moveFolder, deleteFolder, setStatus, setObjective, assignCharacters, createBoardQuest, addQuest, removeQuest, pinQuest, reorderQuests, roll, reset
itemscreate, update, delete, duplicate, setPermissions, createFolder, updateFolder, deleteFolder
combatadd, remove, configure, setInitiative, rollInitiative, toggle, moveUp, moveDown, setTurn, nextRound, previousRound, start, stop, nextTurn, previousTurn
cardsdefine, instantiate, create, draw, shuffle, reset, deleteDeck, discard, return, give, place, take, flip, move
audioupdateTrack, deleteTrack, createPlaylist, updatePlaylist, deletePlaylist, createSpatialSound, updateSpatialSound, deleteSpatialSound, play, pause, stop, seek, setVolume, startScore, pauseScore, resumeScore, stopScore, nextScore, previousScore
compendiumscreate, configure, read, delete, add, remove, import, setPermissions

Additional forms:

MethodBehavior
actors/tokens/maps/journals/items.list(payload, options)Return the collection from the authorized state response
actors/tokens/maps/journals/items.get(id, payload, options)Find an item in that projected collection, or not_found
actors.update(id, changes, options)Convenience overload; reads current version if changes.version is absent
actors.sheet(id, payload, options)Read a sheet; supports token context in payload
maps.layers(payload, options)Read authorized scene layers
chat.history(payload, options)Read visible messages
chat.send(payload, options)Send {text, sceneId?} through native chat processing
chat.remove(id, options) / chat.clear(options)Delete one/all visible-domain messages under native moderation rules
compendiums.read({packId, id}, options)Read one entry using current table access; retain its compendium document/envelope
compendiums.read({packId, ids}, options)Read 1–100 unique IDs and return {entries: [...]} in requested order; missing entries fail the whole batch
dice.roll(expression, payload, options)Roll and publish; payload supports repeat, label, visibility, scene context
table.search(query, options)Search documents with at least read access. Options accept kind (actor, item, journal, quest, quest_board, scene, compendium) and recent (up to 12 {id, type} references with an empty query). Results include authorized snippet, kind, and actions; recent references are revalidated on every request.
table.lobby(options) / table.updateLobby(payload, options)Read/change native lobby state

The six combat transition helpers (nextTurn, previousTurn, nextRound, previousRound, start, stop) also fetch the current version if omitted. Explicit versions make concurrent editing intentional:

javascript
const current = await table.actors.get("<actor-uuid>");
const requestId = crypto.randomUUID();
await table.actors.update(current.id, {
  name: "Renamed actor",
  version: current.version
}, { requestId });

Mutation methods send a generated UUID requestId unless supplied. Commands using native receipts deduplicate retries with the same ID; reuse it only when retrying the same logical command, and generate a new ID for a new change. A stale entity version/expectedVersion can still produce conflict. Do not assume every endpoint is idempotent: lobby updates and chat moderation, for example, have different service signatures.

options.signal cancels pending work. A disposed context produces stale_context; explicit cancellation produces cancelled in scoped module operations. Error handling should inspect error.code, not translated text. Global HTTP adapters can additionally expose transport/authentication errors from the underlying endpoint.

Account, campaign, owner and module adapters

These are available on the global gravewright object, not on the module's domain-only api:

AdapterMethods
accountsstatus, session, login, register, setup, logout, update
campaignslist, create, update(id, payload), join(code), remove(id, code), invite(id, payload), rulesets
administrationstatus, diagnostics, settings, updateSettings(section, payload)
moduleslist, catalog, status, install(payload), state(tableId), configure(tableId, payload)

Owner-only operations and campaign management remain checked on the server. These methods use session cookies and acquire a CSRF token before writes. They do not provide an API-key authentication scheme. Account/admin payloads are defined by the corresponding app views and tests; never place credentials in module source.

The Inside header uses owner-only POST /api/admin/updates/notification with an empty JSON body and the normal CSRF token. Its compact response includes status, available, installed/available versions and labels, channel, checkedAt and the internal Updates URL. It reuses the 24-hour discovery cache; concurrent stale checks return checking while one request refreshes metadata. It does not start installation or expose release artifacts or internal errors.

Owner-only GET /api/marketplace?latest=1 returns one compatible, non-revoked release per package ID, selecting the highest numeric X.Y.Z version after validating the full signed catalog and applying its revocations. Systems and Modules use this selection for installation. Without latest=1, the endpoint continues returning all compatible non-revoked releases; exact-version installation remains available.

GET /api/admin/status includes updates.installedRelease with version, full name, notes, canonical GitHub url and source (bundled, published or metadata). Explicit update-check responses include the same presentation. Bundled notes are available without discovery or network access and are not stored in the update cache. Notes are bounded to 128 KiB of UTF-8; numeric update versions and comparison rules remain independent of the display name.

Domain events

Subscribe using api.events.on(name, handler), which returns an unsubscribe function. Event subscriptions are disposed with their API lifetime. Supported names:

text
actors.changed       tokens.changed       maps.changed
maps.layersChanged   journals.changed     items.changed
combat.changed       cards.changed        audio.changed
compendiums.changed  chat.changed         chat.message
table.presence       table.lobbyChanged

Collection change subscriptions perform an authorized HTTP read initially, on matching socket invalidations, and on reconnect. Concurrent invalidations are coalesced; they do not carry every intermediate mutation. Actor/token/map/journal callbacks receive the projected state plus table/scene identity; item/combat/card/audio/compendium callbacks receive {module, state, tableId, sceneId?}. chat.changed, chat.message, presence, and lobby callbacks forward matching socket payloads rather than automatically reading an initial snapshot. Read their initial state separately as needed.

Subscriptions use the current page's socket event source. A forTable handle on /inside, or one targeting another table, can make HTTP requests but does not create a socket for that table. Do not assume it receives ongoing changes. Callbacks should be synchronous; start async work explicitly with error handling. The older mount-level events interface has different names and delivery caveats documented in the module guide.

Original schema-checked operations

Use the mount's or lifecycle/customization context's host.call for the following names. Input/output/event/context schemas are linked from contracts/registry.json; lifecycle/storage semantics are in contracts/api.json.

OperationScopePurpose
actor.list, actor.readTableRead visible actor summaries/documents
actor.create, actor.update, actor.deleteTableCreate, patch supplied fields, or logically delete with revision checks
actor.data.read, actor.data.updateTableRead/replace the full system data object
token.read, token.moveSceneRead a token or move its center in scene pixels
token.data.read, token.data.updateSceneRead/replace linked actor data or unlinked token snapshot data
asset.list, asset.upload, asset.downloadTableList/upload/download current-system PDF templates
actor.image.uploadTableUpload an actor portrait/token image with native validation
card.asset.uploadTableUpload a reusable card front/back image; GM only
locale.applyLocalApply a client language catalog for the mount lifetime
javascript
const actor = await host.call("actor.read", { id: actorId });
const data = await host.call("actor.data.read", { id: actorId });
// Check the operation's schema for revision and replacement fields before writing.

Scene operations require a mounted scene; a scene ID in an arbitrary payload cannot create that authority. actor.data.update and token.data.update replace whole data objects, including nested arrays/objects; null is a value, not a delete instruction. actor.update changes only supplied patch fields. Use the exact schema for each revision field; SDK revisions are not interchangeable with domain API entity versions.

Binary uploads take a real File/Blob in payload.file, use authenticated multipart transport, and derive metadata on the server. PDF template upload is GM-only and limited to 10 MiB. Downloads return {blob, name, contentType, size}. options.onProgress reports completion for this fetch-based SDK transport, not streaming progress. options.signal is supported; cancellation cannot undo a committed mutation.

card.asset.upload is an additive SDK 1.0 operation available in Gravewright 0.2.3 and later. It accepts purpose: "card_front" | "card_back" and a File/Blob, validates PNG/JPEG/WebP up to 10 MiB and 16 megapixels through the native card uploader, and returns {assetId}. The asset belongs to the authenticated GM and current table. Reuse it in api.cards.define as front_asset_id or default_back_asset_id; deck instances reference the same stored image. Uploads create new assets and have no retry receipt: persist returned IDs before starting another import. No database migration or SDK major-version change is required.

javascript
const { assetId } = await context.host.call("card.asset.upload", {
  file: new File([await context.assets.bytes("cards/front.png")], "front.png", { type: "image/png" }),
  purpose: "card_front",
});
const definition = await context.api.cards.define({
  name: "My deck", cards: [{ name: "One", front_asset_id: assetId }],
});
await context.api.cards.instantiate({ deck_definition_id: definition.deck.id });
Public module errorInterpretation
not_foundResource missing or unavailable to this context
permission_deniedAuthenticated user cannot perform the action
invalid_dataPayload/schema/path/value validation failed
conflictEntity, storage, activation revision or registration conflict
unavailableUnknown/unsupported operation, incompatible SDK, or service failure
cancelledCaller cancelled live work
stale_contextMount/scene/activation context is no longer valid

Python interface

Initialize Django before importing domain facades. manage.py shell, Django views, and app startup already do this. In standalone scripts, set DJANGO_SETTINGS_MODULE=config.settings and call django.setup() first. Do not run database queries during module import or AppConfig.ready().

Context(campaign_id, user_id) normalizes UUIDs and is immutable; constructing it does not authorize an action. Context.from_request(request, campaign_id) checks authentication and membership. Service calls continue to recheck current permissions. Use authenticated identity, not a client-supplied user ID.

python
from uuid import uuid4
from api import Context, actors

# Inside an authenticated Django view; campaign_id comes from the route.
context = Context.from_request(request, campaign_id)
visible = actors.state(context.campaign_id, context.user_id)
created = actors.command(
    context.campaign_id,
    context.user_id,
    "actor.create",
    {"name": "Example actor"},
    uuid4(),
)

Public imports and signatures, with campaign_id/user_id denoting identifiers even where the implementation's parameter names are shorter:

ImportCallable signatures
api.actorsstate(campaign_id, user_id), command(campaign_id, user_id, action, data, request_id), sheet(campaign_id, user_id, actor_id, token_id=None)
api.tokensstate(campaign_id, user_id, map_id), command(campaign_id, user_id, action, data, request_id)
api.mapsstate(campaign_id, user_id), command(campaign_id, user_id, action, data, request_id), layer_state(campaign_id, user_id, scene_id)
api.journalsstate(campaign_id, user_id), command(campaign_id, user_id, action, data, request_id)
api.items, api.combat, api.cards, api.audio, api.compendiumsstate(campaign_id, user_id, scene_id=None), command(campaign_id, user_id, action, data, request_id)
api.combat.register_providerTrusted system callback registration from AppConfig.ready(); see system combat providers
api.resourcesstate(campaign_id, user_id, name, scene_id=None, preview_token_id=None), command(campaign_id, user_id, name, action, data, request_id)
api.chathistory(campaign_id, user_id, map_id=None), send(campaign_id, user_id, text, request_id, map_id=None), remove(campaign_id, user_id, message_id=None)
api.diceevaluate(expression, repeat=1, *, random_source=None), roll(campaign_id, user_id, expression, request_id, *, repeat=1, label="", visibility="public", map_id=None)
api.tablesearch(campaign_id, user_id, query), lobby(campaign_id, user_id), update_lobby(campaign_id, user_id, payload)
api.eventsChange, resource_changed
api.errorsAuthError, JournalError, MapError, RollError

Use positional arguments as illustrated where facade parameter names vary. resources selects the native items, combat, cards, audio, or compendiums domain by name. dice.evaluate computes locally; dice.roll stores/publishes a table roll. Python exceptions preserve their original service types/codes rather than the browser's seven-code normalization.

Transactions and server hooks

Public command wrappers retain native transactions, permission checks, version checks, deduplication receipts where implemented, and notifications after commit. An enclosing transaction rollback produces neither the committed write nor its queued notifications. Direct ORM writes bypass this command behavior and should not be used as an equivalent mutation API.

Connect trusted server hooks to the Django signal:

python
from api.events import resource_changed

def observe_change(sender, change, **kwargs):
    # sender is the resource name; change contains identifiers, not a snapshot.
    print(change.resource, change.action, change.request_id)

resource_changed.connect(
    observe_change,
    dispatch_uid="example.observe-change",
    weak=False,
)

Register receivers once in your explicitly installed app's AppConfig.ready(). Change contains campaign_id, user_id, resource, action, and request_id. The signal is sent after commit through send_robust; receiver failures are logged and do not roll back committed commands. It runs inside the serving process and is not a durable queue or a replayable audit log. Keep handlers short; do not forward identifiers or subsequently loaded private data to unfiltered browser audiences. The native chat and dice publishing paths have their own audience-aware notifications and are not a promise that every write emits resource_changed.

Transport and maintenance

The host browser domain endpoint is POST /api/tables/<table_id>/api; installed modules use POST /api/tables/<table_id>/modules/<module_id>/api. Both receive {domain, method, payload, requestId?} and return {value}. Module calls also carry mountId, moduleSetRevision, and optional sceneId; the bridge acquires a lease before calling. The older schema API uses the same module prefix with /call and a {name, payload, ...context} envelope. These URLs describe the implementation for maintainers; module code should use its provided APIs.

After changing the domain method registry, run:

sh
uv run --locked python scripts/generate_frontend_api.py
uv run --locked python manage.py test gravewright.table.tests.test_frontend_api gravewright.table.tests.test_public_api gravewright.modules
node --test tests/modules/*.test.mjs

frontend-contract.js is generated from frontend.json; update the source registry first. Keep native frontend calls, module-scoped calls, and public Python calls consistent in authorization, payload semantics and post-commit behavior. For new original SDK operations, update the schemas, registry, supported operation constants, dispatcher, and tests together; the repository has no general-purpose generator for all of those files.

Module translations (Translator)

Modules receive context.i18n and context.api.i18n. Register catalogs in the activation's start(context), then translate through the API supplied to a surface:

javascript
export default {
  start({ i18n }) {
    i18n.register({
      en: { 'sheet.title': 'Character sheet', 'sheet.greeting': 'Welcome, {name}' },
      'pt-BR': { 'sheet.title': 'Ficha de personagem', 'sheet.greeting': 'Boas-vindas, {name}' },
      es: { 'sheet.title': 'Ficha de personaje', 'sheet.greeting': 'Bienvenido, {name}' },
    });
  },
  register({ register }) {
    register('actor.directory', async ({ root, api }) => {
      root.setAttribute('translate', 'no');
      root.textContent = api.i18n.t('sheet.greeting', { name: 'Arin' });
    });
  },
};

Catalogs can also be loaded with await context.assets.json('locales/messages.json'). locale follows the user's interface language. Lookup falls back from the exact locale to its base language, then English, then the source key. t(key, params) substitutes named or numbered placeholders as plain text. Use textContent for rendering. has(key) checks coverage; number(value, options) uses Intl.NumberFormat; onChange(handler) returns an unsubscribe function for locale events and registration/removal of catalogs in that namespace. Changing the interface language currently reloads the page, preserving the table URL.

Catalogs are isolated by module ID and shared across that module's contexts on the page. register returns a disposer and also removes its catalog when its owning context is disposed. Partial translations require an English fallback for each key and matching placeholders. Register during activation so surface mounts can reuse the catalog. No HTTP permission or external translation service is needed. Without Translator/language packs, the interface uses English. The native gravewright.i18n adapter resolves host catalogs; installed modules use their supplied context.

Source in the project

docs/en/api.mddocs/pt-BR/api.md