Tokdash API Reference

August 26, 2026 · View on GitHub

Tokdash exposes a local HTTP API (FastAPI) for querying token usage, costs, and session data across Claude Code, Codex, OpenClaw, and other supported tools.

  • Default bind: 127.0.0.1:55423
  • Start: tokdash serve --bind 127.0.0.1 --port 55423
  • OpenAPI schema: GET /openapi.json
  • Interactive docs: GET /docs (Swagger UI), GET /redoc

All endpoints return JSON. The API is unauthenticated and intended to bind to loopback only. State-changing requests are gated (loopback bind + Host/Origin allowlist + per-session token); see docs/SECURITY.md and PUT /api/pricing-db below.


Endpoint Summary

MethodPathPurpose
GET/healthLiveness probe (with a Tokdash fingerprint)
GET/api/versionRuntime version + setup install method
GET/api/csrf-tokenPer-session write token (loopback/same-origin only)
GET/api/update-checkOpt-in cached PyPI version check (read-only)
POST/api/update-check/consentPersist one-time update-check consent (write-gated)
GET/api/usageAggregated token usage and cost across all tools
GET/api/toolsPer-tool usage breakdown (coding apps only)
GET/api/quotaCurrent subscription quota state from local snapshots
GET/api/quota/historyQuota utilization and derived consumption history
POST/api/quota/consentPersist per-provider quota API consent (write-gated)
POST/api/quota/settingsPersist the quota master switch and poll interval (write-gated)
GET/api/quota/refreshRun an immediate consented quota API poll (read-only, cooldown)
GET/api/sessionsList sessions for a given tool
GET/api/sessionDetailed turns for a single session
GET/api/active-timeEstimated active time across every session tool
GET/api/codex/sessionsConvenience wrapper: Codex sessions
GET/api/codex/sessionConvenience wrapper: single Codex session
GET/api/openclawOpenClaw model breakdown
GET/api/statsAnnual stats aggregation
GET/api/pricing-dbCurrent pricing database snapshot
PUT/api/pricing-dbUpdate the pricing database (write-gated, requires token)
GET/Web dashboard (HTML)

Period parameter

Most endpoints accept a period query parameter. Supported values:

ValueMeaning
today (default)Current day (00:00 local time → now)
3daysLast 3 days
weekLast 7 days
14daysLast 14 days
monthCurrent calendar month (1st → today)
<integer>Last N days (e.g. "30" for 30 days)

For arbitrary ranges, use date_from and date_to (format YYYY-MM-DD) where supported.


GET /health

Liveness check. Carries a distinctive service/version fingerprint so a port probe can tell "this is Tokdash" rather than trusting a generic {"status":"ok"} any app could return.

Response

{ "status": "ok", "service": "tokdash", "version": "1.0.7" }

GET /api/version

Local version/provenance. install_method is read from the setup manifest (<data_dir>/install.json) when present, else null.

usage_db_schema_supported is the usage-database schema version this build can read — a compile-time constant, not a read of the database, so this route stays as cheap as /health. Comparing it across two Tokdash processes that share a data directory is how a version skew is spotted without opening the file; migrations run forward only, so the older build cannot read a database the newer one has migrated. tokdash doctor reports the schema actually stored on disk.

Response

{
  "service": "tokdash",
  "runtime_version": "1.0.7",
  "install_method": "pipx",
  "update_check_enabled": false,
  "usage_db_schema_supported": 9
}

GET /api/csrf-token

Issues the per-session write token the dashboard echoes back as X-Tokdash-Token on mutating requests. Returns 403 unless the server is loopback-bound and the request's Host/Origin are in the loopback allowlist (so a page on another localhost port cannot read it).

Response

{ "token": "<per-session-token>" }

GET /api/update-check

Opt-in, default-off PyPI version check (see docs/guides/ONBOARDING.md → Update checks). Read-only — PyPI read plus an in-memory cache, no disk write — so it is served as GET and is not write-gated; it works over Tailscale/WSL/any forward like GET /api/quota/refresh (see docs/SECURITY.md). No-op unless update checks are enabled (TOKDASH_UPDATE_CHECK=1 or saved consent). Result is cached for hours; never an automatic/background call, and it only reports — it never runs an upgrade.

