Skip to content

Latest commit

 

History

History
947 lines (735 loc) · 24.3 KB

File metadata and controls

947 lines (735 loc) · 24.3 KB

Chronicle API Contract

This document defines every Chronicle REST API endpoint and WebSocket message that the Foundry module depends on. A new AI working on this module MUST understand this contract to avoid breaking changes.

Authentication

All REST requests include a Bearer token:

Authorization: Bearer <api-key>

WebSocket connections authenticate via query parameter at connection time:

wss://chronicle.example.com/ws?token=<api-key>

API keys are scoped to a single campaign. The key determines:

  • Which campaign's data is accessible
  • Permission level: read (GET), write (POST/PUT/DELETE), sync (sync endpoints)
  • A sync-level key covers read + write + sync
  • Rate limit: 60 requests/minute (default)

Base URL Pattern

All REST endpoints are prefixed with:

{chronicleUrl}/api/v1/campaigns/{campaignId}

The module's api-client.mjs constructs this from settings:

const baseUrl = getSetting('apiUrl');     // e.g., "https://chronicle.example.com"
const campaignId = getSetting('campaignId'); // UUID
// Requests go to: baseUrl + "/api/v1/campaigns/" + campaignId + path

Error Response Format

All error responses follow:

{
  "error": "Human-readable error message"
}

HTTP status codes:

  • 400 — Bad request (invalid input)
  • 401 — Unauthorized (missing/invalid API key)
  • 403 — Forbidden (insufficient permissions)
  • 404 — Not found
  • 409 — Conflict (optimistic concurrency via expected_updated_at)
  • 429 — Rate limited
  • 500 — Server error

REST Endpoints

Systems

GET /systems

Lists all available game systems for this campaign.

Used by: sync-manager.mjs_detectSystem()

Response:

{
  "data": [
    {
      "id": "dnd5e",
      "name": "D&D 5th Edition",
      "status": "available",
      "enabled": true,
      "has_character_fields": true,
      "has_item_fields": true,
      "foundry_system_id": "dnd5e"
    }
  ]
}

GET /systems/:systemId/character-fields

Returns character preset field definitions with Foundry annotations.

Used by: adapters/generic-adapter.mjscreateGenericAdapter()

Response:

{
  "system_id": "drawsteel",
  "preset_slug": "drawsteel-character",
  "preset_name": "Draw Steel Hero",
  "foundry_system_id": "draw-steel",
  "foundry_actor_type": "hero",
  "fields": [
    {
      "key": "might",
      "label": "Might",
      "type": "number",
      "foundry_path": "system.characteristics.might.value",
      "foundry_writable": true
    },
    {
      "key": "stamina_max",
      "label": "Stamina (Max)",
      "type": "number",
      "foundry_path": "system.stamina.max",
      "foundry_writable": false
    }
  ]
}

Key fields:

  • foundry_actor_type — Actor type to create/filter (e.g., "character", "hero")
  • foundry_path — Dot-notation path on actor.system (e.g., "system.abilities.str.value")
  • foundry_writable — Whether this field can be written back to Foundry (false = read-only from Foundry)
Multi-Preset Systems (e.g., Draw Steel Creatures)

A single game system may expose multiple entity presets, each with its own foundry_actor_type and field mappings. For example, Draw Steel has both a hero preset (drawsteel-character, actor type "hero") and a creature preset (drawsteel-creature, actor type "npc").

Expected creature preset response:

{
  "system_id": "drawsteel",
  "preset_slug": "drawsteel-creature",
  "preset_name": "Draw Steel Creature",
  "foundry_system_id": "draw-steel",
  "foundry_actor_type": "npc",
  "fields": [
    { "key": "stamina_max",   "label": "Stamina (Max)",     "type": "number", "foundry_path": "system.stamina.max",                     "foundry_writable": false },
    { "key": "stamina_value", "label": "Stamina (Current)", "type": "number", "foundry_path": "system.stamina.value",                   "foundry_writable": true },
    { "key": "might",         "label": "Might",             "type": "number", "foundry_path": "system.characteristics.might.value",      "foundry_writable": true },
    { "key": "agility",       "label": "Agility",           "type": "number", "foundry_path": "system.characteristics.agility.value",    "foundry_writable": true },
    { "key": "reason",        "label": "Reason",            "type": "number", "foundry_path": "system.characteristics.reason.value",     "foundry_writable": true },
    { "key": "intuition",     "label": "Intuition",         "type": "number", "foundry_path": "system.characteristics.intuition.value",  "foundry_writable": true },
    { "key": "presence",      "label": "Presence",          "type": "number", "foundry_path": "system.characteristics.presence.value",   "foundry_writable": true },
    { "key": "speed",         "label": "Speed",             "type": "number", "foundry_path": "system.speed.value",                     "foundry_writable": true },
    { "key": "stability",     "label": "Stability",         "type": "number", "foundry_path": "system.stability.value",                 "foundry_writable": true },
    { "key": "level",         "label": "Level",             "type": "number", "foundry_path": "system.level",                           "foundry_writable": false },
    { "key": "ev",            "label": "EV",                "type": "number", "foundry_path": "system.ev",                              "foundry_writable": false }
  ]
}

Current limitation: The Foundry module's generic adapter (generic-adapter.mjs) and actor sync (actor-sync.mjs) currently support only one preset per system. If the primary preset is drawsteel-character, creature entities with slug drawsteel-creature will not sync. Multi-preset support requires extending the adapter architecture to load multiple presets and route entities by type_slug.

GET /systems/:systemId/item-fields

Returns item preset field definitions. Same shape as character-fields.

Used by: item-sync.mjs


Entities

GET /entities

Lists entities in the campaign. Supports pagination and filtering.

Used by: journal-sync.mjs → initial sync

Query params: ?page=1&per_page=50&type_id=X&updated_since=ISO

Response:

{
  "data": [
    {
      "id": "uuid",
      "name": "Entity Name",
      "content": "<p>HTML content</p>",
      "summary": "Short text",
      "entity_type_id": 1,
      "fields_data": { "hp_current": 45, "str": 18 },
      "tags": ["npc", "villain"],
      "visibility": "public",
      "created_at": "2026-01-01T00:00:00Z",
      "updated_at": "2026-01-15T12:00:00Z"
    }
  ],
  "pagination": { "page": 1, "per_page": 50, "total": 120 }
}

POST /entities

Creates a new entity.

Used by: journal-sync.mjs → push new journal to Chronicle

Request:

{
  "name": "Entity Name",
  "content": "<p>HTML content</p>",
  "entity_type_id": 1,
  "visibility": "public"
}

Response: The created entity object (same shape as GET).

GET /entities/:entityId

Returns a single entity with full content.

PUT /entities/:entityId

Updates an entity.

Request: Same shape as POST (partial updates supported).

DELETE /entities/:entityId

Deletes an entity.

PUT /entities/:entityId/fields

Updates only the fields_data on an entity.

Used by: actor-sync.mjs → push character stats to Chronicle

Request:

{
  "fields_data": {
    "hp_current": 45,
    "str": 18,
    "level": 5
  }
}

GET /entities/:entityId/permissions

Returns entity permission/visibility settings.

PUT /entities/:entityId/permissions

Updates entity permissions.

Request:

{
  "visibility": "public"
}

POST /entities/:entityId/reveal

Toggles entity reveal state (NPC reveal to players).

Used by: actor-sync.mjs


Entity Types

GET /entity-types

Lists all entity types in the campaign.

Response:

{
  "data": [
    {
      "id": 1,
      "name": "Character",
      "slug": "dnd5e-character",
      "icon": "fa-user",
      "color": "#7C3AED"
    }
  ]
}

GET /entity-types/:typeId

Returns a single entity type with field definitions.

POST /entity-types

Create a new entity type in the campaign.

Used by: import-wizard.mjs → "Create new type" in Step 3

Request (all 4 fields required):

{
  "name": "Quest",
  "name_plural": "Quests",
  "icon": "fa-solid fa-scroll",
  "color": "#fbbf24"
}

