Skip to content

HTTP API reference

The REST API is served by stackunderflow init on http://localhost:8081 by default. Interactive Swagger docs (auto-generated by FastAPI) are available at /docs. This file is the human-written companion — it explains intent, request/response shapes, and status codes in plain language. All endpoints return JSON. Errors use the shape {"error": "<message>"} with an appropriate non-2xx status code.


MethodPathGroup
GET/api/projectProjects
POST/api/projectProjects
POST/api/project-by-dirProjects
GET/api/projectsProjects
GET/api/recent-projectsProjects
GET/api/global-statsProjects
GET/api/statsDashboard data
GET/api/dashboard-dataDashboard data
GET/api/messagesDashboard data
GET/api/messages/summaryDashboard data
POST/api/refreshDashboard data
GET/api/jsonl-filesSessions
GET/api/jsonl-contentSessions
GET/api/searchSearch
POST/api/search/reindexSearch
GET/api/search/statsSearch
GET/api/qaQ&A
GET/api/qa/{id}Q&A
POST/api/qa/reindexQ&A
GET/api/qa/statsQ&A
GET/api/tagsTags
GET/api/tags/browse/{tag}Tags
GET/api/tags/session/{id}Tags
POST/api/tags/session/{id}Tags
DELETE/api/tags/session/{id}/{tag}Tags
POST/api/tags/reindexTags
GET/api/bookmarksBookmarks
POST/api/bookmarksBookmarks
GET/api/bookmarks/{id}Bookmarks
PUT/api/bookmarks/{id}Bookmarks
DELETE/api/bookmarks/{id}Bookmarks
POST/api/bookmarks/toggleBookmarks
GET/api/bookmarks/session/{id}Bookmarks
GET/api/healthMisc
GET/api/pricingMisc
POST/api/pricing/refreshMisc

Returns the currently selected project. When no project has been set, status is "no_project".

Response (no project selected)

{"status": "no_project", "message": "No project selected"}

Response (project active)

{
"status": "active",
"project_path": "Users/yadkonrad/dev/myproject",
"log_path": "/Users/yadkonrad/.claude/projects/-Users-yadkonrad-dev-myproject",
"log_dir_name": "-Users-yadkonrad-dev-myproject"
}

Status codes: 200 always.


Set the active project by filesystem path. The server locates the Claude log directory automatically.

Request body

{"project_path": "/Users/yadkonrad/dev/myproject"}

Response

{
"status": "success",
"project_path": "/Users/yadkonrad/dev/myproject",
"log_path": "/Users/yadkonrad/.claude/projects/-Users-yadkonrad-dev-myproject",
"message": "Project set successfully. You can now view the dashboard."
}

Status codes: 200 success; 400 missing or non-existent project_path; 404 project path exists but no Claude logs were found there.


Set the active project using the Claude log directory slug (the ~/.claude/projects/<slug> folder name). This is the endpoint the React UI calls when the user picks a project from the sidebar.

Request body

{"dir_name": "-Users-yadkonrad-dev-dev-year26-jan26-StackUnderflow"}

Response

{
"status": "success",
"project_path": "Users/yadkonrad/dev/dev/year26/jan26/StackUnderflow",
"log_path": "/Users/yadkonrad/.claude/projects/-Users-yadkonrad-dev-dev-year26-jan26-StackUnderflow",
"log_dir_name": "-Users-yadkonrad-dev-dev-year26-jan26-StackUnderflow",
"message": "Now analyzing logs from: -Users-yadkonrad-dev-dev-year26-jan26-StackUnderflow"
}

Status codes: 200 success; 400 missing dir_name or path traversal attempt; 404 directory not found or contains no .jsonl files.


List all known projects from the session store with metadata. Supports sorting and pagination.

Query parameters

NameTypeDefaultDescription
include_statsboolfalseInclude per-project statistics (slower)
sort_bystringlast_modifiedOne of last_modified, first_seen, size, name
limitintnoneMax results to return
offsetint0Skip this many results (for pagination)