Response (enabled)

{ "enabled": true, "current": "1.0.7", "latest": "1.0.8", "update_available": true, "error": null, "cached": false }

Response (disabled)

{ "enabled": false, "update_available": false }

POST /api/update-check/consent

Persists one-time consent (update_check: true) to <data_dir>/config.json so update checks are enabled. Write-gated like all mutations. TOKDASH_UPDATE_CHECK=0 remains a hard kill switch that overrides saved consent.

Response

{ "enabled": true }

GET /api/quota

Returns current subscription quota state. This route never performs provider network I/O; it reads the local quota_snapshots table (and local plan/tier metadata). Session files are not scanned here — the background poller ingests them. Provider API polling is default-off and happens only through GET /api/quota/refresh, the background poller after consent, or tokdash quota poll.

enabled is the quota master switch (config.json quota.enabled, default true, forced false by the TOKDASH_QUOTA_POLL=0 kill switch). When it is false the dashboard renders an enable quota tracking card instead of provider data. poll.interval is the effective interval in seconds and poll.interval_source is one of env / config / default.

Response shape

{
  "providers": {
    "codex": {
      "network_enabled": false,
      "plan": "pro",
      "buckets": [
        {"bucket": "5h", "bucket_label": "5-hour window", "used_percent": 25.0, "resets_at": 1782910800}
      ]
    }
  },
  "consent": {
    "credential_scan": false,
    "codex_api": false,
    "claude_api": false,
    "antigravity_api": false,
    "minimax_api": false,
    "kimi_api": false,
    "grok_api": false
  },
  "enabled": true,
  "poll": {
    "enabled": true,
    "network_enabled": false,
    "interval": 1800,
    "interval_source": "default",
    "interval_minutes": 30,
    "interval_choices": [15, 30, 60, 120],
    "last_run": null,
    "kill_switch": false
  }
}

GET /api/quota/history

Returns stored quota utilization points and derived consumption deltas.

Query parameters

NameTypeRequiredDefaultDescription
providerscomma-separated stringnoallFilter providers, e.g. codex,claude
granularityhour or daynohourPeriod used to aggregate consumption deltas
startinteger epoch secondsnoInclusive lower bound
endinteger epoch secondsnoInclusive upper bound
max_pointsintegerno300Max points per series; series longer than this are evenly downsampled, always keeping the most recent point. Must be a positive integer.

History series are unified per (provider, bucket): a Codex session row (account default) and an API row (real account id) for the same window merge into one series, keeping the freshest point on a timestamp collision. MiniMax uses region-qualified bucket IDs so global and mainland-China Token Plans remain separate series. Series are always bounded by max_points (points and consumption deltas are downsampled independently).

POST /api/quota/consent

Persists the separate local-credential-read consent (credential_scan) and per-provider quota API network consent to <data_dir>/config.json. Network polling requires both credential_scan=true and the matching provider flag. Write-gated like all mutations. TOKDASH_QUOTA_POLL=0 remains a hard kill switch.

Request

{"credential_scan": true, "codex_api": true, "minimax_api": true, "kimi_api": true, "grok_api": true}

Response

{"consent": {"credential_scan": true, "codex_api": true, "claude_api": false, "antigravity_api": false, "minimax_api": true, "kimi_api": true, "grok_api": true}}

POST /api/quota/settings

Persists the quota master switch and background poll interval to <data_dir>/config.json (quota.enabled and quota.poll_interval_minutes). Both fields are optional. Write-gated like all mutations. A poll_interval_minutes outside [15, 30, 60, 120] returns 400.

Request

{"enabled": true, "poll_interval_minutes": 30}

Response

{"enabled": true, "config_enabled": true, "poll_interval_minutes": 30, "interval": 1800, "interval_source": "config"}

GET /api/quota/refresh

Runs an immediate network poll for consented providers and stores snapshots in the local usage DB. This only reads providers' usage endpoints (no quota is consumed), so it is served as GET, not write-gated, and works over Tailscale Serve/WSL/any forward — see SECURITY.md. It is still rate-limited with a 60 second cooldown (429). It never refreshes provider tokens; expired tokens produce stale-token snapshots. Returns 409 when quota tracking is disabled (master switch off or TOKDASH_QUOTA_POLL=0).

