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.
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)
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 + pathAll 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 found409— Conflict (optimistic concurrency viaexpected_updated_at)429— Rate limited500— Server error
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"
}
]
}Returns character preset field definitions with Foundry annotations.
Used by: adapters/generic-adapter.mjs → createGenericAdapter()
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 onactor.system(e.g., "system.abilities.str.value")foundry_writable— Whether this field can be written back to Foundry (false = read-only from Foundry)
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 isdrawsteel-character, creature entities with slugdrawsteel-creaturewill not sync. Multi-preset support requires extending the adapter architecture to load multiple presets and route entities bytype_slug.
Returns item preset field definitions. Same shape as character-fields.
Used by: item-sync.mjs
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 }
}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).
Returns a single entity with full content.
Updates an entity.
Request: Same shape as POST (partial updates supported).
Deletes an entity.
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
}
}Returns entity permission/visibility settings.
Updates entity permissions.
Request:
{
"visibility": "public"
}Toggles entity reveal state (NPC reveal to players).
Used by: actor-sync.mjs
Lists all entity types in the campaign.
Response:
{
"data": [
{
"id": 1,
"name": "Character",
"slug": "dnd5e-character",
"icon": "fa-user",
"color": "#7C3AED"
}
]
}Returns a single entity type with field definitions.
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).
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 }
]
}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 }
]
}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.
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 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
}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" }
]
}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": {}
}List all relations on an entity.
Used by: item-sync.mjs → pull inventory relations for actors
Delete a relation.
Used by: item-sync.mjs → remove item from actor inventory
Update relation metadata (e.g., item quantity, equipped state).
Used by: item-sync.mjs → update inventory item metadata
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" }
]
}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"
}
]
}Creates a new sync mapping.
Request:
{
"chronicle_id": "entity-uuid",
"foundry_id": "foundry-doc-id",
"type": "entity"
}Removes a sync mapping.
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.
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": [ ]
}Generic sync endpoint for batch operations.
Lists all maps in the campaign.
Lists drawings for a map.
Creates a new drawing.
Updates a drawing.
Deletes a drawing.
Lists tokens for a map.
Creates a token.
Moves a token (x, y only).
Full token update.
Deletes a token.
Gets fog of war data.
Updates fog of war data.
Lists map layers.
Lists map markers (pins/notes on the map).
Creates a map marker.
Updates a map marker.
Deletes a map marker.
All calendar endpoints require the calendar addon to be enabled.
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 }]
}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.
Sets current calendar date/time to an absolute value.
Request:
{ "year": 1492, "month": 3, "day": 1, "hour": 8, "minute": 0 }Advances the calendar by N days (1-3650).
Request: { "days": 7 }
Advances time by hours/minutes (rolls over into days).
Request: { "hours": 2, "minutes": 30 }
Returns all season definitions.
Replaces all season definitions (bulk replace).
Returns all moon definitions.
Replaces all moon definitions.
Returns all era definitions.
Replaces all era definitions.
Returns all event category definitions.
Replaces all event categories.
Returns zodiac/elemental cycle definitions with entries.
Replaces all cycle definitions (including entries).
Returns fixed calendar festival entries.
Replaces all festival definitions.
Lists events for a month. Query: ?year=1492&month=3 or ?entity_id=uuid.
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 displayicon— Icon identifier (FontAwesome or custom)all_day— Whether event spans entire day(s) vs. specific timesrecurrence_interval— How many periods between recurrences (e.g., every 2 years)recurrence_end_year/month/day— When recurrence stopsrecurrence_max_occurrences— Maximum number of recurrences
Updates a calendar event. Same fields as POST.
Deletes a calendar event.
Returns a single event by ID.
Updates calendar name, time system, leap year, current date/time.
Replaces all month definitions.
Replaces all weekday definitions.
Returns calendar structure in Calendaria-compatible format.
Returns current weather state, or {} if none set.
Sets current weather state (GM override).
Exports the full calendar as Chronicle JSON. Add ?events=true to include events.
Imports a calendar from JSON (Chronicle, Simple Calendar, Calendaria, Fantasy-Calendar formats).
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
}Returns media metadata.
Deletes a media file.
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": {} }
}
]
}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.
{
"type": "entity.updated",
"data": { }
}| 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 |
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
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