Response

{
"projects": [
{
"dir_name": "-Users-yadkonrad-dev-myproject",
"log_path": "/Users/yadkonrad/.claude/projects/-Users-yadkonrad-dev-myproject",
"file_count": 0,
"total_size_mb": 0.0,
"last_modified": 1776649168.04,
"first_seen": 1772585609.12,
"display_name": "-Users-yadkonrad-dev-myproject",
"in_cache": false,
"url_slug": "-Users-yadkonrad-dev-myproject",
"stats": null
}
],
"total_count": 143,
"has_more": true,
"cache_status": {"cached_count": 0, "total_projects": 143}
}

Status codes: 200 always (errors return 500 with {"error": "..."}).


Shorthand for the most recent 20 projects — equivalent to /api/projects?sort_by=last_modified&limit=20 but with a simplified response shape.

Response

{
"projects": [
{
"dir_name": "-Users-yadkonrad-dev-myproject",
"log_path": "",
"last_modified": 1776649168.04,
"file_count": 0
}
]
}

Status codes: 200 always.


Aggregated statistics across all projects in the store. This is the only dashboard endpoint that does not require a project to be selected. The Overview page calls this endpoint exclusively.

See the Data Shapes Appendix for the full field reference.

Response (abbreviated)

{
"first_use_date": "2025-11-29",
"last_use_date": "2026-04-20",
"daily_token_usage": [{"date": "2025-11-29", "input": 0, "output": 0}],
"daily_costs": [{"date": "2025-11-29", "cost": 0.0, "by_model": {}}],
"models": {
"claude-opus-4-6": {"count": 57584, "cost": 29402.30}
},
"total_cache_read_tokens": 16328970052,
"total_cache_write_tokens": 1126449344,
"config": {"max_date_range_days": 30}
}

Status codes: 200 success; 500 on database error.


Project scoping: /api/stats, /api/dashboard-data, /api/messages, and /api/messages/summary all act on the current project — the one most recently set via POST /api/project-by-dir or POST /api/project. If no project has been selected they return 400 {"error": "No project selected"}. /api/global-stats (above) is the only aggregate endpoint that does not require a project. POST /api/refresh refreshes the current project when one is selected; if no project is set it refreshes all projects instead.


Full statistics object for the current project, computed via the pipeline (classifier → enricher → aggregator).

Query parameters

NameTypeDefaultDescription
timezone_offsetint0Minutes offset from UTC for daily bucketing

Response — the top-level statistics dict (same as the statistics key in /api/dashboard-data). Key nested sections include overview, tools, sessions, daily_usage, models, costs.

{
"overview": {
"project_name": "StackUnderflow",
"log_dir_name": "-Users-yadkonrad-dev-dev-year26-jan26-StackUnderflow",
"total_messages": 11341,
"date_range": {"start": "2026-01-30T20:58:11.193Z", "end": "2026-04-20T01:39:11.887Z"},
"sessions": 20,
"message_types": {"user": 4381, "assistant": 6960},
"total_tokens": {
"input": 600390, "output": 3030862,
"cache_creation": 86450124, "cache_read": 1466551362
},
"total_cost": 3767.61
},
"tools": {
"usage_counts": {"Bash": 1296, "Read": 700, "Edit": 531},
"error_counts": {},
"error_rates": {"Bash": 0.0}
}
}

Status codes: 200 success; 400 no project selected; 404 project not in store (run /api/refresh).


Optimised single call for the initial dashboard load. Returns statistics plus the first page of messages in a single round-trip.

Query parameters — same timezone_offset as /api/stats.

Response

{
"statistics": {"overview": {"...": "..."}, "tools": {"...": "..."}},
"messages_page": {
"messages": [{"...": "..."}],
"page": 1,
"per_page": 50,
"total": 11341
},
"message_count": 11341,
"is_reindexing": false,
"config": {
"messages_initial_load": 50,
"max_date_range_days": 30
}
}