Response

{"snapshots": 3, "inserted": 3}

GET /api/usage

Aggregated token usage and cost across all configured tools.

Query parameters

NameTypeRequiredDefaultDescription
periodstringno"today"See Period parameter
date_fromstringnoStart date (YYYY-MM-DD). Overrides period when paired with date_to.
date_tostringnoEnd date (YYYY-MM-DD).

Response fields

FieldTypeDescription
periodstringPeriod that was queried
total_tokensintTotal tokens across all tools
total_costfloatTotal cost in USD
total_messagesintTotal assistant/user message count
by_toolobjectPer-tool aggregates: { tool_name: { tokens, cost } }
appsobjectPer-app detailed breakdown (includes tokens_in, tokens_out, tokens_cache, cost, messages, models[])
coding_appsobjectSame shape as apps, filtered to coding tools (excludes browser/research tools)
coding_modelsarrayFlat list of models from coding apps, each tagged with source
top_modelsarrayTop N models by token usage
openclaw_modelsarrayOpenClaw-specific model breakdown
combined_modelsarrayAll models from all sources, merged and ranked
comparisonobjectComparison vs previous period: tokens_prev, cost_prev, messages_prev, tokens_pct, cost_pct, messages_pct
timestampstringISO 8601 timestamp when the response was generated

Per-app object shape

{
  "tokens": 45990135,        // total tokens
  "tokens_in": 7786995,      // input tokens (non-cache)
  "tokens_out": 375005,      // output tokens
  "tokens_cache": 37828135,  // cache read + write tokens
  "cost": 39.52,             // USD
  "messages": 566,
  "models": [
    {
      "name": "anthropic/claude-opus-4-7",
      "tokens": 23980934,
      "tokens_in": 1468105,
      "tokens_out": 233771,
      "tokens_cache": 22279058,
      "cost": 26.15,
      "messages": 196
    }
  ]
}

Example

curl -s http://127.0.0.1:55423/api/usage?period=today | jq '{total_tokens, total_cost}'
# { "total_tokens": 71091234, "total_cost": 56.4 }

GET /api/tools

Per-tool breakdown limited to coding apps (excludes auxiliary tools like browser/research).

Query parameters

NameTypeRequiredDefault
periodstringno"today"

Response fields

FieldTypeDescription
total_tokensintSum across coding tools
total_costfloatSum in USD
total_messagesintMessage count
appsobjectSame per-app shape as /api/usage apps field

GET /api/sessions

List of sessions for a specific tool.

Query parameters

NameTypeRequiredDefaultDescription
toolstringyesTool name: codex, claude, opencode, pi_agent, omp, mimo, kimi, dsh, reasonix, zcode, kilocode, grok, hermes, antigravity_cli, or cline (OpenClaw is served only via /api/openclaw)
periodstringno"today"See Period parameter
date_fromstringnoStart date (YYYY-MM-DD)
date_tostringnoEnd date (YYYY-MM-DD)
include_review_sessionsbooleannofalseInclude Codex review / auto-permission sessions (hidden by default)

Response fields

FieldTypeDescription
toolstringEcho of tool param
tool_labelstringHuman-readable name (e.g. "Claude Code")
periodstringPeriod queried
latest_sessionobjectMost recent session (same shape as items in sessions[])
sessionsarrayAll sessions in the period, sorted by last_seen_at desc

Costs

cost is calculated when the response is built, from the billing inputs stored per turn (model, fresh input, cache reads, cache writes, output) and the pricing database this process has loaded. Two Tokdash builds sharing one usage database each report under their own pricing.

Editing a rate reprices codex, claude and kimi without rereading any source log: those are the tools whose parsed sessions are cached in the usage database, and pricing is no longer part of the signature that invalidates a cached row. opencode, pi_agent and mimo are read live from their own databases and logs on each cold request, keyed on the pricing signature among others, so a rate edit does make them reread.