Response: The created entity type object (same shape as GET /entity-types items).


Addons

GET /addons

Lists addons with their enabled/disabled state for the campaign.

Used by: import-wizard.mjs → Step 1 addon discovery

Response:

{
  "data": [
    { "slug": "calendar", "name": "Calendar", "category": "worldbuilding", "enabled": true },
    { "slug": "maps", "name": "Maps", "category": "worldbuilding", "enabled": true },
    { "slug": "bestiary", "name": "Bestiary", "category": "worldbuilding", "enabled": false }
  ]
}

Tags

GET /tags

Lists all tags in the campaign.

Used by: import-wizard.mjs → Step 4 tag detection

Response:

{
  "data": [
    { "id": 1, "name": "Important", "color": "#ef4444", "dm_only": false }
  ]
}

POST /tags

Create a new tag.

Used by: import-wizard.mjs → Step 8 tag creation during import

Request:

{ "name": "NPCs", "color": "#60a5fa", "dm_only": false }

Response: The created tag object.

POST /entities/bulk-tags

Bulk assign or remove tags on multiple entities. Maximum 200 entities per request — the Foundry module auto-batches larger sets.

Used by: import-wizard.mjs → bulk tag assignment after import

Request:

{
  "entity_ids": ["uuid1", "uuid2"],
  "tag_ids": [1, 2],
  "action": "add"
}

action must be "add", "remove", or "set" (replace all tags).

Response:

{
  "status": "ok",
  "processed": 2,
  "results": [
    { "entity_id": "uuid1", "status": "ok" },
    { "entity_id": "uuid2", "status": "ok" }
  ]
}

Bulk Operations

POST /entities/bulk-update

Bulk update entity type for multiple entities.

Used by: sync-dashboard.mjs → bulk Change Type action

Response: { "status": "ok", "updated": 5 }

Request:

{
  "entity_ids": ["uuid1", "uuid2"],
  "entity_type_id": 5
}

Relations

GET /relations/types

Lists predefined relation types for the campaign. Types are immutable forward/reverse string pairs (17 built-in pairs like "parent of" / "child of").

Used by: import-wizard.mjs → future relation creation support

Response:

{
  "data": [
    { "forward": "parent of", "reverse": "child of" },
    { "forward": "has item", "reverse": "owned by" },
    { "forward": "member of", "reverse": "has member" }
  ]
}

POST /entities/:entityId/relations

Create a relation on an entity. Uses the forward label string to identify the relation type (not a numeric ID).

Request:

{
  "target_entity_id": "uuid",
  "relation_type": "parent of",
  "metadata": {}
}

GET /entities/:entityId/relations

List all relations on an entity.

Used by: item-sync.mjs → pull inventory relations for actors

DELETE /entities/:entityId/relations/:relationId

Delete a relation.

Used by: item-sync.mjs → remove item from actor inventory

PUT /entities/:entityId/relations/:relationId/metadata

Update relation metadata (e.g., item quantity, equipped state).

Used by: item-sync.mjs → update inventory item metadata


Members

GET /members

Lists campaign members with their display names and roles.

Used by: sync-manager.mjs → auto-match Chronicle users to Foundry users by display name

Response:

{
  "data": [
    { "id": "uuid", "display_name": "Alice", "role": "player" }
  ]
}

Sync Mappings

GET /sync/mappings

Lists all sync mappings for the campaign.

Used by: sync-manager.mjs → initial sync setup

Response:

{
  "data": [
    {
      "id": "uuid",
      "chronicle_id": "entity-uuid",
      "foundry_id": "foundry-doc-id",
      "type": "entity",
      "last_synced": "2026-01-15T12:00:00Z"
    }
  ]
}

POST /sync/mappings

Creates a new sync mapping.

Request:

{
  "chronicle_id": "entity-uuid",
  "foundry_id": "foundry-doc-id",
  "type": "entity"
}

DELETE /sync/mappings/:mappingId

Removes a sync mapping.

GET /sync/lookup

