Eposyne Director

Events

Events tell Eposyne Director what happened in your game. Plots and Memory read them. You send them in batches to POST /v2/events. This page covers the batch, worlds and timelines, sequence numbers, limits, and what costs credits.

Switch the modules on first

Plots and Memory are off by default for every account, on every plan. Switch them on under Modules at https://architect.eposyne.com/profile?section=director-keys. A switch reaches your API keys within a minute and your sign-in tokens within an hour. With both modules off, POST /v2/events answers 403 and nothing is stored.

The Eposyne engine plugins send events for you. Read this page when you send events from your own server.

What an event is

An event is one thing that happened in your game: the player entered a tavern, a guard was defeated, a character told another character a secret. Director reads events and never sends one. An event is an observation. Director never treats it as an instruction.

Events do not make Director act on their own. Plots uses them to start and track story arcs. Memory uses them to decide what each character perceived and believes. Both modules also read the stream when you ask for a decision at a stream position, see The Plots verdict and Memory in a decision.

The batch

Request line and headers
POST https://director.eposyne.com/v2/events
Authorization: Bearer epd_...
Content-Type: application/json

The body is a JSON object with at most 500 events and at most 262,144 bytes (256 KB). Any other media type is refused with 415. Members the format does not define are refused with 422, and nothing in a refused batch is stored.

MemberRequiredMeaning
world_instance_idYesThe world the events belong to, 1 to 128 characters. See Worlds and timelines.
timeline_idYesThe timeline inside that world, 1 to 128 characters.
parentFirst batch of a child timeline onlyWhere the timeline branches from. See Child timelines.
eventsYesOne to 500 events, in strictly increasing seq order.

One event

MemberRequiredMeaning
seqYesThe sequence number, an integer from 1. See Sequence numbers.
capability_idYesA UUID that names the kind of event. Every event of one kind carries the same UUID. The standard events have fixed UUIDs, listed below. For your own kinds, make one UUID per kind and keep it.
keyYesThe readable name of the kind, in lowercase letters and digits with dots, dashes, or underscores between parts, up to 128 characters, for example combat.enemy-defeated.
occurred_atYesWhen it happened, as an ISO 8601 date and time, for example 2026-10-13T18:02:11Z.
game_timeNoThe time inside your game, up to 64 characters. The form day 3, 19:40 lets Memory time rumours by game hours. Without it Memory uses occurred_at.
player_idNoThe player the event belongs to. Plots acts only on events that name a player, here or in players.
playersNoEvery player the event concerns, for example both players of a fight: 1 to 16 different identities of 1 to 128 characters. See Events for several players. When you also send player_id, it must be one of them.
actor, target, locationNoReferences to the game entities involved. A reference has domain (written like a key), id (1 to 128 characters), and an optional name (up to 256 characters). Use the same id for the same entity every time.
witnessesNoUp to 64 references to the characters who perceived the event. See Witnesses.
payloadYesAn object with the details of the event. It may be empty, {}. Member names follow the same rule as key.

Worlds and timelines

A world is one playthrough, for example one save slot. A timeline is one line of play inside a world. You choose both ids, and Director creates them the first time a batch names them. The ids are case sensitive.

A world has one timeline until the player loads an earlier save. After a load you start a new timeline that branches from the old one, see Child timelines. Plots arcs and Memory beliefs belong to a world and a timeline, so each playthrough keeps its own story.

An account has at most 20 active worlds and a world has at most 200 timelines. A world is active while it has received an event or a new timeline in the last 90 days. See Limits, and Deleting a world to remove one you no longer need.

Sequence numbers and acknowledgements

You number the events of each timeline. Numbers start at 1 and go up by 1 for each event. Director uses them to put events in order and to see which ones are missing.

200 acknowledgement
{
  "world_instance_id": "save-1",
  "timeline_id": "main",
  "acknowledged_seq": 42,
  "missing": [{"from": 43, "to": 44}]
}