Non-zero provider-reported costs from OpenCode, Pi and Mimo are kept verbatim — Pi from usage.cost.total, the other two from the message's cost. They are the provider's own figures, so no rate edit moves them. Zero means the provider reported nothing (plan and subscription accounts), and those turns are estimated from rates like any other.

Rows written before turns carried billing inputs — including rows kept by TOKDASH_USAGE_DB_DURABLE after their source log disappeared — are priced from their stored totals instead. That reproduces the same number under any rates, because every cached source billed a turn as get_cost(model, tokens_in, tokens_out, tokens_cache, 0), but it cannot separate a Claude or Kimi cache write from fresh input again. If a provider ever prices those apart, only reparsing those logs restores the distinction; a durable row whose log is gone keeps the combined figure.

Codex is the exception: it bills under provider/model and stores the bare name, and the pricing file keys some aliases by provider, so its rows from before that change are reparsed once rather than reused. A durable Codex row whose log is already gone cannot be, and prices under the bare model name — if that name later gets a provider-specific rate, that row keeps the unqualified one.

Session object shape

{
  "tool": "claude",
  "session_id": "5a8aafce-67f6-4e08-8963-c01eebf9f520",
  "project": "howard",
  "model": "claude-opus-4-7",
  "token_events": 14,         // number of recorded API calls
  "tokens_in": 72528,
  "tokens_cache": 1136969,
  "tokens_out": 6161,
  "tokens_reasoning": 0,
  "tokens": 1215658,           // sum of in + cache + out + reasoning
  "cache_ratio": 0.9353,       // tokens_cache / tokens (0.0–1.0)
  "cost": 1.085,
  "started_at": "2026-05-21T20:41:54.357000+00:00",
  "last_seen_at": "2026-05-21T20:44:22.891000+00:00"
}

GET /api/session

Detailed view of a single session including per-turn breakdown.

Query parameters

NameTypeRequiredDescription
toolstringyesTool name
session_idstringyesSession UUID

Response fields

FieldTypeDescription
sessionobjectSame shape as latest_session from /api/sessions
turnsarrayPer-turn token + cost records

Turn object shape

{
  "turn_index": 1,
  "model": "claude-sonnet-4-6",
  "tokens_in": 23361,
  "tokens_cache": 0,
  "tokens_out": 118,
  "tokens_reasoning": 0,
  "tokens": 23479,
  "cost": 0.0718,
  "timestamp": "2026-05-20T16:02:07.514000+00:00"
}

GET /api/active-time

Estimated active time across every session tool, for the Overview KPI. Kept separate from /api/usage because it reads every supported session source.

Query parameters

NameTypeRequiredDefaultDescription
periodstringno"today"See Period parameter
date_fromstringnoStart date (YYYY-MM-DD)
date_tostringnoEnd date (YYYY-MM-DD)
include_review_sessionsbooleannofalseInclude Codex review / auto-permission sessions
refreshbooleannofalseBypass the response cache and recompute, as on /api/usage. The dashboard's Refresh button sends this

Response fields

FieldTypeDescription
periodstringEcho of the period param
active_msintClock time any agent was working: overlapping sessions and tools count once
active_ms_sumintAgent time: per-stream intervals added up, so concurrent agents count separately
comparisonobject|nullThe same two figures for the previous window and the percentage change: {active_ms_prev, active_ms_sum_prev, active_ms_pct, active_ms_sum_pct}. A percentage is null when the previous window is empty, and the whole object is null if that window could not be read
by_toolobjectPer-tool {tool_label, session_count, active_ms, active_ms_sum}
unavailable_toolsarrayTools that could not be read or summarized for this window (excluded from the totals, so the rest still answer)
active_gap_cap_msintIdle cap in effect (TOKDASH_ACTIVE_GAP_CAP_SECONDS)
active_time_estimatedboolAlways true — see the method below
active_time_methodstring"capped-inter-event-gap"
include_review_sessionsboolThe effective setting applied, param or server default
timestampstringISO 8601 time the payload was computed

Both figures are estimates: each gap between a stream's token events counts up to the idle cap, so a short pause reads the same as work, one long operation is truncated at the cap, and a session with a single event measures zero. The same fields appear per tool in /api/sessions under summary, and per session in sessions[], where the union is over that session's agent streams only.