Looks up a mapping by Foundry ID.

Used by: All sync modules to find existing mappings

Query: ?foundry_id=abc123&type=entity

Response:

{
  "chronicle_id": "entity-uuid",
  "foundry_id": "abc123",
  "type": "entity"
}

Returns 404 if no mapping exists.

GET /sync/pull

Pulls all changes since a timestamp.

Used by: sync-manager.mjs → initial sync

Query: ?since=2026-01-01T00:00:00Z

Response:

{
  "entities": [ ],
  "deleted_entities": [ "uuid1", "uuid2" ],
  "drawings": [ ],
  "tokens": [ ],
  "calendar_events": [ ]
}

POST /sync

Generic sync endpoint for batch operations.


Maps

GET /maps

Lists all maps in the campaign.

GET /maps/:mapId/drawings

Lists drawings for a map.

POST /maps/:mapId/drawings

Creates a new drawing.

PUT /maps/:mapId/drawings/:drawingId

Updates a drawing.

DELETE /maps/:mapId/drawings/:drawingId

Deletes a drawing.

GET /maps/:mapId/tokens

Lists tokens for a map.

POST /maps/:mapId/tokens

Creates a token.

PATCH /maps/:mapId/tokens/:tokenId/position

Moves a token (x, y only).

PUT /maps/:mapId/tokens/:tokenId

Full token update.

DELETE /maps/:mapId/tokens/:tokenId

Deletes a token.

GET /maps/:mapId/fog

Gets fog of war data.

PUT /maps/:mapId/fog

Updates fog of war data.

GET /maps/:mapId/layers

Lists map layers.

GET /maps/:mapId/markers

Lists map markers (pins/notes on the map).

POST /maps/:mapId/markers

Creates a map marker.

PUT /maps/:mapId/markers/:markerId

Updates a map marker.

DELETE /maps/:mapId/markers/:markerId

Deletes a map marker.


Calendar

All calendar endpoints require the calendar addon to be enabled.

GET /calendar

Returns the full calendar with all sub-resources eager-loaded: months, weekdays, moons, seasons, eras, event_categories, cycles, festivals.

Response:

{
  "id": "uuid",
  "campaign_id": "uuid",
  "mode": "fantasy",
  "name": "Calendar of Harptos",
  "description": "...",
  "epoch_name": "DR",
  "current_year": 1492,
  "current_month": 1,
  "current_day": 15,
  "current_hour": 14,
  "current_minute": 30,
  "hours_per_day": 24,
  "minutes_per_hour": 60,
  "seconds_per_minute": 60,
  "leap_year_every": 4,
  "leap_year_offset": 0,
  "months": [{ "id": 1, "name": "Hammer", "days": 30, "sort_order": 0, "is_intercalary": false, "leap_year_days": 0 }],
  "weekdays": [{ "id": 1, "name": "First Day", "sort_order": 0, "is_rest_day": false }],
  "moons": [{ "id": 1, "name": "Selûne", "cycle_days": 30.0, "phase_offset": 0.0, "color": "#c0c0ff" }],
  "seasons": [{ "id": 1, "name": "Winter", "start_month": 11, "start_day": 1, "end_month": 2, "end_day": 28, "color": "#a0c4ff" }],
  "eras": [{ "id": 1, "name": "Dale Reckoning", "start_year": 1, "end_year": null, "color": "#6366f1", "sort_order": 0 }],
  "event_categories": [{ "id": 1, "slug": "holiday", "name": "Holiday", "icon": "", "color": "#f59e0b", "sort_order": 0 }],
  "cycles": [{ "id": 1, "name": "Zodiac", "cycle_length": 12, "type": "yearly", "sort_order": 0, "entries": [] }],
  "festivals": [{ "id": 1, "name": "Midsummer", "month": 7, "day": null, "after_month": 7, "sort_order": 0 }]
}

GET /calendar/date

Returns current date/time with computed state: current season, moon phases, era, weather.

Used by: calendar-sync.mjs → poll current state

Response:

{
  "mode": "fantasy",
  "year": 1492,
  "month": 1,
  "day": 15,
  "hour": 14,
  "minute": 30,
  "current_season": { "id": 1, "name": "Winter", "color": "#a0c4ff" },
  "current_moon_phases": [
    { "moon_id": 1, "moon_name": "Selûne", "phase_name": "Full Moon", "phase_position": 0.5, "phase_icon": "moon" }
  ],
  "current_era": { "id": 1, "name": "Dale Reckoning", "start_year": 1, "color": "#6366f1" },
  "current_weather": {
    "preset_id": "rain",
    "preset_label": "Rain",
    "icon": "cloud-rain",
    "color": "#6b9bd2",
    "temperature_celsius": 12.0,
    "wind": { "speed_kph": 25.0, "speed_tier": "moderate", "direction": "NW", "direction_degrees": 315 },
    "precipitation": { "type": "rain", "intensity": 0.6 },
    "zone_id": "temperate",
    "zone_name": "Temperate",
    "description": "Steady rainfall"
  }
}

Key: current_season, current_moon_phases, current_era, and current_weather are computed server-side. They may be null/absent if no data is configured.

PUT /calendar/date

Sets current calendar date/time to an absolute value.

Request:

{ "year": 1492, "month": 3, "day": 1, "hour": 8, "minute": 0 }

POST /calendar/advance

Advances the calendar by N days (1-3650).

Request: { "days": 7 }

POST /calendar/advance-time

Advances time by hours/minutes (rolls over into days).

Request: { "hours": 2, "minutes": 30 }


Calendar Sub-Resources

GET /calendar/seasons

Returns all season definitions.

PUT /calendar/seasons

Replaces all season definitions (bulk replace).

GET /calendar/moons

Returns all moon definitions.

PUT /calendar/moons

Replaces all moon definitions.

GET /calendar/eras

Returns all era definitions.

PUT /calendar/eras

Replaces all era definitions.

GET /calendar/event-categories

Returns all event category definitions.

PUT /calendar/event-categories

Replaces all event categories.

GET /calendar/cycles

Returns zodiac/elemental cycle definitions with entries.

PUT /calendar/cycles

Replaces all cycle definitions (including entries).

GET /calendar/festivals

Returns fixed calendar festival entries.

PUT /calendar/festivals

Replaces all festival definitions.


Calendar Events

GET /calendar/events

Lists events for a month. Query: ?year=1492&month=3 or ?entity_id=uuid.

POST /calendar/events

Creates a calendar event.

Request:

{
  "name": "Festival of the Moon",
  "description": "ProseMirror JSON or plain text",
  "description_html": "<p>Rendered HTML</p>",
  "entity_id": "optional-entity-uuid",
  "year": 1492, "month": 11, "day": 30,
  "start_hour": 8, "start_minute": 0,
  "end_year": 1492, "end_month": 12, "end_day": 1,
  "end_hour": 23, "end_minute": 59,
  "is_recurring": true,
  "recurrence_type": "yearly",
  "recurrence_interval": 1,
  "recurrence_end_year": null, "recurrence_end_month": null, "recurrence_end_day": null,
  "recurrence_max_occurrences": null,
  "visibility": "everyone",
  "category": "festival",
  "color": "#ffd700",
  "icon": "star",
  "all_day": true
}

New fields (Calendaria parity):

  • color — Hex color for calendar display
  • icon — Icon identifier (FontAwesome or custom)
  • all_day — Whether event spans entire day(s) vs. specific times
  • recurrence_interval — How many periods between recurrences (e.g., every 2 years)
  • recurrence_end_year/month/day — When recurrence stops
  • recurrence_max_occurrences — Maximum number of recurrences

PUT /calendar/events/:eventId

Updates a calendar event. Same fields as POST.

DELETE /calendar/events/:eventId

Deletes a calendar event.

GET /calendar/events/:eventId

Returns a single event by ID.


Calendar Settings & Structure

PUT /calendar/settings

Updates calendar name, time system, leap year, current date/time.

PUT /calendar/months