Director holds at most 1,000 sequence numbers beyond the acknowledged position of a timeline. A batch that reaches further is refused with 422 and "code": "sequence_gap_too_large". Nothing in it is stored and no timeline is created. Send the missing events first.

422
{
  "detail": "sequence number 1100 is 1060 beyond the acknowledged position 40 of timeline 'main'; at most 1000 may be held ahead. Send the missing events first; nothing in this batch was accepted",
  "code": "sequence_gap_too_large"
}

Child timelines

When the player loads an earlier save, the future they abandoned must not count. Start a new timeline and say where it branches from. Send parent on the first batch of the new timeline only:

First batch of a child timeline (events cut)
{
  "world_instance_id": "save-1",
  "timeline_id": "main-b1",
  "parent": {"timeline_id": "main", "seq": 30},
  "events": [ ... ]
}

Witnesses

Memory forms a memory for each character who perceived an event. Say who perceived it with witnesses, a list of references to the characters present.

Events for several players

Some moments belong to more than one player: two players fighting, trading, or discovering something together. Send one event and list the players it concerns in players. Do not send one copy of the event per player: Memory would perceive the moment once for every copy.

fight.json
{
  "world_instance_id": "save-1",
  "timeline_id": "main",
  "events": [
    {
      "seq": 3,
      "capability_id": "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
      "key": "combat.fight-won",
      "occurred_at": "2026-10-13T18:10:00Z",
      "players": ["player-1", "player-2"],
      "actor": {"domain": "actor.instance", "id": "player-1", "name": "Ilse"},
      "target": {"domain": "actor.instance", "id": "player-2", "name": "Bram"},
      "location": {"domain": "location", "id": "loc-harbour-road", "name": "Harbour Road"},
      "witnesses": [{"domain": "actor.instance", "id": "npc-mara", "name": "Mara"}],
      "payload": {"outcome": "fight-won"}
    }
  ]
}
422 player_not_listed
{"detail": "event 2: player_id 'player-3' is not one of its players", "code": "player_not_listed"}

Standard events

These eight events have fixed keys and UUIDs. Use them as listed. Director recognizes a standard event by its key.

Keycapability_idMeaning and payload
loreweaver.conversation.player-said87d7a694-7dba-4907-a05f-f97e1313748bThe player spoke a line. Payload: conversation (string or null), text (the line, up to 4,000 characters). Name the character spoken to as target.
loreweaver.conversation.npc-said5c99a2c6-09c7-4fa9-b86f-e066ef583880A character spoke a line. Payload: conversation, text.
loreweaver.conversation.boundaryac767a08-5e76-483c-ae90-56f865d9af3eA conversation started or ended. Payload: conversation (string), phase (started or ended).
loreweaver.movement.location-enteredf8af85b6-f957-4efe-8d90-a68e8e8ee7feThe player entered a location. Name it as location. Payload: previous (a location id or null).
loreweaver.items.item-transferred5b283b5f-d59e-4c51-b760-482bb425be3dAn item changed hands. Payload: item (an item id), quantity (integer, 1 or more), direction (given or received).
loreweaver.conversation.toldabe9f566-9c61-4185-9942-ba89c0c4c0daSomeone told a character something outside a conversation Director hears line by line, for example through a choice menu or a letter. The teller is the actor (the player, or a character) and the listener is the target. Payload: text (a short statement, up to 4,000 characters).
loreweaver.conversation.characters-met5d2cc6ec-8238-4fea-9b21-53e7901ab83aTwo characters met and could exchange news. They are the actor and the target. Report a meeting of more than two characters as one event per pair. Payload: {}.
loreweaver.movement.character-entered-location59d642d4-a77a-466a-a644-ce8a9a58ed45A character entered a location. The character is the actor and the place is the location. Payload: previous (a location id or null).

What Memory does with them:

The key and UUID of a standard event belong together. Each key also has an Eposyne spelling, with eposyne. in place of loreweaver., for example eposyne.conversation.told. Director treats both spellings of a key as the same standard event, with the same UUID, and keeps the key you sent. Any other key that starts with loreweaver. is one of your own kinds. The engine plugins send the keys listed above.

