POST /events ingests one event; Duraton matches it against every workflow trigger in the project and
returns what it started. Every ingested event is also kept in a durable
event log you can read and tail.

Send an event
- TypeScript
- REST API
Request body
app, runner, targetApp, dedupeId, and session are each bounded at 256 characters; a longer
value, a blank name, or unparseable data returns 400. tags is bounded too: at most 20 tags per
run, each key up to 64 characters matching [A-Za-z0-9_.-] and each value up to 256 characters - an
over-limit tag returns 400.
tags on an event is sendable from the TypeScript SDK - SendEventInput carries a tags field -
as well as over REST. The tags attach to every run the event triggers; filter those runs back with
tag.<key> (see Run tags).Unsupported characters
An event must not carry a NUL character (\u0000). Duraton’s durable event log has no
representation for it, so an event carrying one could never be stored. Duraton rejects it at ingest
with a 400 - before any workflow is matched or any run is started - rather than accepting the event
and failing later.
The check covers every text field (name, app, runner, dedupeId, targetApp, session) and
the whole data payload, at any depth: a NUL inside a nested string, an array element, or even a
JSON object key is rejected.
Only the escaped form
\u0000 reaches this check - a raw NUL byte in the request body is already
invalid JSON and is rejected as a malformed body. Either way the event is refused with a 400, and
nothing is recorded.Response
202 Accepted. The body reports what the event did:
An event that matches nothing still returns
202 with an empty triggered - it is recorded, not lost.
Each triggered entry’s runId resolves to a run, and that run records the event back: its
eventId is this event’s id (see runs). List every run one event fanned out
with GET /runs?eventId=<id>. Cron, child (step.runWorkflow), and on-failure runs carry no
eventId - they have no triggering event.
Two kinds of deduplication
Two independent mechanisms can each reportdeduped: true, and they mean different things. Knowing which
one fired matters, because one throws the event away and the other only suppresses a single run.
In short:
dedupeId is event-level and total - the exact same event is collapsed to a no-op, nothing
is recorded, and nothing is woken. It is the safety net for an at-least-once caller retrying POST /events.
idempotency is run-level and partial - the event still lands, still wakes waiters, and still runs
every other matching workflow; only a second run of the idempotent workflow for the same derived key is
suppressed within the window.
Because both surface deduped: true, tell them apart by the rest of the response: an event-level drop
returns no triggered array and no runId, whereas an idempotency drop still carries the event’s
triggered fan-out with deduped set on the affected entry.
Reading the log
The log records every event with what it did, newest first.
Each record carries the event (
name, app, data), its source (api for an external POST,
emit for a workflow’s step.emit), when it arrived, and the triggered fan-out:
The stream is a best-effort live view - a slow or reconnecting client can miss events.
GET /events
is the complete record.