Status codes: 200 success; 400 no project; 404 project not in store.


Return pipeline-formatted messages for the current project.

Query parameters

NameTypeDefaultDescription
limitintnoneCap the number of messages returned
timezone_offsetint0UTC offset in minutes

Response — a JSON array of message objects.

[
{
"session_id": "020a13e5-ad60-41e5-9313-7cdf03cecf26",
"role": "user",
"content": "What does this function do?",
"timestamp": "2026-01-30T20:58:11.193Z",
"model": null,
"input_tokens": 0,
"output_tokens": 0
}
]

Status codes: 200 success; 400 no project; 404 project not in store.


Lightweight summary of the current project’s messages — counts and model breakdown without loading the full message list.

Response

{
"total": 11341,
"by_type": {"user": 4381, "assistant": 6960},
"by_model": {
"N/A": 4381,
"claude-opus-4-6": 3566,
"claude-opus-4-7": 1455
},
"total_tokens": 3631252
}

Status codes: 200 success; 400 no project; 404 project not in store.


Runs an incremental ingest pass and updates the store. If a project is active, only that project is re-ingested. If no project is selected, all projects are refreshed.

Request body — any JSON object (can be {}); the body is forwarded to the ingest layer but not currently validated.

Response (project active)

{
"status": "success",
"message": "Files changed - data refreshed successfully",
"files_changed": true,
"message_count": 12,
"refresh_time_ms": 340
}

Response (no project — all projects)

{
"status": "success",
"message": "Ingested 42 new records",
"files_changed": true,
"refresh_time_ms": 1820,
"projects_refreshed": 42,
"total_projects": 42
}

Status codes: 200 always (errors are surfaced inside the JSON body or as 500).


List every session (JSONL file) for the current project, with per-session metadata and cost estimates. Accepts an optional project query parameter to override the current project.

Query parameters

NameTypeDefaultDescription
projectstringcurrent projectLog directory slug to query

Response — array of session objects, sorted by created timestamp ascending.

[
{
"name": "020a13e5-ad60-41e5-9313-7cdf03cecf26.jsonl",
"path": "020a13e5-ad60-41e5-9313-7cdf03cecf26.jsonl",
"is_subagent": false,
"created": 1738274291.193,
"modified": 1738274291.193,
"size": 0,
"messages": 120,
"user_messages": 40,
"assistant_messages": 80,
"input_tokens": 24000,
"output_tokens": 96000,
"model": "claude-opus-4-6",
"title": "What does this function do?",
"tool_calls": 15,
"estimated_cost": 0.4812
}
]

Status codes: 200 success; 400 no project; 500 internal error.


Return the raw parsed messages for a single session, identified by filename.

Query parameters

NameTypeRequiredDescription
filestringyesFilename, e.g. 020a13e5-....jsonl
projectstringnoLog dir slug (defaults to current project)

Response

{
"lines": [{"type": "user", "message": {"role": "user", "content": "..."}}],
"total_lines": 120,
"user_count": 40,
"assistant_count": 80,
"metadata": {
"session_id": "020a13e5-ad60-41e5-9313-7cdf03cecf26",
"file_size": 0,
"created": 1738274291.193,
"modified": 1745116760.887,
"first_timestamp": "2026-01-30T20:58:11.193Z",
"last_timestamp": "2026-04-20T01:39:20.887Z",
"duration_minutes": 117.6,
"cwd": "/Users/yadkonrad/dev/myproject"
}
}

Status codes: 200 success; 400 no project or invalid file param; 404 project or session not found in store; 500 internal error.


Full-text search across every indexed session from every configured adapter (Claude Code today), using SQLite FTS5. Returns 503 if the search service failed to initialise on startup.

Query parameters