What you send is stored and read by models

Limits

Every limit is per account. All your API keys and sign-in tokens share it. A batch that a limit refuses stores nothing.

LimitValueWhen exceeded
Events sent per minute600429, "code": "event_rate_exceeded", with a Retry-After header in seconds. The whole batch is refused and not counted.
Events sent per day100,000, counted over a rolling 24 hours429, "code": "event_rate_exceeded", with Retry-After
Size of one batch500 events, 262,144 bytes422
Players on one event16, all different422. A player_id outside players is 422 with "code": "player_not_listed".
Sequence numbers held beyond the acknowledged position, per timeline1,000422, "code": "sequence_gap_too_large"
Active worlds20409, "code": "world_limit", for a batch that would create a 21st active world, or that would send events to an idle world while 20 others are active
Timelines per world200409, "code": "timeline_limit"
Arcs in the Plots library500409, "code": "library_full". See Plots.
Size of one arc document262,144 bytes (256 KB)422, "code": "arc_too_large"
Library writes (PUT and DELETE on the Plots library)600 per minute429, "code": "library_rate_exceeded", with Retry-After
Judgements one events request runs50Not an error. The rest is reported as deferred, see below.
Event payloads kept after processing90 daysThe payload is deleted. Positions and acknowledgements stay.

Events, the library reads, the running arcs read, the Memory read, and the policy do not count toward the request limit of 60 per minute that POST /v2/decide uses. Deleting a world does. Storage and event intake are free, only capped. Model calls are not free, see below.

429
HTTP/1.1 429 Too Many Requests
Retry-After: 12

{"detail": "event rate exceeded: 600 events per minute", "code": "event_rate_exceeded"}

On a 429, wait at least Retry-After seconds, then send the same batch again. On a 409 world_limit, delete a world you no longer need. A world that has been idle for 90 days stops counting by itself, but it comes back when it receives events again, and then it counts again.

Judgements: cost, deferred, and refused

Receiving an event costs nothing. Judging it can. A judgement is a model call that Plots or Memory makes for itself: Plots fitting an event to a story arc, choosing an arc, picking a branch, or filling in a beat; Memory weighing an event in model mode, distorting a rumour, judging a claim as a referee, or checking a reply for inventions.

Every judgement is reserved and charged from your usage credits exactly like a decision, at the price of the model that answers. See Billing and limits. How many judgements your events cause depends on your policy. With Memory on its default settings, events cause no Memory judgements. Plots makes at least one judgement for every player an event names, in player_id or in players.

Deferred

One events request runs at most 50 judgements. The count is checked before each event, so the event that is running when the count is reached finishes, and one request can pass 50 by the calls of that last event. The events after it stay held and acknowledged, but are not processed yet. The response says how many:

200
{
  "world_instance_id": "save-1",
  "timeline_id": "main",
  "acknowledged_seq": 120,
  "missing": [],
  "deferred": {"count": 70}
}

Refused

Before a judgement, Director reserves its cost on your credits. When it cannot, the judgement is skipped and the module acts as if nothing happened for that event. The event is still processed and acknowledged, and the skipped judgement is not made later. The response says so:

200
{
  "world_instance_id": "save-1",
  "timeline_id": "main",
  "acknowledged_seq": 7,
  "missing": [],
  "refused_judgements": {"count": 2, "reason": "balance_under_floor"}
}

Both members are additions to the acknowledgement. Ignore members you do not know.

Deleting a world

DELETE /v2/worlds/{world_instance_id} removes one world and everything Director holds for it.

Request
curl -i -X DELETE https://director.eposyne.com/v2/worlds/save-1 \
  -H "Authorization: Bearer $EPOSYNE_API_KEY"
Response
HTTP/1.1 204 No Content