comparison covers the window immediately before this one — the previous day, month or N days, or for an explicit date_from/date_to the range of equal length ending where it begins. That is the same window /api/usage compares against, so the runtime delta on the Overview means what the token, cost and message deltas beside it mean. Computing it aggregates a second window; the response cache makes that a per-window cost rather than a per-request one.

{
  "period": "week",
  "active_ms": 331980000,       // 92h 13m of clock time
  "active_ms_sum": 494880000,   // 137h 28m of agent time
  "comparison": {               // the week before, on the same terms
    "active_ms_prev": 298620000,
    "active_ms_sum_prev": 421200000,
    "active_ms_pct": 11.2,
    "active_ms_sum_pct": 17.5
  },
  "by_tool": {
    "codex": {"tool_label": "Codex", "session_count": 38, "active_ms": 273780000, "active_ms_sum": 332820000}
  },
  "unavailable_tools": [],
  "active_gap_cap_ms": 300000,
  "active_time_estimated": true,
  "active_time_method": "capped-inter-event-gap",
  "include_review_sessions": false,
  "timestamp": "2026-08-14T14:31:05.412000"
}

GET /api/codex/sessions

Convenience wrapper for Codex sessions. Equivalent to /api/sessions?tool=codex.

Query parameters

NameTypeRequiredDefault
periodstringno"today"
include_review_sessionsbooleannofalse (Codex review / auto-permission sessions hidden by default)

GET /api/codex/session

Convenience wrapper for a single Codex session. Equivalent to /api/session?tool=codex&....

Query parameters

NameTypeRequired
session_idstringyes

GET /api/openclaw

OpenClaw-specific model breakdown.

Query parameters

NameTypeRequiredDefault
periodstringno"today"

Response fields

FieldTypeDescription
total_tokensintSum across all OpenClaw models
total_costfloatSum in USD
total_messagesintMessage count
modelsobject{ model_name: { tokens, tokens_in, tokens_out, tokens_cache, cost, messages } }

GET /api/stats

Yearly stats aggregation.

Query parameters

NameTypeRequiredDescription
yearintegernoYear to query. Defaults to current year if omitted.

GET /api/pricing-db

Returns the effective pricing database: the user override under TOKDASH_DATA_DIR when present (it fully replaces the baseline — WYSIWYG editor semantics), otherwise the packaged baseline. A corrupt override falls back to the baseline (never wipes pricing).

Response fields

FieldTypeDescription
pathstringWhere edits PERSIST — the override file under the data dir (<data_dir>/pricing_db.json)
baseline_pathstringThe read-only packaged baseline (…/site-packages/tokdash/pricing_db.json)
baseline_versionstring | nullThe shipped baseline's version, reported even when an override is active so a UI can warn when an override has drifted behind newer bundled pricing
sourcestring"override" if the data dir override is in effect, else "baseline"
dataobjectThe effective pricing database (versions, aliases, model rates)
textstringPretty-printed canonical JSON of data (trailing newline) — what the editor renders

Trade-off (by design). Because a saved override fully replaces the baseline, it also freezes future bundled pricing updates for the models it covers until you delete it. This is intentional — it keeps the editor WYSIWYG (a deletion stays deleted). Compare baseline_version against your override's version to decide when to re-fork; delete <data_dir>/pricing_db.json to return to the shipped baseline and resume receiving updates.

The data object contains:

  • version — pricing DB version
  • lastUpdated — ISO timestamp
  • note — description string
  • aliases{ alias: canonical_name } for model name normalization
  • models{ model_name: { input, output, cache_read, cache_write } } (USD per million tokens)

PUT /api/pricing-db

Saves pricing edits. Body must match the GET response data shape (or {"text": "<json>"}). Edits are written to the override file under TOKDASH_DATA_DIR (never the packaged baseline), so they survive tokdash update (a pip/pipx reinstall) and succeed on a read-only install. The override fully replaces the baseline once saved (so deletions stick); delete the override file to revert to the shipped defaults. Returns the same {path, baseline_path, baseline_version, source, data, text} shape as GET (with source: "override").

