Quickstart

Three steps from empty account to a live timeline. Anything you POST with the same manifestId lands on the same timeline — no schema registration.

1
Create an application

ApplicationsNew application. Applications separate systems and environments — events, manifests and keys never mix across them.

2
Generate an API key

Open the application and hit Generate API key. The full tlt_… key is shown once. Create one key per producer so a leak revokes narrowly.

3
Send an event

The manifest is created on first write and appears under Search within a second.

Paste it and every snippet on this page updates — then copy. It never leaves this page.

curl -X POST https://timeline.overdoser.org/api/events \
  -H "Authorization: Bearer tlt_…" \
  -H "Content-Type: application/json" \
  -d '{"manifestId":"MAN-2481","events":[{"service":"orders","op":"order.create","method":"POST","url":"/internal/orders"}]}'

The event object

The body is { manifestId, events: [...] }. Every event field is optional — each one only shapes how the event reads on the timeline.

manifestIdstringRequest-level, required. Groups the batch onto one timeline — any id you already have: a trace id, order number, job id. Created on first write.
eventKeystringYour correlation handle, unique within the manifest — choose it yourself ("order-1") whenever you plan to complete the event later. Generated (and returned in the response) when omitted.
servicestringEmitting service — colour-codes the event and feeds the service legend.
origin / destinationstringThe hop’s endpoints, shown as "origin → service" badges on the chip.
opstringThe label on the timeline. Keep it short: "order.create", "charge", "POST /orders".
messagestringOne-line human description of what happened — shown in the detail drawer and matched by search.
levelenumSeverity of a non-request event: debug · info · success · warning · error. Colours the badge the way status does for requests.
method / url / statusstring · numberHTTP shape of the hop. A status ≥ 400 renders as an error badge in the detail drawer. Keep the url bare — parameters go in query.
query / reqHeaders / resHeadersrecordThe rest of the HTTP envelope, rendered as key/value rows. Curate headers to the few that tell the story — authorization/cookie-class values are redacted at ingest either way.
request / responseJSONRequest and response BODIES, pretty-printed in the event-details and full-log drawers, each with a copy button.
payloadJSONGeneric body for events that aren’t requests — an order snapshot, a telemetry frame. Rendered as its own copyable block.
detailsarrayOrdered { label, value } rows for the detail drawer when you want curated display instead of one JSON blob. Optional mono / copy per row.
propertiesJSONFree-form extras nobody agreed on. Searchable from the homepage and matched by the timeline’s keyword highlight.
startedAtISO · epoch msWhen the event began — drives placement on the axis. Defaults to the server’s receive time.
endedAtISO · epoch msProvide it for a known duration. Omitted WITH your own eventKey the event renders in flight (growing) until completed; omitted without one it completes on arrival (end = start).
completebooleanMarks the message as the completion of the event with the same eventKey: response fields merge into the existing row and endedAt is stamped.

The three shapes of an event

1 · A moment. No eventKey, no endedAt — the event completes on arrival and renders as a point at its timestamp:

{"manifestId":"MAN-2481","events":[{"service":"orders","op":"order.created","startedAt":1753948800000}]}

2 · A known duration. Send startedAt and endedAt together — complete from the start, one bar spanning the range:

{"manifestId":"MAN-2481","events":[{"service":"media","op":"transcode","startedAt":"2026-07-31T09:38:12.244Z","endedAt":"2026-07-31T09:38:24.500Z"}]}

3 · In flight, completed later. Choose an eventKey and omit endedAt — the event renders as a dashed bar growing live. Close it with a second message carrying the same key and "complete": true: the outcome merges in and the end time is stamped. The two messages may come from different processes and arrive in any order — still one bar, real duration.

{"manifestId":"MAN-2481","events":[{"eventKey":"charge-91b2","service":"payments","op":"charge","startedAt":1753948800000}]}
{"manifestId":"MAN-2481","events":[{"eventKey":"charge-91b2","complete":true,"status":200,"endedAt":1753948802371,"response":{"state":"captured"}}]}

The response returns { id, eventKey } per event, in input order — so even generated keys are yours to complete against. Batches are transactional: a 2xx means every event stored, an error means none.

Timestamps: send your own. The clock closest to the event is the truest, so best practice is explicit startedAt/endedAt — as in every example above. Both formats work: epoch milliseconds, or ISO-8601 with an explicit timezone (…Z or ±hh:mm— zone-less strings are rejected rather than guessed). Anything omitted is stamped by the nearest hop instead: the client library at call time, the relay at local receipt, or (for direct calls) this API at arrival — never later, so a buffering relay can’t leak queue time into your durations. A completion is also clamped to its start, so clock skew can never produce a negative duration.

One timeline, many producers