Deleting a world is permanent and cannot be undone. It removes the timelines of the world, every event Director holds for it, the Plots arcs running in it with the verdicts waiting to be reported, and the Memory beliefs, positions, and world record of its characters. It does not touch your Plots library, your policy, or any other world. Afterwards a batch for the same world_instance_id starts a new world from nothing.

409
{"detail": "world 'save-1' is being processed; repeat the call in a moment", "code": "world_busy"}

Responses of POST /v2/events

StatusMeaningRetry?
200Held. The body is the acknowledgement, with deferred or refused_judgements when they apply.Resend what missing lists.
401Missing or invalid credential.Not with the same key.
403Neither Plots nor Memory is switched on for the account. The body is {"detail": "no event consumer is enabled for this tenant"}.Never. Switch a module on.
409The timeline exists with a different parent, or world_limit or timeline_limit.Never unchanged.
415The Content-Type is not application/json.Never. Fix the header.
422The batch does not match the format, is over 262,144 bytes, has numbers that do not increase, or is sequence_gap_too_large.Never unchanged.
429event_rate_exceeded.After Retry-After seconds.
503The account service could not be reached for a key Director has not seen in the last minute.With backoff.

The complete list with the shared error body is on Errors and retries.

A worked example

This example sends two events, asks for a decision at that position, and deletes the world. It assumes the Plots module is on and EPOSYNE_API_KEY is set as in Getting started.

Save the first batch as batch.json. The player, Ilse, enters a tavern and speaks to Mara:

batch.json
{
  "world_instance_id": "save-1",
  "timeline_id": "main",
  "events": [
    {
      "seq": 1,
      "capability_id": "f8af85b6-f957-4efe-8d90-a68e8e8ee7fe",
      "key": "loreweaver.movement.location-entered",
      "occurred_at": "2026-10-13T18:02:11Z",
      "game_time": "day 1, 19:40",
      "player_id": "player-1",
      "actor": {"domain": "actor.instance", "id": "player-1", "name": "Ilse"},
      "location": {"domain": "location", "id": "loc-tavern", "name": "The Crooked Lantern"},
      "payload": {"previous": null}
    },
    {
      "seq": 2,
      "capability_id": "87d7a694-7dba-4907-a05f-f97e1313748b",
      "key": "loreweaver.conversation.player-said",
      "occurred_at": "2026-10-13T18:02:40Z",
      "game_time": "day 1, 19:41",
      "player_id": "player-1",
      "actor": {"domain": "actor.instance", "id": "player-1", "name": "Ilse"},
      "target": {"domain": "actor.instance", "id": "npc-mara", "name": "Mara"},
      "location": {"domain": "location", "id": "loc-tavern", "name": "The Crooked Lantern"},
      "payload": {"conversation": "conv-1", "text": "I will find your son. Where was he last seen?"}
    }
  ]
}
Request
curl -X POST https://director.eposyne.com/v2/events \
  -H "Authorization: Bearer $EPOSYNE_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @batch.json
Response to batch.json
{
  "world_instance_id": "save-1",
  "timeline_id": "main",
  "acknowledged_seq": 2,
  "missing": []
}

Both events are held and processed. If the response had listed a missing range, you would send those events now. With Plots on, each of these events named a player, so Plots made one judgement for each, and your credits paid for them.

Now ask for a decision for Mara at position 2. The three query parameters name the timeline and the last event the decision must follow. request.json is a decide request as in Getting started.

Request
curl -X POST "https://director.eposyne.com/v2/decide?world=save-1&timeline=main&after_seq=2" \
  -H "Authorization: Bearer $EPOSYNE_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @request.json

Director first makes sure every event up to 2 is held and processed. If it is not, the answer is 409 with the acknowledgement and the range that is missing, and no model is called. Send the missing events and repeat the decision. Give all three parameters or none, or the answer is 422. The reply carries a Plots element, see The Plots verdict.

When the playthrough is over, delete the world:

Request
curl -i -X DELETE https://director.eposyne.com/v2/worlds/save-1 \
  -H "Authorization: Bearer $EPOSYNE_API_KEY"