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.
Applications → New application. Applications separate systems and environments — events, manifests and keys never mix across them.
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.
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.
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
servicenames. The service string drives the colours and the legend — if checkout calls itselfcheckoutin one producer andcheckout-svcin another, one system shows up as two. - Derive
manifestIdfrom a shared correlation id. Every producer touching order 58291 must compute the same id (ORD-58291) — that is the entire grouping mechanism. - Prefix
eventKeywith the producer (payments:charge-91b2). Keys are unique per manifest, and a prefix makes cross-producer collisions impossible. - Keep one
opstyle —domain.verbreads best:order.created,charge.capture,risk.cleared. - Casing: contract fields are camelCase (
eventKey,startedAt— that part is fixed). Insiderequest/response/payload, send bodies VERBATIM as your system saw them — this is debugging data, re-casing it falsifies the record. Forpropertieskeys 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
Events are retained until their application is deleted — deleting an application removes its manifests, events and keys in one stroke.