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.
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
POST https://director.eposyne.com/v2/events
Authorization: Bearer epd_...
Content-Type: application/jsonThe 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.
| Member | Required | Meaning |
|---|---|---|
world_instance_id | Yes | The world the events belong to, 1 to 128 characters. See Worlds and timelines. |
timeline_id | Yes | The timeline inside that world, 1 to 128 characters. |
parent | First batch of a child timeline only | Where the timeline branches from. See Child timelines. |
events | Yes | One to 500 events, in strictly increasing seq order. |
One event
| Member | Required | Meaning |
|---|---|---|
seq | Yes | The sequence number, an integer from 1. See Sequence numbers. |
capability_id | Yes | A 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. |
key | Yes | The 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_at | Yes | When it happened, as an ISO 8601 date and time, for example 2026-10-13T18:02:11Z. |
game_time | No | The 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_id | No | The player the event belongs to. Plots acts only on events that name a player, here or in players. |
players | No | Every 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, location | No | References 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. |
witnesses | No | Up to 64 references to the characters who perceived the event. See Witnesses. |
payload | Yes | An 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.
{
"world_instance_id": "save-1",
"timeline_id": "main",
"acknowledged_seq": 42,
"missing": [{"from": 43, "to": 44}]
}acknowledged_seqis the highest number up to which Director holds every event of the timeline without a gap. It is 0 when Director holds none.missinglists the gaps Director found, as inclusive ranges. It is empty when there is no gap. In the example Director holds 45 or a later event but lacks 43 and 44.- Acknowledged means held, not yet processed. Director hands events to the modules in order, one timeline at a time, and never past a gap. An event after a gap waits until the gap is filled.
- Sending an event Director already holds is safe. It is ignored and still acknowledged. Resend everything after
acknowledged_seqthat you still have, in order, untilmissingis empty. - Keep every event until it is acknowledged. A batch that fails (a 429 or a 503, for example) holds nothing, so send it again unchanged.
- Numbers within one batch must strictly increase, or the batch is refused with 422.
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.
{
"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:
{
"world_instance_id": "save-1",
"timeline_id": "main-b1",
"parent": {"timeline_id": "main", "seq": 30},
"events": [ ... ]
}parent.seqis the last number of the parent timeline that still counts. Events the parent holds after it stay stored, but they count for nothing on the child.- The numbering of the child continues from there: its first event is
parent.seq + 1, here 31. A number at or belowparent.seqin the child is ignored. - The child sees what its parent held up to the branch point: arcs, beliefs, and positions. Reads on the child, such as the running arcs and a character's beliefs, show exactly that, plus what happened on the child.
- Send the events of the parent first, so Director holds them up to
parent.seq. - A timeline cannot be its own parent. A batch that names an existing timeline with a different
parentis refused with 409, without a code. Fix the timeline ids and never retry that batch unchanged. - Every new timeline counts toward the 200 timelines of its world.
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.
- The
targetand theactorof an event always perceive it, unless the actor is a player. A player never forms memories, and neither does any player an event lists inplayers. - With
witnesses, those characters perceive it as well, and Director looks no further. - Without
witnesses, Director treats the characters it knows to be at the event'slocationas having perceived it. It learns where characters are from the standard movement events. You can turn this off withmemory.perception_location_fallbackin the policy. - Plots ignores witnesses.
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.
{
"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"}
}
]
}playersholds 1 to 16 different player identities. More than 16, an empty list, or a repeated identity is refused with 422, and nothing in the batch is stored.player_idstays valid on its own, and an event that carries onlyplayer_idbehaves as it always did. When you send both,player_idmust be one ofplayers. Otherwise the whole batch is refused with 422 and acode:
{"detail": "event 2: player_id 'player-3' is not one of its players", "code": "player_not_listed"}- Plots fits the event into the arcs of each listed player, in the order of the list, exactly as it does for an event with that one
player_id. Each player's arcs advance, start, or stay as that player's story requires. - Each fit is its own judgement. It is reserved and charged on its own, and it counts toward the 50 judgements of the request, see below. An event for 16 players costs at least 16 fitting calls, so list only the players the moment really concerns.
- When the 50 judgements run out between two players of one event, the event stays deferred as a whole. The next delivery gives it again. Players already fitted are not fitted again and not charged again, and Memory sees the event once, after the last player.
- Memory perceives the event once. The characters who perceive it (the target, the actor, the witnesses) each form one memory, and no listed player ever forms one.
- A decision at the event can name whose Plots verdicts it reports with the query parameter
player_id, see The Plots verdict.
Standard events
These eight events have fixed keys and UUIDs. Use them as listed. Director recognizes a standard event by its key.
| Key | capability_id | Meaning and payload |
|---|---|---|
loreweaver.conversation.player-said | 87d7a694-7dba-4907-a05f-f97e1313748b | The 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-said | 5c99a2c6-09c7-4fa9-b86f-e066ef583880 | A character spoke a line. Payload: conversation, text. |
loreweaver.conversation.boundary | ac767a08-5e76-483c-ae90-56f865d9af3e | A conversation started or ended. Payload: conversation (string), phase (started or ended). |
loreweaver.movement.location-entered | f8af85b6-f957-4efe-8d90-a68e8e8ee7fe | The player entered a location. Name it as location. Payload: previous (a location id or null). |
loreweaver.items.item-transferred | 5b283b5f-d59e-4c51-b760-482bb425be3d | An item changed hands. Payload: item (an item id), quantity (integer, 1 or more), direction (given or received). |
loreweaver.conversation.told | abe9f566-9c61-4185-9942-ba89c0c4c0da | Someone 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-met | 5d2cc6ec-8238-4fea-9b21-53e7901ab83a | Two 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-location | 59d642d4-a77a-466a-a644-ce8a9a58ed45 | A 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:
player-said: every character who perceives the line remembers that the player said it. A decision at this event can also have the referee judge the line, see Memory in a decision.told: the listener remembers what the teller said, with the teller as the source. The chain of retellings is followed from teller to listener.characters-met: both characters perceive it, and when rumour spreading is on andreportedis one of its meeting sources, they exchange news.- The two movement events: Memory records where each character is. When rumour spreading is on and
movementis one of its meeting sources, a character entering a place where another one stands counts as a meeting. - Every other event, your own kinds included, is recorded as something that happened in the world, and each character who perceived it remembers it.
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
- Event payloads are kept for 90 days after Director processes them. Then the payload is deleted. Sequence numbers, timelines, and positions stay, so acknowledgements and branches keep working. What Plots and Memory built from the events (arcs, the world record, beliefs) stays until you delete it or close the account.
- To judge an event, Plots and Memory put it into the prompt of a model call to the provider of the model that answers. Whatever you put in an event can reach that provider.
- Send only what the modules need. Leave out secrets, passwords, and personal data of real people. Names of game characters and places are fine.
- Report only occurrences you are willing to have stored and read. If a payload field is private to the player, leave it out of the payload before you send it.
Limits
Every limit is per account. All your API keys and sign-in tokens share it. A batch that a limit refuses stores nothing.
| Limit | Value | When exceeded |
|---|---|---|
| Events sent per minute | 600 | 429, "code": "event_rate_exceeded", with a Retry-After header in seconds. The whole batch is refused and not counted. |
| Events sent per day | 100,000, counted over a rolling 24 hours | 429, "code": "event_rate_exceeded", with Retry-After |
| Size of one batch | 500 events, 262,144 bytes | 422 |
| Players on one event | 16, all different | 422. A player_id outside players is 422 with "code": "player_not_listed". |
| Sequence numbers held beyond the acknowledged position, per timeline | 1,000 | 422, "code": "sequence_gap_too_large" |
| Active worlds | 20 | 409, "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 world | 200 | 409, "code": "timeline_limit" |
| Arcs in the Plots library | 500 | 409, "code": "library_full". See Plots. |
| Size of one arc document | 262,144 bytes (256 KB) | 422, "code": "arc_too_large" |
Library writes (PUT and DELETE on the Plots library) | 600 per minute | 429, "code": "library_rate_exceeded", with Retry-After |
| Judgements one events request runs | 50 | Not an error. The rest is reported as deferred, see below. |
| Event payloads kept after processing | 90 days | The 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.
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:
{
"world_instance_id": "save-1",
"timeline_id": "main",
"acknowledged_seq": 120,
"missing": [],
"deferred": {"count": 70}
}deferred.countis the number of events held without a gap that no module has seen yet. The member is present only when the count is above 0.- They are processed, in order, by the next delivery of events for the timeline, or by a
POST /v2/decidewith a stream position, before that decision is made. - While another request is already processing the same timeline, your request returns at once and the events it added count as deferred.
- Deferred events are not lost. A decision at a position that has not been processed waits up to 10 seconds, then answers 409 so that you can resend the missing events and try again.
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:
{
"world_instance_id": "save-1",
"timeline_id": "main",
"acknowledged_seq": 7,
"missing": [],
"refused_judgements": {"count": 2, "reason": "balance_under_floor"}
}countis the number of judgements refused while the batch, and any held events it released, was processed.reasonis the reason of the first one. The member is present only when the count is above 0.- The reasons are
balance_under_floor,credits_off,monthly_limit_reached, andinsufficient_for_hold, as for a decision, and alsoaccount_service_unavailable,billing_unavailable,prompt_too_large, andmodel_unavailable(no model you may use is available for the judgement). - When the member appears, check your credits with
GET /v2/usage/me, add credits or lift the limit, and keep playing. Memory falls back to its rules for a memory whose weighing was refused.
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.
curl -i -X DELETE https://director.eposyne.com/v2/worlds/save-1 \
-H "Authorization: Bearer $EPOSYNE_API_KEY"HTTP/1.1 204 No ContentDeleting 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.
- It answers 204 with no body, also for a world Director does not hold and for a second delete of the same world.
- It needs a valid credential and no module switch, so an account that switched the modules off can still clean up. It counts as one request toward the request limit, calls no model, and costs nothing.
- An id over 128 characters is a 400.
- When a timeline of the world is being processed, Director waits about five seconds, then answers 409 with
"code": "world_busy". Repeat the same call and it completes.
{"detail": "world 'save-1' is being processed; repeat the call in a moment", "code": "world_busy"}Responses of POST /v2/events
| Status | Meaning | Retry? |
|---|---|---|
| 200 | Held. The body is the acknowledgement, with deferred or refused_judgements when they apply. | Resend what missing lists. |
| 401 | Missing or invalid credential. | Not with the same key. |
| 403 | Neither 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. |
| 409 | The timeline exists with a different parent, or world_limit or timeline_limit. | Never unchanged. |
| 415 | The Content-Type is not application/json. | Never. Fix the header. |
| 422 | The 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. |
| 429 | event_rate_exceeded. | After Retry-After seconds. |
| 503 | The 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:
{
"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?"}
}
]
}curl -X POST https://director.eposyne.com/v2/events \
-H "Authorization: Bearer $EPOSYNE_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @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.
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.jsonDirector 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:
curl -i -X DELETE https://director.eposyne.com/v2/worlds/save-1 \
-H "Authorization: Bearer $EPOSYNE_API_KEY"