NameTypeDefaultDescription
qstring""Search query
projectstringnoneFilter to a specific project slug
date_fromstringnoneISO date YYYY-MM-DD (inclusive lower bound)
date_tostringnoneISO date YYYY-MM-DD (inclusive upper bound)
modelstringnoneFilter by model name
rolestringnoneFilter by role (user or assistant)
pageint1Result page (1-indexed)
per_pageint20Results per page (max 100)

Response

{
"results": [
{
"id": 238980,
"session_id": "7f72e05c-93a2-4221-9a3b-ce5648e0b433",
"project": "-Users-yadkonrad-dev-myproject",
"role": "assistant",
"content": "Here is the refactored version...",
"timestamp": "2026-03-23T20:58:44.155Z",
"model": "claude-opus-4-6",
"tokens_input": 1,
"tokens_output": 176,
"snippet": "...Here is the <mark>refactored</mark> version..."
}
],
"total": 233,
"page": 1,
"per_page": 20,
"total_pages": 12,
"query": "refactor"
}

Status codes: 200 success; 503 search service unavailable; 500 query error.


Rebuild the full-text search index from all project data in the store. This is a blocking operation.

Request body — empty object {}.

Response

{
"projects_indexed": 98,
"total_messages_indexed": 82759,
"elapsed_ms": 4230.5
}

Status codes: 200 success; 503 search service unavailable; 500 reindex error.


Return metadata about the search index — total message count, known models, and a per-project index log.

Response

{
"total_messages": 82759,
"total_projects": 98,
"models": ["claude-opus-4-6", "claude-sonnet-4-6", "claude-opus-4-7"],
"indexed_projects": [
{
"project": "-Users-yadkonrad-dev-myproject",
"indexed_at": "2026-04-02T02:48:28.332578+00:00",
"message_count": 13525
}
]
}

Status codes: 200 success; 503 search service unavailable; 500 error.


List extracted Q&A pairs with filtering and pagination. A Q&A pair is a (user question, assistant answer) tuple extracted from sessions. Returns 503 if the Q&A service failed to initialise.

Query parameters

NameTypeDefaultDescription
projectstringnoneFilter to a specific project slug
date_fromstringnoneISO date YYYY-MM-DD
date_tostringnoneISO date YYYY-MM-DD
searchstringnoneText filter applied to question and answer text
resolution_statusstringnoneOne of resolved, looped, open
pageint1Page number (1-indexed)
per_pageint20Results per page (max 100)

Response

{
"results": [
{
"id": "edaf6f427ec0a5de",
"session_id": "14b76974-d5c3-4430-b7e6-db7513bb1499",
"project": "-Users-yadkonrad-dev-myproject",
"question_text": "help me find this chrome tab plugin...",
"answer_text": "Let me search for the ObserverTab extension...",
"code_snippets": [],
"tools_used": [],
"timestamp": "2026-04-01T00:07:45.083Z",
"model": "claude-opus-4-6",
"num_attempts": 1,
"resolution_status": "open",
"loop_count": 0
}
],
"total": 4986,
"page": 1,
"per_page": 20,
"total_pages": 250
}

Status codes: 200 success; 503 Q&A service unavailable; 500 error.


Fetch a single Q&A pair by its hex ID.

Path parameter: qa_id — the hex string ID from the id field in /api/qa results.

Response — same shape as a single element from /api/qa’s results array.

Status codes: 200 success; 404 pair not found; 503 service unavailable; 500 error.


Rebuild the Q&A index by re-extracting pairs from all sessions in the store.

Request body — empty object {}.

Response

{
"projects_indexed": 98,
"total_qa_indexed": 4986,
"elapsed_ms": 8120.3
}

Status codes: 200 success; 503 service unavailable; 500 error.


Return aggregate statistics about the Q&A index, including a per-project breakdown.

Response (abbreviated)

{
"total_pairs": 4986,
"by_project": [
{"project": "-Users-yadkonrad-dev-myproject", "count": 1310}
]
}