The same manifest can collect events from an HTTP middleware, a background worker, a shell script and an AI agent at once — the timeline renders each event from whatever fields it carries, so producers never need to look alike. What DOES need agreement is naming. The contract validates types; these conventions keep a mixed timeline readable:

  • Agree on service names. The service string drives the colours and the legend — if checkout calls itself checkout in one producer and checkout-svc in another, one system shows up as two.
  • Derive manifestId from a shared correlation id. Every producer touching order 58291 must compute the same id (ORD-58291) — that is the entire grouping mechanism.
  • Prefix eventKey with the producer (payments:charge-91b2). Keys are unique per manifest, and a prefix makes cross-producer collisions impossible.
  • Keep one op styledomain.verb reads best: order.created, charge.capture, risk.cleared.
  • Casing: contract fields are camelCase (eventKey, startedAt — that part is fixed). Inside request/response/payload, send bodies VERBATIM as your system saw them — this is debugging data, re-casing it falsifies the record. For properties keys you invent, stick with camelCase so search terms stay guessable.
  • Send the least that tells the story. Every field is optional; the named fields exist so that when you have the data there is exactly one right place for it. Headers especially: allowlist the few that matter — authorization/cookie-class values are redacted at ingest, but noise isn’t.

One boundary: manifests are traces — correlated flows of tens to hundreds of events. High-frequency metrics or unbounded log streams belong in a metrics or logging system, not on a timeline.

Live updates

Nothing to integrate on the reading side: an open timeline holds a server-sent-events stream and renders new events within a frame of ingest, and the search page re-runs itself whenever any of your applications receives events — new manifest ids appear without a reload.

Search

The homepage search is fuzzy on manifest ids (typos still match) and full-text across services, origins, destinations, ops, URLs, messages and properties. Exact matches rank first; recency breaks ties.

Node & browser client

@overdoser/timeline-client (npm) wraps the ingest contract for TypeScript and JavaScript projects — zero dependencies, ESM + CJS, Node 18+ and browsers. begin/complete correlate into one bar with the real duration.

npm install @overdoser/timeline-client
import { createTimelineClient } from '@overdoser/timeline-client';

const timeline = createTimelineClient({ url: 'https://timeline.overdoser.org', apiKey: 'tlt_…' });

timeline.event('ORDER-2481', { service: 'orders', op: 'order.create', status: 201 });

timeline.begin('ORDER-2481', 'charge-91b2', { service: 'payments', op: 'charge' });
// … work …
timeline.complete('ORDER-2481', 'charge-91b2', { status: 200 });

Ship through a relay

For latency-sensitive producers, run the relay sidecar next to your application: it acks a local POST /events in under a millisecond, then batches events to this API in the background with retry and backoff — a slow network never blocks your app. The image is public.

docker run -d --name my-app-relay -p 7740:7740 \
  -e TIMELINE_URL=https://timeline.overdoser.org \
  -e RELAY_API_KEY=tlt_… \
  cracoidic/timeline-relay

The relay speaks the same contract as this API, so pointing your integration at http://localhost:7740instead of the API is the only change. A request’s own Bearer tlt_… key always wins over RELAY_API_KEY, so one relay can serve one app with a baked-in key or many apps sending their own. GET /health reports queue depth and forwarding counters.

For AI agents (MCP)

Agents report implementation stages onto a timeline through @overdoser/timeline-mcp (npm) — timeline_send for finished steps, timeline_begin / timeline_complete for stages that render live while they run. No checkout or build needed.

Claude Code

claude mcp add timeline -e TIMELINE_URL=https://timeline.overdoser.org -e TIMELINE_API_KEY=tlt_… -- npx -y @overdoser/timeline-mcp

…or any MCP client via .mcp.json

{
  "mcpServers": {
    "timeline": {
      "command": "npx",
      "args": ["-y", "@overdoser/timeline-mcp"],
      "env": {
        "TIMELINE_URL": "https://timeline.overdoser.org",
        "TIMELINE_API_KEY": "tlt_…"
      }
    }
  }
}

Codex CLI

# ~/.codex/config.toml
[mcp_servers.timeline]
command = "npx"
args = ["-y", "@overdoser/timeline-mcp"]
env = { TIMELINE_URL = "https://timeline.overdoser.org", TIMELINE_API_KEY = "tlt_…" }

When a call omits manifest, events default to <project-folder>-<date>derived from the server’s working directory — one shared config separates projects automatically. Set TIMELINE_MANIFEST to pin it, or have the agent pass manifest per feature. TIMELINE_URL may also point at a local relay.

Limits & guarantees

1000
Events per ingest request (one manifest per request)
sha-256
How keys are stored — the full key is shown only once, at creation
SSE
Live updates — open timelines and the search page refresh as events arrive

Events are retained until their application is deleted — deleting an application removes its manifests, events and keys in one stroke.