Replaces all month definitions.

PUT /calendar/weekdays

Replaces all weekday definitions.

GET /calendar/structure

Returns calendar structure in Calendaria-compatible format.

GET /calendar/weather

Returns current weather state, or {} if none set.

PUT /calendar/weather

Sets current weather state (GM override).

GET /calendar/export

Exports the full calendar as Chronicle JSON. Add ?events=true to include events.

POST /calendar/import

Imports a calendar from JSON (Chronicle, Simple Calendar, Calendaria, Fantasy-Calendar formats).


Media

POST /media/upload

Uploads a media file (image, etc.).

Used by: api-client.mjs for image sync

Request: Multipart form data with file field.

Response:

{
  "id": "media-uuid",
  "url": "/media/media-uuid.png",
  "filename": "map-background.png",
  "content_type": "image/png",
  "size": 1048576
}

GET /media/:mediaId

Returns media metadata.

DELETE /media/:mediaId

Deletes a media file.


Relations (Shops/Inventory)

GET /entities/:entityId/relations

Lists relations for an entity (used for shop inventory).

Response:

{
  "data": [
    {
      "id": "relation-uuid",
      "source_id": "shop-entity-uuid",
      "target_id": "item-entity-uuid",
      "relation_type_id": 1,
      "metadata": { "quantity": 5, "equipped": false },
      "target": { "id": "item-uuid", "name": "Longsword", "fields_data": {} }
    }
  ]
}

WebSocket Protocol

Connection

GET /ws?token=<api-key>
Upgrade: websocket

Authentication happens at connection time via the token query parameter. If the token is invalid, the server rejects the upgrade.

Message Format (Server → Client)

{
  "type": "entity.updated",
  "data": { }
}

Message Types

Type Data Payload Description
entity.created Full entity object New entity created
entity.updated Full entity object Entity modified
entity.deleted { id: "uuid" } Entity deleted
entity_type.created Full entity type object Entity type created
entity_type.updated Full entity type object Entity type modified
entity_type.deleted { id: "uuid" } Entity type deleted
drawing.created Full drawing object Map drawing created
drawing.updated Full drawing object Map drawing modified
drawing.deleted { id, map_id } Map drawing deleted
token.created Full token object Map token created
token.moved { id, map_id, x, y } Map token moved
token.updated Full token object Map token modified
token.deleted { id, map_id } Map token deleted
marker.created Full marker object Map marker created
marker.updated Full marker object Map marker modified
marker.deleted { id } Map marker deleted
fog.updated { map_id, fog_data } Fog of war changed
layer.updated Full layer object Map layer changed
note.created Full note object Note created
note.updated Full note object Note modified
note.deleted { id } Note deleted
calendar.event.created Full event object Calendar event created
calendar.event.updated Full event object Calendar event modified
calendar.event.deleted { id } Calendar event deleted
calendar.date.advanced { year, month, day, hour, minute } Date/time changed
calendar.season.changed { id, name, color } Season boundary crossed
calendar.moon.phase_changed { moon_id, moon_name, phase_name, phase_position } Moon phase changed
calendar.weather.changed Weather input object Weather set or generated
calendar.structure.updated null Calendar structure modified
calendar.era.changed { id, name, color } Era boundary crossed
sync.status { connected: bool } Connection state change
sync.error { message } Synchronization error
sync.conflict Conflict details Data conflict detected

Reconnection

The API client automatically reconnects on WebSocket disconnection:

  • Initial retry delay: 2 seconds
  • Max retry delay: 30 seconds (exponential backoff)
  • Infinite retries (never gives up)
  • Queued messages are replayed on reconnection

CORS Requirements

Chronicle must whitelist the Foundry VTT server's origin in its CORS configuration. The module makes cross-origin requests from the Foundry server (typically http://localhost:30000 or a custom domain) to the Chronicle server.

CORS origins are managed in Chronicle's admin panel: Admin > API Settings > CORS Origin Whitelist

Required CORS headers from Chronicle:

Access-Control-Allow-Origin: <foundry-origin>
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true