Status codes: 200 success; 503 service unavailable; 500 error.


Return the full tag cloud — all tags with their session counts, category, and display colour. Returns 503 if the tag service failed to initialise.

Response

{
"tags": [
{"name": "database", "count": 130, "category": "topic", "color": "#dd6b20"},
{"name": "python", "count": 78, "category": "language", "color": "#3572A5"},
{"name": "Bash", "count": 101, "category": "tool", "color": "#718096"}
],
"total_sessions": 144
}

Tag category values observed: topic, language, framework, tool.

Status codes: 200 success; 503 service unavailable; 500 error.


List every session that carries a given tag.

Path parameter: tag — the tag name (e.g. python, debugging).

Response

{
"tag": "python",
"sessions": [
{"session_id": "020a13e5-...", "project": "-Users-yadkonrad-dev-myproject"}
],
"count": 78
}

Status codes: 200 success; 503 service unavailable; 500 error.


Return all tags attached to a specific session.

Path parameter: session_id — UUID of the session.

Response — illustrative; shape determined by tag service (source: routes/tags.py:28)

["python", "debugging", "fastapi"]

Status codes: 200 success; 503 service unavailable; 500 error.


Manually add a tag to a session.

Path parameter: session_id — UUID of the session.

Request body

{"tag": "my-custom-tag"}

Response — illustrative; shape determined by tag service (source: routes/tags.py:55)

{"status": "added", "tag": "my-custom-tag", "session_id": "020a13e5-..."}

Status codes: 200 success; 400 missing tag; 503 service unavailable; 500 error.


DELETE /api/tags/session/{session_id}/{tag}

Section titled “DELETE /api/tags/session/{session_id}/{tag}”

Remove a manually added tag from a session.

Path parameters: session_id (UUID), tag (tag name).

Response — illustrative (source: routes/tags.py:63)

{"status": "removed", "tag": "my-custom-tag", "session_id": "020a13e5-..."}

Status codes: 200 success; 503 service unavailable; 500 error.


Rebuild auto-tags for all sessions across all projects.

Request body — empty object {}.

Response

{
"projects_indexed": 98,
"total_sessions_tagged": 144,
"elapsed_ms": 3100.8
}

Status codes: 200 success; 503 service unavailable; 500 error.


List all bookmarks, optionally filtered by tag and sorted.

Query parameters

NameTypeDefaultDescription
tagstringnoneFilter to bookmarks carrying this tag
sort_bystringcreated_atSort field

Response

{
"bookmarks": [
{
"id": "bm_abc123",
"session_id": "020a13e5-...",
"title": "Great refactor session",
"notes": "Shows the clean pipeline pattern",
"tags": ["python", "refactoring"],
"message_index": 42,
"created_at": "2026-03-01T12:00:00Z",
"session_first_ts": "2026-03-01T09:00:00Z",
"session_last_ts": "2026-03-01T11:55:00Z",
"session_message_count": 120
}
]
}

The session_first_ts, session_last_ts, and session_message_count fields are enriched from the session store and may be absent if the session is not in the store.

Status codes: 200 success; 503 service unavailable; 500 error.


Create a new bookmark.

Request body

{
"session_id": "020a13e5-ad60-41e5-9313-7cdf03cecf26",
"title": "Great refactor session",
"message_index": 42,
"notes": "Shows the clean pipeline pattern",
"tags": ["python", "refactoring"]
}

session_id is required. All other fields are optional (title defaults to "Untitled bookmark").

Response — the created bookmark object (same shape as one element in GET /api/bookmarks).

Status codes: 201 created; 400 missing session_id; 503 service unavailable; 500 error.


Fetch a single bookmark by ID. Illustrative — behaviour delegated to the bookmark service (source: routes/bookmarks.py).

Status codes: 200 success; 404 not found; 503 service unavailable; 500 error.