Write protection. As a state-changing endpoint it is gated (returns 403 otherwise):

  • the server must be bound to loopback;
  • Host (and any Origin/Referer) must be a loopback address in the allowlist;
  • the request must carry a valid X-Tokdash-Token (fetch it from GET /api/csrf-token).

The dashboard does this automatically. A scripted client must fetch the token first:

TOKEN=$(curl -s http://127.0.0.1:55423/api/csrf-token | jq -r .token)
curl -s -X PUT http://127.0.0.1:55423/api/pricing-db \
  -H "Content-Type: application/json" -H "X-Tokdash-Token: $TOKEN" \
  -d '{"data": { ... }}'

Integration Example: Claude Code Status Line

Ready-made templates: docs/guides/statusline/ ships a minimal and a full statusline script plus install/config notes. The snippet below is the minimal one, reproduced here for reference.

Tokdash's /api/usage endpoint is well suited for embedding daily totals into the Claude Code status line. The snippet below queries today's usage with a 1-second timeout, falls back silently if tokdash is unreachable, and renders a compact summary like 📊 69.9M (\$55.64) today.

Status line script (~/.claude/scripts/statusline.sh)

#!/bin/bash
input=$(cat)

MODEL=$(echo "$input" | jq -r '.model.display_name')
DIR=$(echo "$input" | jq -r '.workspace.current_dir')

# Fetch tokdash totals — fail silently if unreachable
TOKDASH_STR=""
TOKDASH_JSON=$(curl -s -m 1 "http://127.0.0.1:55423/api/usage?period=today" 2>/dev/null)
if [ -n "$TOKDASH_JSON" ]; then
  TODAY_TOKENS=$(echo "$TOKDASH_JSON" | jq -r '.total_tokens // 0' 2>/dev/null)
  TODAY_COST=$(echo "$TOKDASH_JSON" | jq -r '.total_cost // 0' 2>/dev/null)
  if [ -n "$TODAY_TOKENS" ] && [ "$TODAY_TOKENS" != "0" ]; then
    if [ "$TODAY_TOKENS" -ge 1000000 ]; then
      TOK_FMT=$(awk "BEGIN {printf \"%.1fM\", $TODAY_TOKENS/1000000}")
    elif [ "$TODAY_TOKENS" -ge 1000 ]; then
      TOK_FMT="$(( (TODAY_TOKENS + 500) / 1000 ))k"
    else
      TOK_FMT="$TODAY_TOKENS"
    fi
    COST_TODAY=$(printf '$%.2f' "$TODAY_COST")
    TOKDASH_STR=" | 📊 ${TOK_FMT} (${COST_TODAY}) today"
  fi
fi

echo "[$MODEL] 📁 ${DIR##*/}${TOKDASH_STR}"

Claude Code settings (~/.claude/settings.json)

{
  "statusLine": {
    "type": "command",
    "command": "bash ~/.claude/scripts/statusline.sh",
    "refreshInterval": 30
  }
}

refreshInterval (added in Claude Code 2.1.97) re-runs the script every N seconds so the totals stay live even while you're idle.

Output

[Claude Sonnet 4.6] 📁 myproject | 📊 69.9M (\$55.64) today

Notes

  • Keep the curl timeout small (-m 1) so the status line doesn't stall if tokdash is restarting.
  • The 📊 ... segment is omitted entirely when tokdash returns nothing — no error noise in the status bar.
  • For per-tool detail, swap in .by_tool.claude.tokens or similar from the same response.
  • For weekly/monthly totals, change period=today to period=week or period=month.

Other Integration Patterns

Shell alias for quick check

alias tokens-today='curl -s http://127.0.0.1:55423/api/usage?period=today | jq "{tokens: .total_tokens, cost: .total_cost, by_tool}"'

Polling for cost alerts

#!/bin/bash
# Warn when daily spend crosses \$50
COST=$(curl -s http://127.0.0.1:55423/api/usage?period=today | jq -r '.total_cost')
if (( $(echo "$COST > 50" | bc -l) )); then
  notify-send "Tokdash" "Daily spend has exceeded \$50 ($COST)"
fi

Prometheus / metrics scraping

For richer monitoring setups, the /api/usage JSON can be parsed by a small exporter sidecar. The comparison block gives period-over-period deltas without extra requests.