Update a bookmark’s title, notes, and/or tags.

Path parameter: bookmark_id — the bookmark’s ID string.

Request body — all fields optional; only provided fields are updated.

{
"title": "Updated title",
"notes": "New notes",
"tags": ["python"]
}

Response — the updated bookmark object.

Status codes: 200 success; 404 not found; 503 service unavailable; 500 error.


Remove a bookmark by ID.

Response

{"status": "success", "message": "Bookmark removed"}

Status codes: 200 success; 404 not found; 503 service unavailable; 500 error.


Add a bookmark if the session is not already bookmarked, or remove it if it is. Useful for a single-click “star” button.

Request body

{
"session_id": "020a13e5-ad60-41e5-9313-7cdf03cecf26",
"title": "Interesting session",
"message_index": 0
}

session_id is required. title and message_index are optional.

Response — illustrative (source: routes/bookmarks.py:161)

{"action": "added", "bookmark": {"id": "bm_abc123", "session_id": "020a13e5-..."}}

or {"action": "removed", "bookmark_id": "bm_abc123"} when the bookmark existed.

Status codes: 200 success; 400 missing session_id; 503 service unavailable; 500 error.


List all bookmarks attached to a specific session.

Path parameter: session_id — UUID of the session.

Response

{"bookmarks": [{"id": "bm_abc123", "title": "Great session", "...": "..."}]}

Status codes: 200 success; 503 service unavailable; 500 error.


Liveness and readiness check. Reports whether each optional service initialised successfully.

Response

{
"status": "ok",
"services": {
"search": true,
"tags": true,
"qa": true,
"bookmarks": true,
"pricing": true
}
}

false for a service means it failed to start (the corresponding /api/<service>/* endpoints will return 503).

Status codes: 200 always.


Return the current per-model pricing table. Data is fetched from LiteLLM and cached locally.

Response

{
"pricing": {
"claude-opus-4-6": {
"input_cost_per_token": 1.5e-05,
"output_cost_per_token": 7.5e-05,
"cache_creation_cost_per_token": 1.875e-05,
"cache_read_cost_per_token": 1.5e-06
}
},
"source": "litellm",
"timestamp": "2026-04-20T01:39:47.194182+00:00",
"is_stale": false
}

Status codes: 200 success; 503 service unavailable; 500 error.


Force a re-fetch of pricing data from LiteLLM, bypassing the local cache.

Request body — empty object {}.

Response

{"status": "success", "message": "Pricing updated successfully"}

Status codes: 200 success; 500 fetch failed or service error; 503 service unavailable.


Global Stats shape (GET /api/global-stats)

Section titled “Global Stats shape (GET /api/global-stats)”

This endpoint is the primary data source for the Overview page. The authoritative implementation is in stackunderflow/store/queries.py:get_global_stats(). The config key is appended by the route handler.

FieldTypeDescription
first_use_datestringISO date YYYY-MM-DD of the earliest message in the store
last_use_datestringISO date YYYY-MM-DD of the most recent message
daily_token_usagearrayPer-day {date, input, output} token totals across all projects
daily_costsarrayPer-day {date, cost, by_model} cost rollup; by_model maps model name to cost float
modelsobjectMap of model name to {count, cost} across all time
total_cache_read_tokensintSum of cache_read_tokens across every message in the store
total_cache_write_tokensintSum of cache_create_tokens across every message in the store
configobjectServer config values surfaced to the UI; currently {max_date_range_days}

daily_token_usage element

{"date": "2026-03-23", "input": 24000, "output": 96000}

daily_costs element

{
"date": "2026-03-23",
"cost": 1.44,
"by_model": {
"claude-opus-4-6": 1.20,
"claude-sonnet-4-6": 0.24
}
}

models entry

{
"claude-opus-4-6": {"count": 57584, "cost": 29402.30}
}

count is the number of messages attributed to the model; cost is the cumulative USD cost computed using the pricing table.