API and MCP tool reference
searchcode enterprise has one HTTP API, and everything uses it: the web UI, your scripts, and the MCP tools your agents call. This page is generated from the same definitions as the router, so it describes what the server actually accepts.
Every instance also serves this reference itself, with no credential needed:
the rendered page at /api/v1/docs and the OpenAPI document at
/api/v1/openapi.json. Neither is built from your estate, so publishing them
discloses nothing about your repositories.
The basics
- Base path. Every endpoint is under
/api/v1, on the API listener (server.listen, port 8080 by default). The UI is a separate listener. - Credentials. Present an API key, minted on your
/accountpage, as eitherX-Api-Key: <key>orAuthorization: Bearer <key>. An OAuth access token is accepted on MCP only. - MCP. The MCP endpoint is
/v1/mcpon the same listener, over streamable HTTP. Each MCP tool is a thin wrapper over one endpoint below: a tool call is the REST call, with the same principal, authorisation, rate limit and audit record. Every principal sees the same tool list. - Gaps. Every response that can be partially answered carries
gaps, and it is never null. An empty result with no gaps means nothing matched. An empty result with gaps means some of the estate could not be read, and each gap says what and why. - 404, never 403, for what you cannot see. A repository you are not allowed to read answers exactly like one that does not exist, so a guessed name confirms nothing.
- Unknown parameters are refused. A misspelled or unsupported parameter is a 400 naming what is accepted, never silently ignored. Ranking is tuned by the operator, not per request.
- Result limits.
max_resultshas a default and a hard maximum. Asking for more than the maximum is not an error: you get the maximum, the true total, and a gap naming the offset that reaches the rest.
Search
POST /api/v1/search
Search code across the estate
MCP tool: search. Authentication: API key or OAuth access token
One content search across every repository the caller may see, merged into one ranked set with one total.
CHOOSE A GRANULARITY FIRST — each rung is roughly an order of magnitude cheaper than the next, so this one parameter decides whether an answer costs 200 tokens or 20,000:
repos — repository names and match counts, no paths, no content. Roughly a hundredth the cost of snippets. Use it for "does anyone use this at all", which is the question this product exists to answer.
files — paths and per-file counts, no content. Roughly a tenth the cost of snippets. Use it for "which files".
snippets — ranked snippets with a little context. THE DEFAULT. Use it for "how does this work".
lines — every matching line, with context lines around each. The most expensive rung by an order of magnitude. Use it for "show me every use", and set line_limit.
Two mistakes to avoid, both common: OVER-SEARCHING. Once you have identified a file, READ IT with read_file rather than searching it again. Repeated searches against one file cost more than the file. OVER-NARROWING. Terms are ANDed, so four terms usually return nothing. Start with one or two terms and narrow with the filters below — never with more terms. An empty result means fewer terms, not more.
A ZERO IS NEVER BARE. When a query of plain terms matches nothing, notices says why: which of your words matched no file at all, and how many files each of the others is in. Read it before concluding anything. An empty result whose notice says some terms matched nothing is NOT evidence that the code is absent — it is evidence that one of your words was, and the same question asked without that word frequently answers it.
Every result carries the repository it came from, at every granularity. Across an estate, attributing code to the wrong service is the expensive mistake.
Snippets come back as hunks: contiguous runs of lines, each with the line number of its first line, so no line is ever returned twice however close two matches are. A gap between two hunks means lines were SKIPPED — the code is not contiguous, and reading it as though it were is a wrong conclusion about the file. match_lines says which lines matched and match_ranges says where within them, as byte offsets the engine reported — so you never have to guess which part of a line the query hit, and a line absent from match_ranges still matched.
Terms are ANDed by default. OR, NOT and (grouping) are supported, "quoted phrases" match literally, /regex/ matches as a regular expression and term~1 matches fuzzily. In-query filters are file:, path:, lang:, ext:, repo: and tech:, each accepting = and != and comma-separated multi-values.
PRECEDENCE TRAP: an in-query filter binds to the ADJACENT TERM ONLY. a OR b path:src parses as a OR (b AND path:src), so matches for a leak in from anywhere in the estate. Use the top-level path, file, language and include_ext parameters when you want the filter ANDed against the WHOLE query.
Ranking is tuned server-side by the operator and is not a parameter. There is no gravity, profile or ranker option, and passing one is refused rather than ignored — an agent that believes it is tuning something has been misled.
Request body:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
query |
body | string | no | The search query. Optional only in the sense that a filter may stand in for it: give a query, or scope the request with repo, language, path or tech and get a listing of what is in scope. Terms are ANDed by default. OR, NOT and (grouping) are supported, "quoted phrases" match literally, /regex/ matches as a regular expression and term~1 matches fuzzily. In-query filters are file:, path:, lang:, ext:, repo: and tech:, each accepting = and != and comma-separated multi-values. PRECEDENCE TRAP: an in-query filter binds to the ADJACENT TERM ONLY. a OR b path:src parses as a OR (b AND path:src), so matches for a leak in from anywhere in the estate. Use the top-level path, file, language and include_ext parameters when you want the filter ANDed against the WHOLE query. |
||
granularity |
body | string | no | snippets |
How much of the answer to return, cheapest first: repos (names and counts only), files (paths and counts), snippets (the default), lines (every matching line). Each rung costs roughly ten times the one before it, so start coarse and go finer only once you know what you are looking for. Per-file technologies are on the files rung, and only there: ask for it when you need to know what a matched file uses. One of: repos | files | snippets | lines. | |
path |
body | string | no | Only match files whose path contains this. ANDed against the WHOLE query, unlike the in-query filter of the same name, which binds only to the term beside it. | ||
file |
body | string | no | Only match files whose name contains this. ANDed against the WHOLE query, unlike the in-query filter of the same name, which binds only to the term beside it. | ||
language |
body | string | no | Only match files in this language, as scc names it. ANDed against the WHOLE query, unlike the in-query filter of the same name, which binds only to the term beside it. | ||
include_ext |
body | array of string | no | Only match files with these extensions, without the dot. ANDed against the WHOLE query, unlike the in-query filter of the same name, which binds only to the term beside it. | ||
technology |
body | string | no | Only match files that use this technology, as the detector names it — e.g. "Django", "AWS SDK (Boto3)". The names in the technologies facet on this response are exactly the accepted values. It is the named form of the tech: filter and composes with it. Attribution comes from the analysis pass, so a repository whose deep pass has not finished reports a gap rather than dropping out silently. | ||
repo |
body | string | no | Restrict the search to one repository, by identifier or by unique short name. Narrows within what the principal may already see; it can never widen it. | ||
case_sensitive |
body | boolean | no | false |
Match case exactly. Off means case-insensitive. | |
code_filter |
body | string | no | Restrict matching to one structural part of a file. only-declarations is jump-to-definition and only-usages is find-callers; both are far cheaper than reading files and are the right first call when you know which you want. One of: only-code | only-comments | only-strings | only-declarations | only-usages. | ||
repositories |
body | array of string | no | Restrict the search to these repositories, by identifier or unique short name. The plural form of repo, and like it, it can only narrow what the principal may already see. | ||
context_lines |
body | integer | no | 2 |
How many lines either side of a match to return. Zero means none: the matching lines and nothing around them. Matches whose windows overlap or touch share one hunk rather than repeating each other's lines. | |
context |
body | integer | no | Alias for context_lines, named as cs names it. With granularity=lines and context=0 this is cs's grep mode exactly. | ||
line_limit |
body | integer | no | 500 |
With granularity=lines, the total number of matching lines to return across all files. The rung is exhaustive by design, so this is the bound that keeps it usable. | |
max_results |
body | integer | no | 50 |
1000 | Cap on returned files. The total is reported before this cap, so you can tell a truncated answer from a complete one. It is a default a caller may raise, up to a hard maximum of 1000 — raise it to 50-100 for broad discovery, and page past the maximum with offset rather than asking for more. |
offset |
body | integer | no | 0 |
How many of the ranked files to skip before listing. With max_results it is a page: offset 0 is the first page, offset 20 with max_results 20 is the second. Counted in FILES, like files_total and unlike total, which counts matches. It is an offset and not a cursor: each request is a fresh query, so if a repository is refreshed between two pages a file may be seen twice or missed once — page numbers stay meaningful, and no result set is pinned per caller. An offset past the end is an empty files array and not an error, because files_total is the only way to know where the end was and it may move. It does not change total, files_total, technologies or languages, all of which describe the whole match set. Accepted at granularity files and snippets, which list files; refused at repos and lines, whose rows are repositories and matching lines rather than files. | |
max_matches_per_file |
body | integer | no | 5 |
Cap on matches returned within one file. | |
backend_detail |
body | boolean | no | false |
Name the search backend for EVERY repository searched rather than only for the ones that contributed results. A diagnostic switch: the response then carries one entry per repository this principal may see, which on a large estate is most of the answer. Leave it off unless you are working out why a query was slow. |
Response fields:
| Field | Type | Description |
|---|---|---|
total |
integer | Matches across every repository searched, BEFORE max_results truncation, and counting only repositories this principal may see. |
files_total |
integer | Matching FILES, before max_results truncation. Different from total, which counts matches: one file holding forty matches is one entry in files. This is the number to page on, and total is the number to quote. When truncated is true it is a lower bound. |
granularity |
string | The rung this response was shaped at, echoed so a caller can tell a defaulted answer from one it asked for. |
elapsed_ms |
integer | How long the SEARCH took, in milliseconds: the query across every repository this principal may see, whichever backend answered for each. It excludes authorisation, the technology filter, the facet counts and serialisation, so it is a lower bound on the time a client waited and a statement about the engine rather than about the request. Compare it across runs of the same query to see a regression; do not compare it across granularities, which read different amounts. |
repositories |
array | With granularity=repos, one entry per matching repository and nothing else. Absent at every other rung. |
repository |
string | The repository identifier. |
name |
string | Its human-facing name. |
matches |
integer | How many matches it holds. |
files |
integer | How many of its files matched. |
repositories_total |
integer | With granularity=repos, how many repositories matched before max_results truncation. Absent at every other rung. This is the rung's own denominator: total counts MATCHES and files_total counts FILES, so neither of them is the number to quote beside a census of repositories. It is exact whether or not truncated is true: fewer repositories rows than this means max_results cut the list, and nothing else does. |
lines |
array | With granularity=lines, every matching line with its context. Absent at every other rung. |
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
path |
string | Path relative to the repository root. |
line |
integer | 1-based line number. |
content |
string | The matching line. |
context_before |
array of string | Lines above. |
context_after |
array of string | Lines below. |
offset |
integer | The offset this page was read at, echoed so a paging caller can check what it got rather than what it asked for. |
truncated |
boolean | Files matched beyond the window this response returned — beyond offset+max_results, or beyond line_limit at granularity=lines. Compare total against what was returned to see by how much. On the last page of a paged read it is false, so it says "there is more after this" rather than "this query matched more than one page". At granularity=repos it is ALSO true when the census tally bound was reached, which cuts each row's files and matches counts rather than the list and is stated as a complete_results gap; whether the list itself was cut is repositories against repositories_total. |
files |
array | The matching files, ranked across repositories. Present at granularity files, snippets and lines; at files the hunks array is empty, because that rung returns no content. |
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
path |
string | Path relative to the repository root. |
language |
string | The language scc detected. |
score |
number | Ranking score. Higher is better. |
total_matches |
integer | Matches in this file before max_matches_per_file truncation. |
technologies |
array of string | What THIS file uses, as the detector names it — the join the technologies facet beside it cannot make, because that facet counts over every file the query matched and this response lists one page. Present at granularity=files only, and empty rather than absent when nothing is attributable to the file: an empty array says the analysis pass looked and found nothing, where an absent one would say it was not asked. Pass one back as technology to narrow to the files using it. |
hunks |
array | The returned matches as contiguous runs of lines, the way grep -A/-B prints them. Read this one. Matches whose context windows overlap or touch share a hunk, so no line is ever returned twice; matches far enough apart are separate hunks, so a gap between two hunks means lines were skipped and the code is NOT contiguous. |
start_line |
integer | 1-based line number of the first line in lines. |
end_line |
integer | 1-based line number of the last line in lines. |
lines |
array of string | The hunk's lines in file order, without their terminators. The line number of lines[i] is start_line + i. |
match_lines |
array of integer | The line numbers within this hunk that the query matched, ascending. Absolute file line numbers, not offsets into lines. |
match_ranges |
array | WHERE within those lines the query matched, from the search engine rather than from re-scanning the line for the query's words — so a phrase, a fuzzy term and a case-folded term are all marked exactly where they hit. Each entry names its own line, so this is never read positionally against match_lines. match_lines stays the authority on WHICH lines matched: a matching line may be absent here, which means the match has no reportable offset and not that the line did not match. At most 100 ranges are reported for one line; total_matches is the count. |
line |
integer | Absolute file line number, one of match_lines. |
ranges |
array | The [start, end) BYTE offsets into that line's text, ascending, end exclusive. Bytes and not characters, so a line with multi-byte UTF-8 above the match is still indexed correctly. Two ranges may overlap where two query terms do. |
backends |
object | How many repositories each search backend answered for, keyed by backend name — the backend is chosen per repository at query time, so a single query routinely uses more than one. The counts SUM to the repositories searched, which is every repository this principal may see after repo and repositories narrowed it. It is an aggregate rather than a row per repository because a row per repository grows with the estate and not with the answer: on 3,000 repositories that was roughly 174 KB on every query, most of a tool call's whole byte budget, spent restating a repository list that list_repositories already answers. For the repositories that actually contributed, read backends_by_repository. |
backends_by_repository |
object | Which backend answered for each repository that CONTRIBUTED results, keyed by repository identifier — the set anybody debugging a slow or surprising answer is asking about. Bounded by the matches the search SCANNED rather than by the estate or by this page: up to the first 5000 matching files, so it can name repositories whose results are past max_results and not on this page. Pass backend_detail=true to get every repository searched instead. Present and empty when nothing matched. |
technologies |
object | Detected technology to the number of MATCHING FILES using it, counted over every file this query matched and not over the estate — it says what THESE results are built on, so every entry narrows to at least one of them when passed back as tech:. A technology no matching file uses is absent rather than zero. Counted over the whole match set rather than over the files this response lists, so it does not change with max_results. It IGNORES tech: and honours every other filter, so passing an entry back as tech: — which REPLACES the selection rather than adding to it — returns exactly the count shown, and a technology already selected does not hide the alternatives to it. Present at granularity files and snippets, which are the rungs that list files; absent at repos and lines. For the estate-wide rollup, call estate. |
languages |
object | Language to the number of MATCHING FILES scc detected as that language — so the count equals files_total when that language is passed back as the language parameter. Counted over the files this response lists over, which is every file the query matched with EVERY filter applied including language itself: unlike technologies above, a request that already names a language reports only that language here. An object, unlike the languages array on estate and repo, which is a rollup of lines and complexity rather than a facet over a result set. Present at granularity files and snippets; absent at repos and lines. |
notices |
array of string | Statements that change what this answer MEANS, and on this endpoint that is why a zero is a zero. Terms are ANDed, so one word that matches nothing returns nothing — and a bare 0 does not say which word it was. When a query of plain terms matches no file, this names the three causes apart: some terms matched nothing (drop them), no term matched anything (the estate does not contain this), or every term matched but no one file holds them all (ask for fewer words, or quote a phrase). It carries a per-term count of the FILES that hold each word, counted over the repositories you can search and under the filters in force. Present and EMPTY on any populated answer, and on a zero the engine cannot take apart — a facet-only browse, or a query using OR, NOT or a regular expression. It is not a gap: nothing was skipped and nothing was unanswerable, so the zero is a complete and correct answer that was simply missing its reason. DO NOT REPORT "no such code exists" off an empty result that carries one of these sentences. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: Find the retry helper, declarations only
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
-X POST \
-H 'Content-Type: application/json' \
-d '{"query": "retry backoff", "code_filter": "only-declarations", "max_results": 5}' \
"$SEARCHCODE_URL/api/v1/search"
{
"total": 2,
"files": [
{
"repository": "example__deploy-tool",
"commit_sha": "4f0c2e1b9a7d…",
"path": "retry.go",
"language": "Go",
"score": 41.2,
"total_matches": 1,
"hunks": [
{"start_line": 11, "end_line": 13,
"lines": ["// RetryWithBackoff retries with exponential backoff.",
"func RetryWithBackoff(ctx context.Context, attempts int) error {",
"\tvar err error"],
"match_lines": [12],
"match_ranges": [{"line": 12, "ranges": [[5, 10], [14, 21]]}]}
]
}
],
"backends": {"brute-force": 22, "caisson": 8},
"backends_by_repository": {"example__deploy-tool": "brute-force"},
"technologies": {"Go Modules": 1},
"languages": {"Go": 1},
"elapsed_ms": 12,
"notices": [],
"gaps": []
}
POST /api/v1/find-usages
Which files use a technology, with the evidence
MCP tool: find_usages. Authentication: API key or OAuth access token
The dependency index: every file across the visible estate that uses a named technology, with the line and the matched keyword that attributed it.
Evidence is returned rather than asked to be trusted — an agent can decide whether to believe an attribution or go and read the line. Whole files are scanned, not just their first hundred lines, so a deep import is found.
Request body:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
technology |
body | string | yes | The technology name, as the detector reports it — e.g. "Django", "AWS SDK (Boto3)". Matched exactly and case-sensitively, so boto3 finds nothing. The detected vocabulary is the technologies map on estate_summary. |
||
repo |
body | string | no | Restrict to one repository, by identifier or unique short name. | ||
offset |
body | integer | no | 0 |
How many usages to skip before listing. With max_results it is a page: offset 0 is the first page, offset 50 with max_results 50 is the second. max_results is a hard maximum rather than a default, so this is the only way to reach what lies past it. It is an offset and not a cursor: each request is a fresh read, so a row added or removed between two pages may be seen twice or missed once. An offset past the end is an empty list and not an error, because total is the only way to know where the end was and it may move. It does not change total, which counts the whole set before either parameter is applied. | |
max_results |
body | integer | no | 50 |
1000 | Cap on returned usages. A HARD MAXIMUM of 1000 rather than a default a caller raises: a larger value is clamped to it and the response says so in gaps. total counts every usage before it bit, and offset reaches the rest. |
Response fields:
| Field | Type | Description |
|---|---|---|
offset |
integer | The offset this page was read at, echoed so a paging caller can check what it got rather than what it asked for. |
usages |
array | One entry per file that uses the technology. |
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
technology |
string | The technology that was matched. |
path |
string | Path relative to the repository root. |
language |
string | The language scc detected. |
line |
integer | 1-based line the evidence was found on. |
evidence |
string | The matched keyword, so the attribution can be checked rather than trusted. |
confidence |
string | exact or probable. |
version |
string | The resolved version, absent when no manifest could answer. An absent version means unknown rather than none. |
version_source |
string | lock, declared or inferred. A lock says what IS installed; a declared range says what MIGHT be, and only the first supports a vulnerability claim. |
version_confidence |
string | exact when a lockfile pinned it and the import-to-package mapping is known, probable when either half was a convention. Separate from confidence above, which is about whether this file uses the technology at all. |
manifest_path |
string | The manifest the version came from, relative to the repository root — "resolved from services/api/go.mod" can be opened and checked. |
total |
integer | Usages found before max_results truncation. Compare it against the length of usages to see how much was left out. |
truncated |
boolean | More usages matched than max_results returned. False means usages holds every match, so total is the whole answer rather than a lower bound. |
notices |
array of string | Sentences that change what this answer MEANS, above the data rather than beside it. Present and empty on an answer that needs none. A zero carries one when the detector declares the technology you named and no file you can see uses it: that is a COMPLETE answer about the code, and it is deliberately not a gap, because a gap says the question could not be answered. A name no detector rule declares is the opposite case and appears in gaps instead — the zero then says something about the spelling and nothing about the estate. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: Everything that imports boto3
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
-X POST \
-H 'Content-Type: application/json' \
-d '{"technology": "AWS SDK (Boto3)", "max_results": 3}' \
"$SEARCHCODE_URL/api/v1/find-usages"
{"usages": [{"repository": "example__data-pipeline", "commit_sha": "7c9a...", "technology": "AWS SDK (Boto3)", "path": "app/deep_import.py", "language": "Python", "line": 154, "evidence": "boto3", "confidence": "probable"}], "total": 1, "truncated": false, "notices": [], "gaps": []}
GET /api/v1/findings
Code quality AND security findings, one surface
MCP tool: findings. Authentication: API key or OAuth access token
Both kinds of finding come from here, and kind partitions them. The hosted product splits these across two calls and merges them in the client; that split is deliberately not propagated.
The deep analysis pass trails ingestion, so a repository that has not been analysed yet appears in gaps rather than contributing zero findings.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
query | string | no | Restrict to one repository. Repository identifier, clone URL, owner/name, or a unique name suffix. |
||
path |
query | string | no | Restrict to one file, relative to the repository root. Filtered before max_results, so a file's findings are never truncated away by a large repository's. | ||
kind |
query | string | no | Which ENGINE found it: security is the secret scanner, quality is the code-quality rules, and the two sum to total. Omit for both. Not the same axis as category, which is what the rule is ABOUT — see it. One of: security | quality. | ||
severity |
query | string | no | Only findings at this severity. One of: error | warning | info. | ||
category |
query | string | no | What the rule is ABOUT, across BOTH engines. A different axis from kind, and they collide on one word: category=security INCLUDES the quality rules that are about security, so it is a superset of kind=security and is the larger of the two — measured on one estate, 6,687 against 699. One of: accessibility | concurrency | correctness | deprecated | maintainability | modernization | performance | safety | security. | ||
tests |
query | string | no | exclude |
Findings in TEST files — test.go, test/, tests/, spec/, tests/, testdata/, *.test.ts, *.spec.js, *Test.java, *Tests.cs, test.py, conftest.py, *_spec.rb and the like, by one definition both products share. exclude, the default, leaves them out of findings and total and counts them in tests_excluded; include returns every finding, each carrying test; only returns just the test-file ones. They are kept, not deleted: a real credential committed in a test fixture is still a real credential, and include reaches it. One of: exclude | include | only. | |
offset |
query | integer | no | 0 |
How many findings to skip before listing. With max_results it is a page: offset 0 is the first page, offset 50 with max_results 50 is the second. max_results is a hard maximum rather than a default, so this is the only way to reach what lies past it. It is an offset and not a cursor: each request is a fresh read, so a row added or removed between two pages may be seen twice or missed once. An offset past the end is an empty list and not an error, because total is the only way to know where the end was and it may move. It does not change total, which counts the whole set before either parameter is applied. | |
max_results |
query | integer | no | 50 |
1000 | Cap on returned findings. A HARD MAXIMUM of 1000 rather than a default a caller raises: a larger value is clamped to it and the response says so in gaps. total counts every finding before it bit, and offset reaches the ones past this page. |
Response fields:
| Field | Type | Description |
|---|---|---|
offset |
integer | The offset this page was read at, echoed so a paging caller can check what it got rather than what it asked for. |
findings |
array | The findings, most severe first. |
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
path |
string | Path relative to the repository root. |
line |
integer | 1-based line the finding is on. |
rule |
string | The rule identifier that fired. A security rule's id comes from the betterleaks rule set vendored into this binary and is stable for the life of a release; a quality rule's is this product's own. A trailing .N — 69 of the 468 security rules carry one — numbers the TOKEN FORMATS one vendor issues, not revisions of a rule: pinecone-api-key.1 is the UUID format and pinecone-api-key.2 the pcsk one, and they carry different confidences and therefore different severities. So FILTER ON THE WHOLE ID, never on the stem before the suffix: a format added upstream arrives as a new sibling, and a stem match would silence or count a rule nobody has looked at. |
kind |
string | Which engine found it: security or quality. |
category |
string | What the rule is about. A quality finding may be category security. |
severity |
string | error, warning or info. |
message |
string | What the rule found, in prose. |
test |
boolean | The finding is in a test file. Only ever true under tests=include or tests=only, because the default leaves test-file findings out. Read from the path, so it is the same answer for a finding however long ago it was analysed. |
cwe |
string | The weakness class the RULE looks for, as CWE-89. Only on a security finding — a secret, or a quality rule whose category is security — and absent on every other finding by scope, not because it has no weakness. Absent too on a security finding whose rule names no single weakness class. The label is never quotable on its own: whenever any row carries one, the first entry of notices says what it does and does not claim, and a CWE count from this product is not comparable with one from a data-flow tool. |
cwe_fit |
string | Present with cwe. exact: the matched text is itself the weakness. candidate: the text is the operation the CWE names, and whether it is the weakness depends on something the pattern cannot see, such as whether untrusted input reaches it or whether a matched secret is live. |
detection |
string | Present with cwe. Always pattern: a lexical match with no data flow or taint analysis. |
rule_precision |
string | Present with cwe. The rule's measured precision and where it was measured, or unmeasured. It is the rule's, not this finding's. |
owasp |
string | The OWASP Top 10 2021 category OWASP's own published mapping places cwe in, as A03:2021. Absent when that mapping does not place it, rather than a guessed category. |
tests_excluded |
integer | Findings that matched every other filter and were left out because they are in test files and tests was exclude, the default. Zero under include and only. When it is not zero, notices says so in a sentence as well. |
total |
integer | Findings matching this query before max_results truncation, across every repository this principal may see. It is the same number the estate summary reports for the same filter, and it is NOT the length of findings — compare the two to see how much a cap left out. |
truncated |
boolean | More findings matched than max_results returned. False means findings holds every match at this filter, which is the only state in which the answer can be quoted as complete. |
notices |
array of string | Statements that change what these findings MEAN, and which a consumer must show its reader. Three can appear here. A repository whose stored analysis was produced by an earlier version of the secret-scanning rule set says so, says that a re-analysis is queued, says that findings may be ADDED and none removed when it completes, and says how many repositories are still pending — the answer is complete under the rules that produced it, so it is NOT a gap. A repository that has not been analysed at all says so in as many words, and says that this is not a statement that it is clean; that one is also reported in gaps, which names which repositories. The last is ALWAYS present whenever security findings are in scope: every finding is read from the current checkout at HEAD, so a secret that was committed and later removed is still in the git history and is not reported. Asking for kind=quality is the only thing that suppresses it. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: Security findings only
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/findings?kind=security&max_results=2"
{"findings": [{"repository": "example__billing-service", "commit_sha": "9b1c...", "path": "deploy/signing.pem", "line": 1, "rule": "private-key", "kind": "security", "category": "security", "severity": "error", "message": "Identified a Private Key, which may compromise cryptographic security and sensitive data encryption.", "test": false, "cwe": "CWE-321", "cwe_fit": "candidate", "detection": "pattern", "rule_precision": "unmeasured", "owasp": "A02:2021"}], "tests_excluded": 0, "total": 1, "truncated": false, "notices": ["About the weakness labels: a CWE here names the weakness class the RULE looks for ...", "Findings come from the current checkout at HEAD. A secret committed earlier and since removed is still in the git history and is NOT reported here."], "gaps": []}
Estate
GET /api/v1/repos
List the repositories this principal can reach
MCP tool: list_repositories. Authentication: API key or OAuth access token
Every repository the calling principal may search, with its size in lines and bytes and the capabilities each source tier can answer for. A repository that is not listed is either absent from the estate or invisible to this principal; the two are deliberately indistinguishable.
Rank the estate from this one call rather than analyzing each repository: lines and bytes come off the same inventory pass analyze reads, and include=languages adds the per-language rollup when the question is what the code is written in rather than where it is. A row whose inventory pass has not run carries neither field, and the gap on this response names it — absent is not zero.
Read the capabilities before asking a history question. A directory source has no commits, so blame and log over one are gaps rather than empty answers.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
group |
query | string | no | Only repositories in this repository group. | ||
label |
query | string | no | Only repositories carrying this label. Labels are categorisation and grant nothing. | ||
name_like |
query | string | no | Only repositories whose name CONTAINS this, case-insensitively. A substring match and deliberately not a glob or a regex: the question this answers is "which repositories are called something like kai", and a caller that guesses at kai* versus *kai* gets the same answer either way. Matched against the name and the identifier, so a trailing segment works whichever form the caller has. Use it instead of searching the estate for a name — a content search pays index cost to answer a directory question. |
||
include |
query | string | no | Add the per-language breakdown to every repository row, the same array analyze returns for one repository. OFF BY DEFAULT and opt-in deliberately: lines and bytes are two integers a row and are always there, and this is a variable-length array whose size is the number of languages a repository is written in — on a large estate that is the difference between a listing an agent can hold and one it truncates. Ask for it when the question is what the estate is written in; leave it off when the question is where the code is. One of: languages. | ||
offset |
query | integer | no | 0 |
How many repositories to skip before listing. With max_results it is a page: offset 0 is the first page, offset 100 with max_results 100 is the second. It is an offset and not a cursor: each request is a fresh read, so a repository added or retired between two pages may be seen twice or missed once. An offset past the end is an empty repositories array and not an error. It does not change total, which describes the whole filtered set. | |
max_results |
query | integer | no | 100 |
1000 | Cap on repositories returned in one page. total is reported before this cap, so a truncated answer is distinguishable from a complete one. It is a default a caller may raise, up to a hard maximum of 1000. An estate larger than that is read with offset rather than in one request. The default is a page rather than everything because an estate of a few thousand repositories is one response no client renders and no model reads to the end of. |
Response fields:
| Field | Type | Description |
|---|---|---|
repositories |
array | The visible repositories, sorted by name. This page only: offset and max_results decide which slice of total it is. |
id |
string | Stable identifier, derived from the clone URL — github.com__owner__name. Accepted wherever {repo} appears. |
name |
string | Human-facing name, usually owner/name. |
source |
string | git or directory. |
commit_sha |
string | The checked-out commit, or "unavailable" for a directory source. |
groups |
array of string | The repository groups authorisation is granted over. |
labels |
array of string | Categorisation and facets. They grant nothing. |
phase |
string | cloning, ready, no_content, empty, skipped or failed. A repository still cloning is answerable for nothing yet; no_content cloned and walked cleanly and kept no files, so it is not searchable and is not a failure. |
files_indexed |
integer | How many files the corpus walk kept for this repository. ZERO is the answer this field exists for: a repository whose every file was excluded contributes nothing to search, findings or the technology catalogue, and before this field the only way to find one was to read every repository's state off disk. It is a FILE count and does not balance against the admin endpoint's files_skipped, which counts skip EVENTS — a skipped directory is one event whatever it holds. |
lines |
integer | Total lines the inventory pass measured, the same number analyze returns for this repository. ABSENT rather than zero when that pass has not reached it — a zero here would rank an unmeasured repository as a small one, and the inventory gap on this response names every row it is missing from. Use it with bytes to decide where the code is before deciding what to read: files_indexed counts FILES, so five files of generated JSON report the same 5 as five files of source. |
bytes |
integer | Total source bytes the inventory pass measured, the same number analyze returns. Absent on the same terms as lines, and the better of the two for ordering an estate a language at a time: lines varies by an order of magnitude between languages and bytes does not. |
languages |
array | The per-language rollup, largest first — ONLY when include=languages was asked for, and absent otherwise. The same array analyze returns for one repository, so 'what is the estate written in, by repository' is one call rather than one call per repository. |
language |
string | scc's name for the language. |
files |
integer | Files in it. |
lines |
integer | Lines in it, including blanks and comments. |
code |
integer | Lines of code in it. |
complexity |
integer | Summed cyclomatic complexity. |
refresh_interval |
string | How often this repository is fetched, as a duration string ("1h0m0s"). It is the repository's own setting where it has one and indexing.refreshInterval otherwise, and it is what skip_reason's "checked again on the ordinary refresh interval" means. Absent when it is not known, never zero: a zero on a sentence about waiting reads as "constantly". |
skip_reason |
string | Why this repository is not ready, in ingestion's own words — over the configured size limit, a remote with no commits, or the error a clone that has never succeeded last failed with. ABSENT rather than empty when there is none, because a ready repository was not skipped and an empty string in a field you render reads as a reason that says nothing. It is the same sentence the absence gap on this response carries, exposed as a field so it does not have to be parsed back out of one. |
capabilities |
object | Which questions this source can answer: search, dependencies, findings, history, blame, coupling, commit_sha. |
repository_groups |
array | Every repository group this principal can see something in, sorted by name. A repository group is the unit authorisation is granted over, so this is the vocabulary the group parameter above accepts — and it is deliberately NOT narrowed by the group or label parameters, so a caller that has already filtered can still see what else it could ask for. |
name |
string | The group name, as it appears in each repository's groups. |
description |
string | The operator's description of what the group is for. Empty when none was configured, which is ordinary. |
repositories |
integer | How many visible repositories are in this group. Counted over everything visible, so it is what you get by asking for this group. |
total |
integer | How many repositories match the filters, BEFORE max_results truncation. Compare it against the length of repositories to tell a complete answer from a page of one. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: What can this credential reach?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/repos"
{"repositories": [{"id": "example__docs-drop", "name": "example/docs-drop", "source": "directory", "commit_sha": "unavailable", "groups": ["example-group"], "labels": [], "phase": "ready", "capabilities": {"search": true, "dependencies": true, "findings": true, "history": false, "blame": false, "coupling": false, "commit_sha": false}}, {"id": "example__monorepo", "name": "example/monorepo", "source": "git", "commit_sha": "unavailable", "groups": ["example-group"], "labels": [], "phase": "skipped", "skip_reason": "the repository is larger than maxRepoSizeMB", "capabilities": {"search": true, "dependencies": true, "findings": true, "history": true, "blame": true, "coupling": true, "commit_sha": true}}], "repository_groups": [{"name": "example-group", "description": "Whatever you pointed this instance at", "repositories": 2}], "total": 2, "gaps": [{"capability": "history", "reason": "source type 'directory' has no history", "sources": ["example__docs-drop"], "source_count": 1, "kind": "unanswered"}, {"capability": "search", "reason": "the repository was not ingested: the repository is larger than maxRepoSizeMB", "sources": ["example__monorepo"], "source_count": 1, "kind": "unanswered"}]}
GET /api/v1/repos/{repo}/analyze
One repository's inventory, languages, technologies and findings rollup
MCP tool: analyze. Authentication: API key or OAuth access token
The overview an agent reads before it reads any code: how big the repository is, what it is written in, what it depends on and what the analysis pass found.
The deep pass trails the inventory pass, so a repository that has just been ingested answers with complete language totals and a gap for findings and technologies rather than with zeroes. Check gaps before concluding a repository has no findings: an unanalysed repository and a clean one produce the same empty numbers, and only the gaps array tells them apart.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
path | string | yes | Repository identifier, clone URL, owner/name, or a unique name suffix. |
Response fields:
| Field | Type | Description |
|---|---|---|
repository |
string | The repository identifier. |
name |
string | The repository's human-facing name. |
commit_sha |
string | The commit analysed, or "unavailable". |
files |
integer | Files the inventory pass counted. |
lines |
integer | Total lines across those files. |
code |
integer | Lines of code, excluding comments and blanks. |
comment |
integer | Comment lines. |
blank |
integer | Blank lines. |
bytes |
integer | Total bytes across analysed files. |
complexity |
integer | Summed cyclomatic complexity estimate. |
languages |
array | Per-language totals, largest first. |
language |
string | The language name, as scc reports it. |
files |
integer | Files in this language. |
lines |
integer | Lines in this language. |
code |
integer | Code lines in this language. |
complexity |
integer | Complexity in this language. |
technologies |
object | Detected technology to the number of files using it. Empty with a gap present means the deep pass has not finished, not that nothing was detected. |
findings |
object | Finding counts: total, by_severity, by_category and by_kind. |
total |
integer | All findings in this repository OUTSIDE test files; tests_excluded counts the rest. |
by_severity |
object | Counts keyed by severity. |
by_category |
object | Counts keyed by what the rule is ABOUT. A different axis from by_kind: its security bucket INCLUDES the quality rules that are about security, so it is larger than by_kind's. |
by_kind |
object | Counts keyed by which ENGINE found it: security is the secret scanner, quality is the code-quality rules, and the two sum to total. by_kind's security is a SUBSET of by_category's. |
tests_excluded |
integer | Findings in TEST files, which every count beside this one leaves out so that the headline is production code's. They are kept, not deleted: the findings endpoint's tests=include or tests=only lists them. When it is not zero, notices says so in a sentence as well. |
notices |
array of string | Statements that change what these findings MEAN, and which a consumer must show its reader. Three can appear here. A repository whose stored analysis was produced by an earlier version of the secret-scanning rule set says so, says that a re-analysis is queued, says that findings may be ADDED and none removed when it completes, and says how many repositories are still pending — the answer is complete under the rules that produced it, so it is NOT a gap. A repository that has not been analysed at all says so in as many words, and says that this is not a statement that it is clean; that one is also reported in gaps, which names which repositories. The last is ALWAYS present whenever security findings are in scope: every finding is read from the current checkout at HEAD, so a secret that was committed and later removed is still in the git history and is not reported. Asking for kind=quality is the only thing that suppresses it. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: Overview before reading any code
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/repos/billing-service/analyze"
{"repository": "example__billing-service", "name": "example/billing-service", "commit_sha": "9b1c...", "files": 3, "lines": 214, "code": 168, "comment": 22, "blank": 24, "bytes": 5120, "complexity": 11, "languages": [{"language": "Go", "files": 3, "lines": 214, "code": 168, "complexity": 11}], "technologies": {"Go Modules": 1}, "findings": {"total": 2, "by_severity": {"warning": 2}, "by_category": {"correctness": 2}, "by_kind": {"quality": 2}, "tests_excluded": 0}, "notices": ["Findings come from the current checkout at HEAD. A secret committed earlier and since removed is still in the git history and is NOT reported here."], "gaps": []}
GET /api/v1/estate
Totals across everything this principal can see
MCP tool: estate_summary. Authentication: API key or OAuth access token
The estate-wide rollup: repositories, files, lines, languages, technologies and findings, summed over the repositories this principal may search and no others.
An aggregate is where authorisation leaks most quietly — a count is not obviously a disclosure until somebody notices the number moves when a repository they cannot read gains a file. Coverage says how much of the estate the deep pass has finished.
findings.by_kind and findings.by_category are TWO DIFFERENT AXES and they appear here eight keys apart. by_kind counts which ENGINE produced a finding — security is the secret scanner, quality is the code-quality rules — and the two sum to total. by_category counts what the RULE IS ABOUT, across both engines, and the two axes collide on the word security: dozens of quality rules are about security, so by_category's security bucket INCLUDES them and is the larger of the two. Measured on one 434-repository estate they read 6,687 and 699. Neither is wrong; do not add them, and do not quote one as the estate's security posture without saying which it is.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
max_results |
query | integer | no | 15 |
1000 | How many languages and how many technologies to return, each largest-first. It bounds BOTH lists, because both have the same long tail on a real estate: one measured 88 languages and roughly 300 technologies, most of the latter used by one or two files. languages_total and technologies_total carry the true counts whatever this is set to, so a capped answer is never mistaken for the whole estate. There is no offset: both populations are far below the ceiling, so raising this reaches all of them. |
Response fields:
| Field | Type | Description |
|---|---|---|
repositories |
integer | Repositories visible to this principal. |
files |
integer | Files across those repositories. |
lines |
integer | Lines across those files. |
code |
integer | Code lines, excluding comments and blanks. |
bytes |
integer | Bytes across analysed files. |
complexity |
integer | Summed complexity estimate. |
languages |
array | Per-language totals across the estate. |
language |
string | The language name. |
files |
integer | Files in this language. |
lines |
integer | Lines in this language. |
code |
integer | Code lines in this language. |
complexity |
integer | Complexity in this language. |
languages_total |
integer | How many distinct languages the visible estate holds, whatever max_results returned. A cap with no denominator is how an agent reports that the estate uses fifteen languages. |
technologies |
object | Technology name to the number of files using it, the most-used max_results of them. |
technologies_total |
integer | How many distinct technologies the visible estate holds, whatever max_results returned. |
notices |
array of string | Statements that change what this answer MEANS. One appears when the technology list was capped, because the response states the language cap in its own counted sentence and one response can carry only one of those. The other is always present: the findings counts here are read from each repository's current checkout at HEAD, so a secret that was committed and later removed is still in the git history and is not counted. |
findings |
object | Estate-wide finding counts. |
total |
integer | All findings across the visible estate OUTSIDE test files; tests_excluded counts the rest. |
by_severity |
object | Counts keyed by severity. |
by_category |
object | Counts keyed by what the rule is ABOUT. A different axis from by_kind: its security bucket INCLUDES the quality rules that are about security, so it is larger than by_kind's. |
by_kind |
object | Counts keyed by which ENGINE found it: security is the secret scanner, quality is the code-quality rules, and the two sum to total. by_kind's security is a SUBSET of by_category's. |
tests_excluded |
integer | Findings in TEST files, which every count beside this one leaves out so that the headline is production code's. They are kept, not deleted: the findings endpoint's tests=include or tests=only lists them. When it is not zero, notices says so in a sentence as well. |
coverage |
object | How much of the visible estate the deep analysis pass has finished. |
repositories |
integer | Visible repositories. |
analysed |
integer | Those whose deep pass has completed. |
pending |
integer | Those still waiting for it. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: How big is what I can see?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/estate"
{"repositories": 3, "files": 12, "lines": 640, "code": 500, "bytes": 18240, "complexity": 31, "languages": [{"language": "Go", "files": 9, "lines": 500, "code": 400, "complexity": 25}], "languages_total": 1, "technologies": {"Go Modules": 3}, "technologies_total": 1, "notices": ["Findings come from the current checkout at HEAD. A secret committed earlier and since removed is still in the git history and is NOT reported here."], "findings": {"total": 4, "by_severity": {"warning": 4}, "by_category": {"correctness": 4}, "by_kind": {"quality": 4}, "tests_excluded": 0}, "coverage": {"repositories": 3, "analysed": 2, "pending": 1}, "gaps": [{"capability": "findings", "reason": "the deep analysis pass has not finished for this repository", "sources": ["example__docs-drop"], "source_count": 1, "kind": "unanswered"}]}
Operations
GET /api/v1/repos/{repo}/complexity
Cyclomatic complexity for EVERY file in a repository, ranked by nothing
MCP tool: file_complexity. Authentication: API key or OAuth access token
The complete per-file complexity map, which is what a tool needs to decide which files to read into a bounded context window: complex files explain a system, and a top-20 list cannot drive that choice for a repository of two thousand files.
RANKED BY NOTHING. Rows come back in path order so that the caller does its own ranking, and so that paging is stable — a ranking this endpoint chose would be one the caller then has to undo. If you want risk rather than complexity, hotspots ranks complexity × churn and is a different question.
NO COMMIT HISTORY IS REQUIRED. These numbers come from the inventory pass, which reads the working tree, so a directory source and a depth-1 clone answer exactly as a full clone does. Nothing here is degraded by a shallow clone.
The corpus is the one search reports: a file excluded by indexing.exclude or by the built-in exclusion floor appears in neither the rows nor files_total.
PAGE IT with offset and max_results, and read commit_sha on every page — the listing describes one revision, so a page whose commit_sha differs from the previous page's was read after a refresh and the two do not compose into one map.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
path | string | yes | Repository identifier, clone URL, owner/name, or a unique name suffix. |
||
offset |
query | integer | no | 0 |
How many files to skip before listing. With max_results it is a page: offset 0 is the first page, offset 500 with max_results 500 is the second. It is an offset and not a cursor: each request is a fresh read, so if the repository is refreshed between two pages a file may be seen twice or missed once — which is what commit_sha on the response is for. An offset past the end is an empty files array and not an error. It does not change files_total, which describes the whole corpus. | |
max_results |
query | integer | no | 500 |
1000 | Cap on rows returned in one page. files_total is reported before this cap, so a truncated answer is distinguishable from a complete one. It is a default a caller may raise, up to a hard maximum of 1000. A repository with more rows than that is read with offset rather than in one request. |
Response fields:
| Field | Type | Description |
|---|---|---|
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
repository_name |
string | The repository's own name — its normalised clone URL, or its path for a directory source. This is what to show a person; repository is the identifier to pass back. |
files |
array | One row per file in the corpus, in path order. This page only: offset and max_results decide which slice of files_total it is. |
path |
string | Path relative to the repository root. |
language |
string | The language scc detected. |
complexity |
integer | The file's cyclomatic complexity estimate. Zero is a real answer and means the file has no branching — a JSON document, a licence, a straight-line script — and never that it was not measured; a file that was not measured has no row at all. |
lines |
integer | Total lines in the file. |
code |
integer | Lines of code, excluding comments and blanks. |
comment |
integer | Comment lines. |
blank |
integer | Blank lines. |
bytes |
integer | The file's size in bytes. |
files_total |
integer | Files in the corpus, counted BEFORE offset and max_results. It is the denominator a caller pages against and the number to compare the row count with; excluded files are in neither. |
offset |
integer | The offset this page was read at, echoed so a paging caller can check what it got rather than what it asked for. |
truncated |
boolean | Files remain after this page — files_total is greater than offset plus the rows returned. False on the last page, so it says "there is more after this" rather than "this repository has more than one page". |
notices |
array of string | Statements that change what this answer MEANS rather than saying a question went unanswered. Empty on the first page; a page past it carries one naming the revision it describes, because each page is a fresh read and pages read at different commits do not compose into one map. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: The complete complexity map, first page
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/repos/billing-service/complexity?max_results=2"
{"repository": "example__billing-service", "commit_sha": "9b1c...", "repository_name": "https://example.invalid/billing-service", "files": [{"path": "invoice.go", "language": "Go", "complexity": 4, "lines": 82, "code": 64, "comment": 8, "blank": 10, "bytes": 1937}, {"path": "orders.go", "language": "Go", "complexity": 6, "lines": 74, "code": 60, "comment": 5, "blank": 9, "bytes": 1704}], "files_total": 3, "offset": 0, "truncated": true, "notices": [], "gaps": []}
GET /api/v1/repos/{repo}/hotspots
Where the change activity is, weighted by complexity
MCP tool: hotspots. Authentication: API key or OAuth access token
Complexity multiplied by churn, over a window. A file with complexity 500 that has not changed in a year is probably stable infrastructure; a simpler file changed forty-seven times in ninety days is where the work is. Complexity alone ranks the first and churn alone ranks trivial-but-busy files, so both inputs are returned beside the score.
REQUIRES HISTORY. A directory source has no commits and a shallow clone has few, so both answer with a populated gaps array rather than an empty ranking — "no history here" and "nothing has changed here" are different statements.
A DORMANT REPOSITORY IS STILL RANKABLE. Complexity comes from the code and churn comes from the commit walk; neither depends on the calendar. If no commit falls inside the last days days, the same window is re-anchored to this repository's NEWEST COMMIT and you get a real complexity × churn ranking of the period it was last worked on — which is the useful answer when deciding whether to adopt, fork or rewrite a finished dependency. anchor says which window produced the rows and window_start says when it began, so READ THEM: a re-anchored answer may be about 2019. Pass anchor=today to refuse it. Only when no window of either anchor scores anything does the ranking collapse to complexity alone, and the response says so.
The walk behind this runs after each index, never per request, so the answer describes the commit named in the response rather than whatever the branch is at this second.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
path | string | yes | Repository identifier, clone URL, owner/name, or a unique name suffix. |
||
days |
query | integer | no | 90 |
Churn window in days. days accepts 0 to 365. 0 disables churn and ranks by complexity alone, which the response states. Omit to use the instance's configured window. The ceiling is not a limit on how old a repository can be ranked: a window that catches nothing re-anchors to the newest commit, so there is never a reason to ask for a wider one. | |
anchor |
query | string | no | auto |
What the churn window is measured back from. auto uses today and re-anchors to this repository's newest commit if that catches nothing. today is the calendar window and never re-anchors — ask for it when the question is where risk is NOW, and accept a complexity ranking on a dormant repository. last-commit always measures back from the newest commit, which answers "which files churned most while this was being built". The response reports which one produced the rows. One of: auto | today | last-commit. |
|
limit |
query | integer | no | 20 |
1000 | How many files to rank. |
Response fields:
| Field | Type | Description |
|---|---|---|
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
window_days |
integer | The churn window that was applied. 0 means churn was disabled. |
churn |
boolean | Whether churn contributed to the ranking. False means the scores are complexity alone — either because you passed days=0, or because no window scored anything. It is read off anchor: a re-anchored ranking is still a churn ranking, so this is true there even though churn_fell_back is too. |
anchor |
string | What the window that produced these rows was measured back from: today, last-commit, or none when no window was applied. READ THIS BEFORE TREATING THE ROWS AS RECENT — window_days says how long the window was and this says which window it was. |
window_start |
string | RFC 3339 timestamp of the beginning of that window. Empty when anchor is none. "90 days" says nothing about which 90 once the anchor can move, so this is what makes a re-anchored answer checkable. |
commits_in_window |
integer | How many of the scanned commits CONTRIBUTED TO THESE SCORES. This is the ranking's denominator, counted over the window anchor names — zero means every row's score is its complexity alone, which is also what days=0 reports because no commit contributed. commits_scanned beside it is how much history was read, which is a different question. |
churn_fell_back |
boolean | True when the CALENDAR window caught no commits, so this is not the ranking the window you asked for would have given. It does not mean the ranking is complexity alone: the first fallback re-anchors the same window to the newest commit and is a real churn ranking. Read anchor to tell the two apart. A dormant repository is the ordinary cause and it is not an error: an open-source estate is mostly finished repositories. The notices array says so in words, and window_days names the window that was empty. |
hotspots |
array | Ranked files, hottest first. |
path |
string | Path relative to the repository root. |
language |
string | The language scc detected. |
complexity |
integer | The file's cyclomatic complexity. |
changes |
integer | Commits touching it inside the window. |
score |
integer | complexity × changes, or complexity alone when churn is disabled. |
last_change |
string | RFC 3339 timestamp of the newest commit touching this file ANYWHERE in the walked history, not only inside the window. That is what makes it the answer to "how far back was this touched" on a complexity-only ranking, where it used to be empty on every row. Empty means no walked commit touched it. |
hotspots_total |
integer | How many files scored at all, before limit trimmed the list. Raising limit returns more of these. |
commits_scanned |
integer | Commits the cached walk covered. Zero with a populated gaps array means no history was available, which is not the same as a repository nobody has changed. |
truncated |
boolean | The LIST was cut short by limit: hotspots_total files scored and fewer were returned. Raise limit for more. |
commits_truncated |
boolean | The WALK was cut short: the commit cap was reached, so older commits are not represented in any score here. No request parameter widens this — it is analysis.history.maxCommits, which the operator sets. |
notices |
array of string | Sentences a consumer must show the reader — truncation, a partial walk, churn being disabled. Prose rather than flags, because a flag a client does not render reaches nobody. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: Where is the work happening in this repository?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/repos/shop/hotspots?days=90&limit=3"
{"repository": "example__shop", "commit_sha": "9b1c…", "window_days": 90, "churn": true, "hotspots": [{"path": "orders.go", "language": "Go", "complexity": 6, "changes": 4, "score": 24, "last_change": "2026-07-30T09:12:00Z"}], "hotspots_total": 1, "commits_scanned": 66, "truncated": false, "commits_truncated": false, "notices": [], "gaps": []}
GET /api/v1/repos/{repo}/complexity-trend
Is this file getting worse?
MCP tool: complexity_trend. Authentication: API key or OAuth access token
One file's complexity measured at every commit that TOUCHED it, oldest first. Every point is a real measurement of the real blob at a real SHA — nothing is interpolated and nothing is sampled at calendar intervals, because a file untouched for six months has identical complexity at every commit in that gap.
READ THE DENSITY, NOT THE COMPLEXITY. Raw complexity rises with any growth, so a file that doubled in length and doubled in complexity has not got worse — it got bigger, at unchanged density. That is why every point carries code, lines and language beside complexity, and why the quantity that trends is complexity per 1,000 code lines. The unit is cyclomatic complexity as scc counts it, calibrated against nothing: a trend is a fact about this file, an absolute value is not a grade.
THE COMPARISON NEVER STARTS AT THE FILE'S CREATING COMMIT. Every file begins as a stub, so first-to-last says "denser" for essentially everything that has ever grown. comparison names both ends and the rule that chose them.
A RENAME IS FOLLOWED and each point records the path it was measured at, so the points before a rename are real measurements rather than the zeros a naive walk produces.
REQUIRES HISTORY. A directory source has no commits and a repository declared with fullHistory: false has one, and both answer with a gap rather than a one-point flat line — "this clone holds no history" and "this file has never changed" are different statements and only the second is about the code.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
path | string | yes | Repository identifier, clone URL, owner/name, or a unique name suffix. |
||
path |
query | string | yes | File path relative to the repository root. A trend is a question about one file, so unlike commit_log this cannot be omitted. | ||
symbol |
query | string | no | Narrow the trend to ONE function, method or constructor declared in path today: its name, or Class.name where a bare name matches more than one. Each point measures that declaration's own lines with the same scc count, and the series is FOLLOWED BACK ACROSS A SPLIT when exactly one other file lost a function of that name in the commit it arrived in — two or more and the series stops and says so rather than guess, and a function renamed in the commit that moved it is not followed. measures says which kind of number this is. A name the file does not declare is refused naming what it does declare. |
||
since |
query | string | no | Bound the walk to commits at or after this instant — an RFC 3339 timestamp, or a bare YYYY-MM-DD date. Omit for the file's whole history, which is the ordinary call: the cost is the file's own churn rather than the length of the repository's history. | ||
max_points |
query | integer | no | 200 |
1000 | How many points to measure at most, capped at 1000. A file with more touching commits than this is DOWNSAMPLED rather than declined — every point is still a real measurement and the series still spans the file's whole history, so only the resolution drops. sampled, interval and total_commits say what happened. There is no offset because there is nothing past the end to page to. |
Response fields:
| Field | Type | Description |
|---|---|---|
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
measures |
string | What every number in this response is OF: file for the whole file, function when symbol was given. Stated on every response so a function's number is never read as its file's, or the reverse Not called scope, which hotspot_trend already uses for WHICH REPOSITORIES. |
symbol |
string | The function a function scope series is about, as it resolved today — qualified with its class when it has one. Absent for a file series. |
moves |
array | Where a function series crossed from one file to another, newest first: sha, when, from and to. Empty when it never moved, and always empty for a file series, whose renames are in renames. |
sha |
string | The commit the function arrived in. |
when |
string | RFC 3339 timestamp of that commit. |
from |
string | The path it was in before. |
to |
string | The path it arrived in. |
walk_ended |
string | For a function series, why the walk back stopped where it did: where the function was written, the start of the window, or a split with more than one candidate that the series declined to guess between. Absent for a file series. |
path |
string | The path asked for, as it is today. Each point carries the path it was measured at, which differs for anything renamed inside the window. |
unit |
string | What the numbers are: cyclomatic complexity as scc counts it, and density as complexity per 1,000 code lines. Stated on every response because an absolute density is calibrated against nothing and means nothing on its own — the trend is the answer. |
points |
array | The measurements, OLDEST FIRST, in the order git walked them rather than sorted by date: a burst of commits inside one second carries one timestamp, and a rebase makes author dates non-monotonic. |
sha |
string | The commit this blob was read at, in full. |
when |
string | RFC 3339 timestamp of the commit. |
path |
string | The path the file was at IN THIS COMMIT, which is not today's path before a rename. |
language |
string | The language scc detected in these bytes. |
lines |
integer | Total lines in the file at this commit. |
code |
integer | Code lines at this commit, which is the DENOMINATOR of density and the reason a growing file is not automatically a worsening one. |
complexity |
integer | Cyclomatic complexity at this commit. |
density |
number | complexity per 1,000 code lines. Absent for an unmeasured point and for a file with no code lines at all, because 0 is a measurement and neither of those is. |
created |
boolean | This commit ADDED the file, so the file is a stub here — or, for a function series, the function was WRITTEN here. No comparison is made against it. |
moved_from |
string | On a function series, the path the function was in before this commit moved it here. Absent everywhere else. |
measured |
boolean | False for an UNKNOWN point. Never read a false one as a zero: a binary blob, a language scc does not know, a commit missing from the clone and a deletion all land here, and every one of them would draw as "simple" if it were zero. |
unknown |
string | Why this point could not be measured. Empty when measured is true. |
total_commits |
integer | How many commits touched the file inside the window, before any downsampling. This is the denominator sampled is about. |
sampled |
boolean | The series is a downsample of total_commits. False means it is exhaustive for this window — every commit that touched the file is a point. |
interval |
integer | One point per this many touching commits. 1 means no sampling. |
unknown_points |
integer | How many points could not be measured. They are EXCLUDED from the comparison rather than counted as zero, so this is what says how much of the series the answer rests on. |
unknown_reasons |
array of string | One line per distinct cause, with how many points it accounts for. "2 unknown" alone cannot tell a shallow clone from a binary file. |
renames |
integer | How many points were measured at a path other than the one asked for. Non-zero means this file has been moved inside the window. |
comparison |
object | The movement, and the TWO POINTS it was taken between. Absent when there is none, with no_comparison saying why — a comparison that quietly used the file's creating commit would report growth as decay. |
basis |
string | The rule that chose the baseline, in prose, so the pair is checkable rather than trusted. |
from |
object | The baseline point: sha, when, path, language, lines, code, complexity and density, as in points. |
to |
object | The newest measured point, in the same shape. |
no_comparison |
string | Why no movement is reported: fewer than two measurable points, or nothing to compare that is not the file's creating commit. Empty when comparison is present. |
notices |
array of string | Sentences a consumer must show the reader — the unit and method, the sampling, the unknown points and which two points the movement was taken between. Prose rather than flags, because a flag a client does not render reaches nobody. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: Is this file getting worse?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/repos/shop/complexity-trend?path=orders.go"
{"repository": "example__shop", "commit_sha": "9b1c…", "path": "orders.go", "unit": "cyclomatic complexity counted by scc; density is complexity per 1,000 code lines", "points": [{"sha": "3f2a…", "when": "2024-02-11T09:02:00Z", "path": "orders.go", "language": "Go", "lines": 40, "code": 31, "complexity": 2, "density": 64.5, "created": true, "measured": true}, {"sha": "9b1c…", "when": "2026-07-30T09:12:00Z", "path": "orders.go", "language": "Go", "lines": 620, "code": 498, "complexity": 96, "density": 192.8, "measured": true}], "total_commits": 2, "sampled": false, "interval": 1, "unknown_points": 0, "unknown_reasons": [], "renames": 0, "no_comparison": "the only baseline available is the commit that CREATED this file…", "notices": ["…"], "gaps": []}
GET /api/v1/hotspot-trend
Are the hotspots coming down?
MCP tool: hotspot_trend. Authentication: API key or OAuth access token
The churn-weighted MEAN complexity density of a hotspot set, over time, for one repository, a group, a label or every repository you can see. Built entirely from complexity_trend's points — each file measured at every commit that touched it — with no second walk and no second analysis pass.
IT IS A MEAN AND NEVER A SUM. A summed complexity trend rises when a team ships and falls when it is laid off, whatever the code is like; it measures headcount. A mean is scale-free, which is the one property this has to have before the question can be asked at all. The weight is CHURN rather than size, because a size-weighted mean makes the biggest file the metric and the biggest files are usually generated.
THE SET IS FIXED AT EACH REPOSITORY'S NEWEST COMMIT and is stated as such: the question is "did the files we are worried about TODAY improve". A file that improved its way out of the ranking still contributes; one that rotted its way in since the baseline is not here.
THE SPAN IS NARROWED to the interval where every file in the set exists. Measured on a real repository, an aggregate over a set whose membership changed read 267.4 at three files and 235.6 at eleven a month later, arrowed DOWN, and nothing had improved — the denominator moved. So the span starts where the last member does, the narrowing is reported, and every point carries the file count it was taken over.
TESTS AND BUILD FILES ARE OUT by default and file_set says so on every response. A table-driven test gets longer and denser as coverage improves, so a headline driven by a test suite moves the wrong way when the right thing is happening.
REQUIRES HISTORY, and requires the analysis pass to have ranked the repository. Repositories that supply neither are reported as gaps rather than left out.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
query | string | no | Narrow to one or more repositories, comma-separated. Repository identifier, clone URL, owner/name, or a unique name suffix. Omit it for every repository the calling principal may read the history of. |
||
group |
query | string | no | Narrow to one repository group — the unit authorisation is granted over, and the ownership filter, so "everything the payments team owns" is one call. Applied inside the authorised set, never instead of it. | ||
label |
query | string | no | Narrow to one repository label, which is the looser categorisation beside a group. Applied inside the authorised set. | ||
days |
query | integer | no | 90 |
The churn window that both RANKS the set and WEIGHTS the headline, in days. 0 disables churn: the ranking becomes complexity alone and no file has a weight, so the headline falls back to the unweighted mean and the response says it did. | |
limit |
query | integer | no | 12 |
How many of the ranked files the set holds, clamped at 25. This is the bound an unbounded scope is answered with: every file at every commit is a walk that does not terminate on a real estate, so the answer is the top N hotspots across the scope with the candidate count and the ceiling stated. There is no offset because a second page of a mean is a DIFFERENT MEAN over a different set, not the rest of this one — the rest of the ranking is hotspots, and one file at full resolution is complexity_trend. |
|
file_set |
query | string | no | production |
Which files may enter the set. production, the default, leaves test and build files out; all measures everything the analysis pass kept. Stated on every response either way, because an exclusion nobody can see is an accident of the walker rather than a decision. One of: production | all. |
|
since |
query | string | no | Bound every member's walk to commits at or after this instant — an RFC 3339 timestamp or a bare YYYY-MM-DD date. Omit for the whole history; the span is narrowed to where every member exists in either case. | ||
boundary |
query | string | no | Split the same series in two and compare the sides — a date such as 2026-03-01 for "we started doing it this way in March", or the word agent to split on commits carrying a Co-authored-by trailer that names a coding agent. Each side reports the MEAN density movement per commit, never a total, and the agent split's two sides are "attributable to an agent" and "everything else" — never "human-written", because inline completion leaves no trace and a squash drops the trailers. The attributable share travels beside the comparison. |
Response fields:
| Field | Type | Description |
|---|---|---|
measures |
string | What the numbers are OF: always file. Every density here is a file's, averaged over files, never a function's. |
scope |
object | Which repositories this is over and how they were selected. |
kind |
string | estate, repository, group or label. |
value |
string | The selector, empty for the whole visible estate. |
repositories |
integer | How many repositories are in scope. EXACT, and the number to scale by: names below is a capped sample. |
names |
array of string | Their identifiers, capped at 20. A repository, group or label selection under the cap is listed in full; only an estate-wide call samples. Enumerating them cost 64KB on a 448-repository estate and grew linearly. |
as_of |
object | The commit each repository's hotspot set was ranked at. On every response because the set is FIXED there rather than recomputed per point, and a reader has to know which commit to know which question was answered. |
repositories |
integer | How many repositories were ranked. EXACT. |
unavailable |
integer | How many of them had NO commit to rank at, so they contributed nothing. EXACT, and the half that had to survive summarising: a scope where sixty repositories had no commit is a different answer from one where every repository did, and a sample cannot say that. |
commits |
array | The commit per repository, capped at 20. |
repository |
string | The repository identifier. |
commit_sha |
string | The commit its inventory was taken at. |
unit |
string | What the numbers are: cyclomatic complexity as scc counts it, over code lines, per 1,000. Calibrated against nothing, so the trend is the answer and an absolute value is not a grade. |
method |
string | How the number was arrived at, in one sentence, because a mean over a set is only readable beside the rule that built the set. |
window_days |
integer | The churn window that ranked and weighted. |
file_set |
string | production or all — which files were eligible. |
set |
object | The hotspot set this is over, and what bounded it. |
files |
integer | How many files contributed at least one measurement. |
distinct_paths |
integer | How many of the members above are distinct PATHS rather than distinct (repository, path) pairs. Lower than files means the same path appears under more than one repository, which forks, mirrors and in-flight migrations produce: the ranking is that much narrower than it looks and the duplicates crowded out genuine entries. They are NOT merged, because two copies diverge the moment either is edited and choosing a canonical one is a judgement with no basis. |
candidates |
integer | How many files ranked at all across the scope, before limit cut the set. This is what says whether the ceiling bound the answer. |
limit |
integer | The set size applied. |
ceiling |
integer | The hard maximum limit is clamped to. |
declined |
boolean | limit was above the ceiling and was clamped. |
dropped |
array | Ranked files that produced no measurable point, with the reason. EXCLUDED from every mean rather than counted as zero, and listed so a set of twelve reported over nine is visible. |
repository |
string | Which repository. |
path |
string | Which file. |
why |
string | Why nothing could be measured. |
span |
object | The interval reported over, and what narrowed it. The span starts where the LAST member of the set was first measured, so the member count is constant across every point — an average over a set whose membership changes falls when files join it, and that fall is not about the code. |
start |
string | RFC 3339 instant the series begins at. |
end |
string | RFC 3339 instant of the newest measurement. |
files |
integer | The constant member count the span holds. |
narrowed |
boolean | start is later than the oldest member's first measurement. |
narrowed_by |
string | repository:path of the member that decided start. |
oldest_member_starts |
string | Where the series would have begun without the guard. |
dropped_days |
integer | How much history the narrowing gave up. |
days |
integer | How long the series ACTUALLY covers. Read this and not window_days, which is the churn window that ranked the set and is a different number: the two sat side by side with nothing saying so, and a verdict computed over 8 days was read as covering 90. |
policy |
string | How a member younger than the window was handled. trim-set dropped it and kept the survivors' history, which is what happens whenever a churn window is in force. intersect clipped the span to it, which is what days=0 produces and what trimming falls back to when every member is younger than the window, because an aggregate over no files answers nothing. Both are defensible answers to a real question, so the response says which one it gave. |
trimmed_members |
integer | How many members were dropped for being born inside the window. They are in set.dropped with the reason, never silently removed. |
hotspot_complexity |
object | THE HEADLINE: the churn-weighted mean density over the set, at each reporting instant. |
scope |
string | What population this number is over, in prose. |
weighting |
string | churn, or the reason it is not. |
points |
array | The series, oldest first. |
when |
string | RFC 3339 reporting instant. |
value |
number | The mean density at that instant. |
files |
integer | How many files this point is a mean over. CONSTANT across the series by construction, and published on every point so a moving denominator cannot be read as a moving metric. |
changed |
integer | How many members had a new measurement since the previous instant, so a flat stretch reads as "nothing was touched". |
from |
number | The first point's value. |
to |
number | The last point's value. |
change_pct |
number | Percentage movement from first to last. |
verdict |
string | improving, worsening or stable, keyed on DENSITY and never on raw complexity. |
available |
boolean | False when there was nothing to compute. |
unavailable |
string | Why, when available is false. An absent number is not a zero. |
mean_complexity |
object | THE CONTROL: the same series unweighted, over the same files. Where it disagrees with the headline, the difference is the churn weighting rather than the code. Same shape as hotspot_complexity. |
worst_file |
object | The single densest file in the set today, named, with its own movement — the one a reader can act on this afternoon and the one a mean hides. Null when no member could be measured. |
corpus_today |
object | The unweighted mean density over EVERY analysed file in scope, at the newest commit. A point in time and not a series, deliberately: a series over every file is one read per file per commit over the estate. It is what says whether the set is representative of the corpus it came from. |
scope |
string | The population, stated, because it is not the set above. |
files |
integer | How many analysed files it is over. |
density |
number | The mean density. |
files_left_out |
integer | Analysed files the file_set excluded. |
members |
array | One row per file in the set, so the mean can be read back to the files that made it. |
repository |
string | Which repository. |
path |
string | Which file, as it is today. |
changes |
integer | Its churn in the window, which is its weight. |
points |
integer | How many measurements its series carries. |
from_density |
number | Density at the baseline point. |
to_density |
number | Density at the newest measured point. |
complexity |
integer | Complexity at that newest point. |
code |
integer | Code lines at that newest point, which is the denominator. |
verdict |
string | improving, worsening, stable or unknown. Keyed on DENSITY: a file that shrank while its complexity fell can still be worsening, and a rule keyed on raw complexity is blind to it. unknown is never stable. |
why |
string | Why the verdict is unknown. Absent otherwise. |
basis |
string | The rule that chose this file's baseline pair. |
boundary |
object | The before-and-after comparison, present only when boundary was given. Each side carries the MEAN density movement per commit and never a total. |
kind |
string | date or agent. |
value |
string | The date, or what the agent split was read from. |
available |
boolean | False when one side has nothing in it. |
unavailable |
string | Why there is no comparison. A side of zero commits reported as a mean of 0.0 reads as "that made no difference", which is the wrong answer rather than the missing one. |
attributable_commits |
integer | How many measured commits carry a Co-authored-by trailer naming a coding agent. The remainder carry no such trailer, and that is the ONLY way to be unattributable here: a transition is counted at all only after the creating commit and both density reads have been excluded. So the gap is mostly ordinary work rather than a measurement failure, and it also holds any agent work whose trace was lost, which is why the other side is "everything else" and never "human-written". |
measured_commits |
integer | How many measured transitions the whole set produced. This counts the EVIDENCE and not the comparison: it is every transition over every member, including members the comparison excluded, because a reader asking how much was seen means all of it. |
excluded_files |
integer | How many members were left out of the comparison for having changed on ONE side of the boundary only. EXACT; excluded below is a capped sample. A file that did not exist before the boundary cannot be compared across it, and counting it on one side puts the two means over different populations — the same error the span logic gives up history to avoid. |
excluded |
array of string | repository:path of the excluded members, capped. Named rather than only counted, because "two of these twelve files did not exist before June" is itself an answer to the question being asked. |
sides |
array | Exactly two, in order. |
label |
string | The side's name. For an agent split these are exactly "attributable to an agent" and "everything else". |
commits |
integer | Commits on this side. |
files |
integer | Files this side touched. EQUAL on both sides by construction: the comparison runs only over members changed on both, so a difference here would mean the two means were over different populations. |
mean_density_change |
number | Mean density movement per commit, in complexity per 1,000 code lines. Negative is the code getting easier per line. |
commits_that_raised_density |
integer | How many made it denser. |
commits_that_lowered_density |
integer | How many made it less dense. |
contributors |
object | How the work on this file is SPREAD, and it names nobody. The subject is the file: "this file improved across nine contributors" and "across one" are different facts about the code and the risk it carries. ABSENT when no commit touching the file could be attributed, rather than an object of zeroes, because "0 contributors" reads as a file nobody works on instead of one nobody could attribute. |
commits |
integer | Attributable commits the answer is over. Always present: it is the coverage of the evidence rather than a property of the people. |
distinct |
integer | How many different people. Present only ABOVE analytics.concentrationFloor; below it you get band instead, because a count identifies when the denominator is small. |
top_share |
number | The largest contributor's share of those commits, 0 to 1. Present only above the floor, for the same reason as distinct: "one person made 100%" names them in a team of four as well as a name would. |
band |
string | "few" when the distinct count is below analytics.concentrationFloor. A band and never silence: an absent answer is indistinguishable from an unmeasured one, and silence would read as nothing to worry about. |
names |
array | WHO they were, most commits first. ABSENT ON EVERY DEFAULT INSTALL: it appears only when the operator has set analytics.attributeAuthors, which also makes per_user_breakdown read true on /api/v1/usage. The concentration floor still applies, so a file below it names nobody even with attribution on — turning it on widens what you can see and does not switch off the anonymity floor underneath it. |
name |
string | The commit author's identity, as git recorded it. |
commits |
integer | How many of this file's attributable commits were theirs. |
notices |
array of string | Sentences a consumer must show the reader — the unit and method, the fixed set, the narrowed span, the file set, the headline and its control. Prose rather than flags, because a flag a client does not render reaches nobody. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: Are the payments team's hotspots coming down?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/hotspot-trend?group=payments&days=90&limit=12"
{"scope": {"kind": "group", "value": "payments", "repositories": 3, "names": ["example__shop"]}, "as_of": {"repositories": 3, "unavailable": 0, "commits": [{"repository": "example__shop", "commit_sha": "9b1c…"}]}, "unit": "cyclomatic complexity counted by scc; density is complexity per 1,000 code lines", "window_days": 90, "file_set": "production", "set": {"files": 11, "candidates": 143, "limit": 12, "ceiling": 25, "declined": false, "dropped": []}, "span": {"start": "2025-08-04T00:00:00Z", "end": "2026-09-17T00:00:00Z", "files": 11, "narrowed": true, "narrowed_by": "example__shop:orders.go", "dropped_days": 412, "days": 409, "policy": "trim-set", "trimmed_members": 1}, "hotspot_complexity": {"scope": "the hotspot set measured here, weighted by each file's change count in the churn window", "weighting": "churn", "points": [{"when": "2025-08-04T00:00:00Z", "value": 267.4, "files": 11, "changed": 0}, {"when": "2026-09-17T00:00:00Z", "value": 231.2, "files": 11, "changed": 4}], "from": 267.4, "to": 231.2, "change_pct": -13.54, "verdict": "improving", "available": true}, "worst_file": {"repository": "example__shop", "path": "orders.go", "verdict": "worsening", "to_density": 150.2}, "corpus_today": {"files": 1423, "density": 41.2, "files_left_out": 318}, "members": [], "notices": ["…"], "gaps": []}
GET /api/v1/repository-trends
Which repositories are improving, side by side
Not an MCP tool: a side-by-side page for people; an agent asking whether code is improving has hotspot_trend, which answers for any scope with every caveat attached Authentication: API key or OAuth access token
One row per repository you may read the history of, saying whether its complexity is improving, worsening or stable over the last 30, 90, 180 or 365 days, with the series a chart draws it from.
EVERY NUMBER IS hotspot_trend's: a row is that endpoint's churn-weighted mean complexity density for one repository, read across the window. No second metric, and improving means here exactly what it means there.
COMPUTED IN THE BACKGROUND, never on this request. Each repository is rebuilt after its history is refreshed, so this call is a cache read, and a repository not reached yet is pending and counted as pending. insufficient is a repository with no measurable movement in the window — no history, nothing committed in it, too few readings — with the reason. Neither is ever reported as stable.
THE CHART IS DRAWN ON CHANGE, NOT LEVEL. Densities differ by an order of magnitude between repositories for reasons that are not quality, so each series is the change from that repository's own first reading in the window. The rows carry both.
Filters narrow inside what you may see, never beside it: a repository you cannot see contributes no row, no count and no place in a ranking.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
window |
query | integer | no | 90 |
The window in days, ending now. One of four presets, because each is computed ahead of time; any other window would have to be computed on the request, which takes minutes over a large estate. One of: 30 | 90 | 180 | 365. | |
repo |
query | string | no | Only these repositories, comma-separated. Repository identifier, clone URL, owner/name, or a unique name suffix. This is "the ones I care about": pick them, bookmark the URL, and that is the view. |
||
group |
query | string | no | Only repositories in this repository group. Applied inside the authorised set, never instead of it. | ||
label |
query | string | no | Only repositories carrying this label. Applied inside the authorised set. | ||
name_like |
query | string | no | Only repositories whose name or identifier CONTAINS this, case-insensitively: a substring, not a glob or a regex, as list_repositories spells it. | ||
verdict |
query | string | no | Only rows with this verdict. counts on the response is taken before this filter, so it still says how many of each there are. One of: improving | worsening | stable | insufficient | pending. |
||
sort |
query | string | no | worsening |
Row order. worsening, the default, puts the largest rise first; improving the largest fall; density the highest current density; churn the most changes in the window; name alphabetical. Rows with no movement sort after every row with one, except by name. One of: worsening | improving | density | churn | name. |
|
offset |
query | integer | no | 0 |
How many repositories to skip before listing. With max_results it is a page: offset 0 is the first page, offset 50 with max_results 50 is the second. max_results is a hard maximum rather than a default, so this is the only way to reach what lies past it. It is an offset and not a cursor: each request is a fresh read, so a row added or removed between two pages may be seen twice or missed once. An offset past the end is an empty list and not an error, because total is the only way to know where the end was and it may move. It does not change total, which counts the whole set before either parameter is applied. | |
max_results |
query | integer | no | 50 |
200 | How many repositories one page returns, at most 200. Each row carries its series, so a page is bounded by its own size and never by the estate's. |
Response fields:
| Field | Type | Description |
|---|---|---|
window_days |
integer | The window this answer is over, in days, ending now. |
windows |
array of integer | Every window that is computed and may be asked for. |
threshold_pct |
number | How far the density has to move, in per cent, to count as improving or worsening rather than stable. The same threshold hotspot_trend uses. |
unit |
string | What the densities are: cyclomatic complexity as scc counts it, per 1,000 code lines. |
method |
string | How each repository's line is computed. |
scope |
object | Which repositories the filters selected, before verdict. |
kind |
string | estate, repository, group or label. |
value |
string | The selector, empty for everything visible. |
repositories |
integer | How many repositories the filters selected. |
counts |
object | How many of the selected repositories carry each verdict: improving, worsening, stable, insufficient and pending. Taken before the verdict filter. |
computed |
object | When the rows were computed. |
oldest |
string | The oldest computation among the selected rows, RFC 3339. Empty when none is computed. |
newest |
string | The newest computation, RFC 3339. |
pending |
integer | How many selected repositories are not computed yet. |
sort |
string | The order the rows are in. |
total |
integer | How many rows match, before paging. |
offset |
integer | The offset this page was read at, echoed so a paging caller can check what it got rather than what it asked for. |
truncated |
boolean | Whether rows exist past this page. Read on with offset. |
repositories |
array | One row per repository on this page. |
repository |
string | The repository identifier. |
name |
string | Its human-facing name. |
verdict |
string | improving, worsening, stable, insufficient or pending. |
why |
string | Why there is no movement, for insufficient and pending. Empty otherwise. |
start_density |
number | The first reading in the window. Zero when there is no movement. |
end_density |
number | The newest reading. |
change_pct |
number | The change from start_density to end_density, in per cent. |
churn |
integer | Changes to the measured files in the window, the weight the mean was taken with. |
files |
integer | How many files the hotspot set measured. |
commit_sha |
string | The commit the row was computed at. |
computed_at |
string | When the row was computed, RFC 3339. Empty when pending. |
span_start |
string | The first reading's instant. |
span_end |
string | The newest reading's instant. |
points |
array | The series, oldest first, as change from the first reading. |
when |
string | The reading's instant, RFC 3339. |
change_pct |
number | Per cent change from the first reading. |
chart |
object | The lines a chart should draw: never more than ten, chosen from every matching row rather than from this page. |
selection |
string | all when ten or fewer rows are measured and every one is drawn; extremes when the five most improved and five most worsened are. |
measured |
integer | How many matching rows have a movement to draw. |
repositories |
array | The chosen rows, in the same shape as repositories. |
repository |
string | The repository identifier. |
name |
string | Its human-facing name. |
verdict |
string | improving, worsening, stable, insufficient or pending. |
why |
string | Why there is no movement, for insufficient and pending. Empty otherwise. |
start_density |
number | The first reading in the window. Zero when there is no movement. |
end_density |
number | The newest reading. |
change_pct |
number | The change from start_density to end_density, in per cent. |
churn |
integer | Changes to the measured files in the window, the weight the mean was taken with. |
files |
integer | How many files the hotspot set measured. |
commit_sha |
string | The commit the row was computed at. |
computed_at |
string | When the row was computed, RFC 3339. Empty when pending. |
span_start |
string | The first reading's instant. |
span_end |
string | The newest reading's instant. |
points |
array | The series, oldest first, as change from the first reading. |
when |
string | The reading's instant, RFC 3339. |
change_pct |
number | Per cent change from the first reading. |
notices |
array of string | Sentences a reader must see: what improving means, how many rows are pending or insufficient, and which lines the chart chose. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: Which of the payments team's repositories are getting worse?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/repository-trends?group=payments&window=90"
{"window_days": 90, "windows": [30, 90, 180, 365], "threshold_pct": 5, "unit": "…", "method": "…", "scope": {"kind": "group", "value": "payments", "repositories": 3}, "counts": {"improving": 1, "worsening": 1, "stable": 0, "insufficient": 1, "pending": 0}, "computed": {"oldest": "2026-10-12T09:00:00Z", "newest": "2026-10-12T09:04:00Z", "pending": 0}, "sort": "worsening", "total": 3, "offset": 0, "truncated": false, "repositories": [{"repository": "example__shop", "name": "example/shop", "verdict": "worsening", "why": "", "start_density": 182.4, "end_density": 201.9, "change_pct": 10.69, "churn": 41, "files": 25, "commit_sha": "9b1c…", "computed_at": "2026-10-12T09:04:00Z", "span_start": "2026-07-14T09:00:00Z", "span_end": "2026-10-11T16:20:00Z", "points": [{"when": "2026-07-14T09:00:00Z", "change_pct": 0}, {"when": "2026-10-11T16:20:00Z", "change_pct": 10.69}]}], "chart": {"selection": "all", "measured": 2, "repositories": []}, "notices": ["…"], "gaps": []}
GET /api/v1/risk
Which files carry the most risk, factor by factor
MCP tool: risk_ranking. Authentication: API key or OAuth access token
The refactor list: every analysed file ranked by four factors reported SIDE BY SIDE — complexity, churn over the instance's window, the number of DISTINCT AUTHORS who changed it in that window, and findings in production code (test-file findings are counted beside them, never in them). For one repository, several, or every repository you may read the history of.
NOT A PRODUCT OF THE FACTORS. A product zeroes when any factor is zero, ranks whichever detector is noisiest, and has to turn an unknown into a number. Each factor is instead placed at a PERCENTILE within its own repository, the row's score is the mean of its known percentiles, and every input is on the row so the reader can see which one is carrying it. elevated names the factors a file is in its repository's top 10% for.
UNKNOWN IS NEVER ZERO. A source with no history, a shallow clone whose history does not cover the window, a walk cut short, or a deep pass that has not run leaves a factor null and listed in unknown, with a gap saying why — and it is left out of the mean rather than ranking the file low.
FILE SCOPE. Every number is about a whole file and none is attributed to a function in it. Authors are a COUNT and never names; a count below the instance's anonymity floor is reported as the band few.
The same factors reach read_file as a one-sentence advisory when a file is in its repository's top 10% for any of them. Nothing here is computed on the request: the table is built after each index from the commit walk hotspots already uses.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
query | string | no | Narrow to one or more repositories, comma-separated. Repository identifier, clone URL, owner/name, or a unique name suffix. Omit it for every repository the calling principal may read the history of. |
||
offset |
query | integer | no | 0 |
How many files to skip before listing. With max_results it is a page: offset 0 is the first page, offset 50 with max_results 50 is the second. max_results is a hard maximum rather than a default, so this is the only way to reach what lies past it. It is an offset and not a cursor: each request is a fresh read, so a row added or removed between two pages may be seen twice or missed once. An offset past the end is an empty list and not an error, because total is the only way to know where the end was and it may move. It does not change total, which counts the whole set before either parameter is applied. | |
max_results |
query | integer | no | 20 |
1000 | Cap on rows returned in one page. files_total is counted before it. A default a caller may raise, up to a hard maximum of 1000; page past that with offset. |
Response fields:
| Field | Type | Description |
|---|---|---|
scope |
string | What every number was computed at: always file. No figure here is about a function or a class, and none may be presented as one. |
window_days |
integer | The churn and author window, in days, measured back from when each repository's table was built after its last index. |
threshold_percent |
integer | The share of a repository's files a factor must place a file in, from the top, to be listed in elevated. |
files |
array | Ranked files, highest mean percentile first. This page only. |
repository |
string | The repository identifier. |
path |
string | Path relative to the repository root. |
language |
string | The language scc detected. |
commit_sha |
string | The commit the analysis described, or "unavailable" for a source with no commits. |
scope |
string | Always file: every figure is about the whole file, never a symbol in it. |
complexity |
integer | The file's cyclomatic complexity, from scc. |
churn |
integer | Commits touching the file inside the window. Null when UNKNOWN, never zero. |
authors |
integer | DISTINCT authors of those commits — a count, never a name. Null when UNKNOWN, and null beside authors_band when the count is below the anonymity floor. |
authors_band |
string | few when the distinct count is below the instance's anonymity floor; absent otherwise. |
findings |
integer | Findings in production code. Null when UNKNOWN, never zero. |
test_findings |
integer | Findings in test files, reported beside and never counted in findings. |
percentiles |
object | Each KNOWN factor's percentile within its repository, 0 to 100: the share of files with a strictly lower value. An unknown factor has no entry. |
score |
number | The mean of the known percentiles. The order, not a measurement. |
elevated |
array of string | Factors this file is in its repository's top 10% for. |
unknown |
array of string | Factors that could not be measured. Never read an unknown factor as low. |
files_total |
integer | Files ranked across the scope, counted before offset and max_results. |
offset |
integer | The offset this page was read at, echoed so a paging caller can check what it got rather than what it asked for. |
truncated |
boolean | Rows remain after this page: files_total is greater than offset plus the rows returned. |
notices |
array of string | Sentences a consumer must show the reader: what the order is and that an unknown factor is not zero. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: What should we refactor first?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/risk?repo=shop&max_results=1"
{"scope": "file", "window_days": 90, "threshold_percent": 10, "files": [{"repository": "example__shop", "commit_sha": "9b1c…", "path": "orders.go", "language": "Go", "scope": "file", "complexity": 212, "churn": 83, "authors": 4, "findings": 2, "test_findings": 5, "percentiles": {"complexity": 97, "churn": 99, "authors": 95, "findings": 88}, "score": 94.75, "elevated": ["complexity", "churn", "authors"], "unknown": []}], "files_total": 143, "offset": 0, "truncated": true, "notices": ["FILE SCOPE: …"], "gaps": []}
GET /api/v1/repos/{repo}/coupling
The files that usually change with this one
MCP tool: change_coupling. Authentication: API key or OAuth access token
What else you will have to touch. Files that changed in the same commits as the one you named, with SUPPORT (how many commits contained both) and CONFIDENCE (of the commits touching yours, what fraction touched the other).
READ BOTH NUMBERS. Confidence 1.0 over two commits and confidence 0.4 over two hundred are not the same claim, and a support floor is applied by default for exactly that reason.
Commits touching more than the configured file count — reformats, licence sweeps, generated-code refreshes — are EXCLUDED, because one such commit couples every file to every other. The count that was excluded is in the response.
REQUIRES HISTORY. A directory source has no commits and a shallow clone has few. Both answer with a populated gaps array rather than an empty list: in a migration an empty coupling result reads as "safe to change", and that is the false conclusion this endpoint must never hand anybody.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
path | string | yes | Repository identifier, clone URL, owner/name, or a unique name suffix. |
||
path |
query | string | yes | The file to report the blast radius of, relative to the repository root. | ||
min_support |
query | integer | no | 2 |
Minimum commits a pair must share. 1 admits coincidences — most obviously the initial import, which touches everything a repository started with. | |
min_confidence |
query | number | no | 0.3 |
Minimum fraction of this file's commits that also touched the other, between 0 and 1. | |
limit |
query | integer | no | 20 |
1000 | How many coupled files to return. |
Response fields:
| Field | Type | Description |
|---|---|---|
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
path |
string | The file that was asked about. |
commits |
integer | Commits touching that file, after bulk commits were excluded. It is the denominator of every confidence below. |
coupled |
array | Files that change with it, strongest first. |
path |
string | The coupled file. |
support |
integer | Commits that contained both files. |
confidence |
number | support divided by commits — the fraction of this file's changes that also changed that one. |
coupled_total |
integer | How many files passed both thresholds, before limit trimmed the list. Raising limit returns more of these. |
excluded_commits |
integer | Commits skipped for touching more than bulk_commit_threshold files. |
bulk_commit_threshold |
integer | The file count above which a commit was excluded (analysis.history.bulkCommitFiles). |
min_support |
integer | The support floor that was applied. |
min_confidence |
number | The confidence floor that was applied. |
commits_scanned |
integer | Commits the cached walk covered, before exclusions. |
truncated |
boolean | The LIST was cut short by limit: coupled_total files passed the thresholds and fewer were returned. Raise limit for more. |
commits_truncated |
boolean | The WALK was cut short: the commit cap was reached, so older commits are not represented in any support or confidence here. No request parameter widens this — it is analysis.history.maxCommits, which the operator sets. |
notices |
array of string | Sentences a consumer must show the reader — how many commits were excluded, truncation, a partial walk. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: What else will I have to touch?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/repos/shop/coupling?path=orders.go"
{"repository": "example__shop", "commit_sha": "9b1c…", "path": "orders.go", "commits": 4, "coupled": [{"path": "invoice.go", "support": 4, "confidence": 1}], "coupled_total": 1, "excluded_commits": 2, "bulk_commit_threshold": 50, "min_support": 2, "min_confidence": 0.3, "commits_scanned": 66, "truncated": false, "commits_truncated": false, "notices": ["2 commits touching more than 50 files were excluded; a reformat commit couples every file to every other."], "gaps": []}
GET /api/v1/repos/{repo}/dependencies
What a file imports, what imports it, and which tests cover it
MCP tool: file_dependencies. Authentication: API key or OAuth access token
One file's dependency graph, read from the analysis pass: what it imports, what imports it, one or two hops out, and — with tests=only — the runnable tests that cover it. The three questions an agent asks around an edit: what to read first, how far the change reaches, and which tests to run.
FILES AND PACKAGES, NEVER SYMBOLS. An edge says one file imports another, or imports a package — a Go package, a Python namespace package — which is a directory and not any one file in it. There is no calls edge and there will not be one: which function is used is answered by reading the two files.
Every row carries a KIND — file, package, same_package (Go files of one package, which see each other without importing and are how a Go file's tests are found), name_convention (foo_test.go, test_foo.py, foo.test.ts) or mock (jest.mock, vi.mock) — and a RESOLUTION: exact when the language's own rules name the target, probable when a convention mapped it (a tsconfig alias, a Python project root), external when it is outside the repository. A hop-2 row is only as sure as the weaker of its two links.
AN UNRESOLVED IMPORT IS NOT A MISSING ONE. Imports that named nothing in this repository — a computed require(), a relative path to a deleted file — are counted and named in unresolved, never dropped. A file in a language this release reads no imports from (TypeScript, JavaScript, Python and Go are read), one the analysis pass skipped, and one whose parse may have lost imports each answer with a gap rather than an empty list.
DEPTH STOPS AT 2. Past two hops a typical file reaches half of a test suite, so further hops are COUNTED in further and never listed. Nothing is parsed on the request: the graph was resolved when the repository was last analysed.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
path | string | yes | Repository identifier, clone URL, owner/name, or a unique name suffix. |
||
path |
query | string | yes | The file to report on, relative to the repository root. | ||
direction |
query | string | no | both |
imports for what the file depends on, imported_by for what depends on it — the blast radius of a change — or both. One of: imports | imported_by | both. | |
depth |
query | integer | no | 1 |
Hops to list: 1 or 2. A larger value is reduced to 2 and says so; what lies further is counted in further and never returned as rows. |
|
tests |
query | string | no | include |
only for the runnable tests among the rows — the tests to run after editing this file; exclude for production files only; include for both. Test SUPPORT (fixtures, mocks, conftest.py) is never a runnable test. One of: exclude | include | only. | |
resolution |
query | string | no | external |
The weakest resolution to return: exact for edges the language's own rules decided, probable to add convention-mapped edges, external (the default) for everything. One of: exact | probable | external. | |
offset |
query | integer | no | 0 |
How many rows to skip before listing. With max_results it is a page: offset 0 is the first page, offset 50 with max_results 50 is the second. max_results is a hard maximum rather than a default, so this is the only way to reach what lies past it. It is an offset and not a cursor: each request is a fresh read, so a row added or removed between two pages may be seen twice or missed once. An offset past the end is an empty list and not an error, because total is the only way to know where the end was and it may move. It does not change total, which counts the whole set before either parameter is applied. | |
max_results |
query | integer | no | 100 |
1000 | Cap on rows returned in one page; total counts them all. A default a caller may raise, up to a hard maximum of 1000; page past that with offset. |
Response fields:
| Field | Type | Description |
|---|---|---|
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
path |
string | The file that was asked about. |
file |
object | What is known about the asked file itself. |
language |
string | The language scc detected. |
state |
string | read when its imports were read; no_import_query for a language this release reads no imports from; not_analysed when the analysis pass skipped it. Only read means its own imports are known. |
role |
string | test (a runnable test), test_support or production. |
generated |
boolean | The file carries a generated-code header. |
direction |
string | The direction answered. |
depth |
integer | The hops listed, after any reduction. |
dependencies |
array | The edges, hop 1 first, then path order. This page only. |
direction |
string | imports or imported_by. |
hop |
integer | 1 or 2. |
path |
string | The file at the far end. Absent on a forwards package row and on an external row, which end at something that is not a file. |
package |
string | On a package row, the package directory; on an external row, the import as written. |
members |
integer | On a forwards package row, how many files the package holds. |
kind |
string | file, package, same_package, name_convention or mock. |
resolution |
string | exact, probable or external — on a hop-2 row, the weaker of its two links. |
role |
string | test, test_support or production. |
generated |
boolean | The file is generated code; edges into it swamp a blast radius. |
import |
string | The import that produced the edge, as written. Absent on same_package and name_convention rows, which no statement produced. |
line |
integer | The import's line in the importing file. |
via |
string | On a hop-2 row, the hop-1 file it was reached through. |
total |
integer | Rows across every page, counted before offset and max_results. |
offset |
integer | The offset this page was read at, echoed so a paging caller can check what it got rather than what it asked for. |
truncated |
boolean | Rows remain past this page. |
further |
object | Per direction, how many more files are reachable past the last hop listed. Counts only: they are never rows. |
unresolved |
array | The asked file's imports that resolved to nothing in this repository. |
import |
string | The import as written. |
line |
integer | Its line. |
reason |
string | Why it resolved to nothing. |
internal |
boolean | It names this repository's own code (a relative path, the repository's module or package, an alias) rather than a dependency. |
unresolved_total |
integer | How many of the asked file's imports resolved to nothing. |
notices |
array of string | Sentences a consumer must show the reader: what package and probable rows mean, how runnable tests were identified, and what did not resolve. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: Which tests should I run after editing cart.go?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/repos/shop/dependencies?path=internal/cart/cart.go&direction=imported_by&tests=only"
{"repository": "example__shop", "commit_sha": "9b1c…", "path": "internal/cart/cart.go", "file": {"language": "Go", "state": "read", "role": "production", "generated": false}, "direction": "imported_by", "depth": 1, "dependencies": [{"direction": "imported_by", "hop": 1, "path": "internal/cart/cart_test.go", "kind": "name_convention", "resolution": "probable", "role": "test", "generated": false}, {"direction": "imported_by", "hop": 1, "path": "internal/cart/cart_test.go", "kind": "same_package", "resolution": "exact", "role": "test", "generated": false}], "total": 2, "offset": 0, "truncated": false, "further": {"imported_by": 3}, "unresolved": [], "unresolved_total": 0, "notices": ["Runnable tests are identified by each test runner's default file naming …"], "gaps": []}
GET /api/v1/repos/{repo}/definitions
Where a symbol is declared, within one repository
MCP tool: definitions. Authentication: API key or OAuth access token
Jump-to-definition, scoped to one repository. For the same question across every repository you may see, use GET /api/v1/definitions instead — the scope is the only difference and neither endpoint resolves anything.
PRECISE RESOLUTION IS NOT ATTEMPTED, here or anywhere in this product, and that is a decision rather than a gap in the implementation: a precise answer needs matching indexed package versions on both sides or an uploaded SCIP index, and that is where precision quietly dies. What you get is every declaration of the name, labelled.
Every result is labelled exact or probable and the two are never merged unmarked. A declaration a language grammar parsed at ingest is exact and carries a real kind; one the line-start heuristic guessed is probable. A language covered by neither produces a GAP saying so rather than an empty list: "we cannot resolve definitions for Cobol" and "this symbol is not defined here" are different answers and only one of them is safe to act on.
An empty answer says so IN THE RESPONSE, as a notice rather than as a gap: nothing failed, no declaration of the name was recognised in this repository, and it may well be declared in another one. That notice states the boundary rather than claiming to be complete across it — the SCOPE is exact and the COVERAGE is not, because a declaration is found by matching a parsed symbol table and that table holds the forms this build's grammars extract. Search for the name before concluding nothing declares it.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
path | string | yes | Repository identifier, clone URL, owner/name, or a unique name suffix. |
||
symbol |
query | string | yes | The identifier to resolve. Matched case-sensitively, and as a WHOLE identifier: Walk does not match ErrTerminateWalk or walkDirectory. | ||
offset |
query | integer | no | 0 |
How many locations to skip before listing. With max_results it is a page: offset 0 is the first page, offset 50 with max_results 50 is the second. max_results is a hard maximum rather than a default, so this is the only way to reach what lies past it. It is an offset and not a cursor: each request is a fresh read, so a row added or removed between two pages may be seen twice or missed once. An offset past the end is an empty list and not an error, because total is the only way to know where the end was and it may move. It does not change total, which counts the whole set before either parameter is applied. | |
max_results |
query | integer | no | 50 |
1000 | Cap on returned locations. A HARD MAXIMUM of 1000 rather than a default a caller raises: a larger value is clamped to it and the response says so in gaps. total counts every declaration before it bit, and offset reaches the ones past this page. |
Response fields:
| Field | Type | Description |
|---|---|---|
offset |
integer | The offset this page was read at, echoed so a paging caller can check what it got rather than what it asked for. |
repository_name |
string | The repository's own name — its normalised clone URL, or its path for a directory source. This is what to show a person; repository is the identifier to pass back. |
locations |
array | The declarations, in repository order. |
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
path |
string | Path relative to the repository root. |
line |
integer | 1-based line of the declaration. |
content |
string | The declaring line. |
ranges |
array | Where within content the symbol was found: [start, end) BYTE offsets, ascending, end exclusive. From the search that produced the line rather than from scanning it for the symbol, so a line naming both validate and valid marks the one that was matched. Empty is a real answer and means there is no offset to report — the line is still a location. |
language |
string | The language scc detected. |
kind |
string | What the declaration IS: function, method, constructor, class, interface, struct, enum, trait, type, module, property, macro, impl, constant or variable where a grammar read it, and the undifferentiated word "declaration" where the line-start heuristic classified it. It is the field to group, filter or disambiguate on when a name has several declarations: a class Router and a variable Router are not the same answer, and the declaring line only tells you which for a language you can read. Read it beside confidence rather than instead of it — confidence says how the answer was reached and kind says what was found. |
confidence |
string | exact when a parser resolved it, probable when a heuristic did. Never absent: an unlabelled navigation result is one the caller cannot decide whether to trust. |
total |
integer | Locations found before max_results truncation. |
references_total |
integer | How many USES of the same name this repository holds — the other question, answered here so that a reader of a declaration does not have to ask a second time to find out whether anything calls it. ABSENT when this answer could not count them, and present as 0 when the name is genuinely used nowhere: those are different facts and only the second is safe to act on, so read it by presence and never by a non-zero test. It is omitted whenever the count would be a lower bound — a matching file in a language with no grammar and no declaration patterns, for one — because a lower bound presented as a count is the failure this product exists not to have. |
notices |
array of string | Sentences that change what this answer MEANS, above the data rather than beside it. Present and empty on an answer that needs none. An empty locations list carries one here, because a repository-scoped zero is otherwise indistinguishable from an estate-wide one: this endpoint resolves within one repository, so a symbol declared in another repository correctly returns nothing and that is a different fact from "nothing declares it". Not a gap — nothing was skipped and nothing was unanswerable. The same sentence also says what the answer does NOT know: the symbol table it was matched against covers the declaration forms this build's grammars extract, so an empty answer is "nothing here was recognised as declaring it" and never "nothing here declares it". |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: Where is RetryWithBackoff defined?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/repos/deploy-tool/definitions?symbol=RetryWithBackoff"
{"repository_name": "https://example.invalid/deploy-tool", "locations": [{"repository": "example__deploy-tool", "commit_sha": "4f0c...", "path": "retry.go", "line": 11, "content": "func RetryWithBackoff(ctx context.Context, attempts int) error {", "ranges": [[5, 21]], "language": "Go", "kind": "declaration", "confidence": "probable"}], "total": 1, "references_total": 1, "notices": [], "gaps": []}
GET /api/v1/repos/{repo}/references
Where a symbol is used, within one repository
MCP tool: references. Authentication: API key or OAuth access token
Find-references, scoped to one repository and labelled the same way definitions are. A match on a declaring line is a definition and is excluded here, so the two calls partition the matches rather than overlapping.
Precise resolution is not attempted; see definitions for why. EVERY answer here is repository-scoped, including a populated one: three uses means three uses IN THIS REPOSITORY, and estate-wide impact analysis is GET /api/v1/references. An empty answer restates the scope as a notice; a populated one does not, because a sentence on every response is one nobody reads.
A LOCATION HERE CARRIES NO kind, WHERE A DEFINITION DOES, and the asymmetry is structural rather than an omission: a reference is a name that appeared in a file, and nothing this endpoint does decides what the thing named is. Reporting a kind would be the resolution it says it does not perform, which is the same reason every result here is probable. To learn what a name declares, ask definitions.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
path | string | yes | Repository identifier, clone URL, owner/name, or a unique name suffix. |
||
symbol |
query | string | yes | The identifier to find uses of. Matched case-sensitively, and as a WHOLE identifier: Walk does not match ErrTerminateWalk or walkDirectory. Content search matches substrings; find-references does not. | ||
offset |
query | integer | no | 0 |
How many locations to skip before listing. With max_results it is a page: offset 0 is the first page, offset 50 with max_results 50 is the second. max_results is a hard maximum rather than a default, so this is the only way to reach what lies past it. It is an offset and not a cursor: each request is a fresh read, so a row added or removed between two pages may be seen twice or missed once. An offset past the end is an empty list and not an error, because total is the only way to know where the end was and it may move. It does not change total, which counts the whole set before either parameter is applied. | |
max_results |
query | integer | no | 50 |
1000 | Cap on returned locations. A HARD MAXIMUM of 1000 rather than a default a caller raises: a larger value is clamped to it and the response says so in gaps. total counts every use before it bit, and offset reaches the ones past this page. |
Response fields:
| Field | Type | Description |
|---|---|---|
offset |
integer | The offset this page was read at, echoed so a paging caller can check what it got rather than what it asked for. |
repository_name |
string | The repository's own name — its normalised clone URL, or its path for a directory source. This is what to show a person; repository is the identifier to pass back. |
locations |
array | The uses, in repository order. |
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
path |
string | Path relative to the repository root. |
line |
integer | 1-based line of the use. |
content |
string | The using line. |
ranges |
array | Where within content the symbol was found: [start, end) BYTE offsets, ascending, end exclusive. From the search that produced the line rather than from scanning it for the symbol, so a line naming both validate and valid marks the one that was matched. Empty is a real answer and means there is no offset to report — the line is still a location. |
language |
string | The language scc detected. |
confidence |
string | exact or probable, never absent. |
total |
integer | Locations found before max_results truncation. |
definitions_total |
integer | How many DECLARATIONS of the same name this repository holds — the other question, answered here so that a reader of a use list can see whether the thing being used is declared in the same repository at all. ABSENT when this answer could not count them, and present as 0 when the name is declared nowhere here: read it by presence and never by a non-zero test, because a repository whose deep pass has not run and a repository that declares the name nowhere are different facts. Omitted whenever the count would be a lower bound — the same rule references_total carries on the definitions endpoint. |
notices |
array of string | Sentences that change what this answer MEANS. Present and empty on an answer that needs none; an empty locations list carries one saying that uses were looked for in this repository only, because "nothing here uses it" and "nothing uses it" are different facts. A POPULATED answer carries none: every answer from this endpoint is repository-scoped, and a sentence on every response is one nobody reads. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: Who calls RetryWithBackoff?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/repos/deploy-tool/references?symbol=RetryWithBackoff"
{"repository_name": "https://example.invalid/deploy-tool", "locations": [{"repository": "example__deploy-tool", "commit_sha": "4f0c...", "path": "deploy.go", "line": 42, "content": "\tif err := RetryWithBackoff(ctx, 3); err != nil {", "ranges": [[11, 27]], "language": "Go", "confidence": "probable"}], "total": 1, "definitions_total": 1, "notices": [], "gaps": []}
GET /api/v1/definitions
Where a symbol is declared, across the whole visible estate
MCP tool: estate_definitions. Authentication: API key or OAuth access token
Jump-to-definition across every repository you may see, rather than one at a time. The scope is exactly what your permissions allow and is applied before anything is read, so a repository you cannot see contributes no result and no count.
WHAT exact MEANS HERE, AND WHAT IT DOES NOT. A declaration labelled exact was read by a language grammar during ingestion, so its name, kind, file and line are parsed facts and not a guess. That is a claim about WHAT EACH DECLARATION IS. It is NOT a claim that the right one was picked: no binding resolution is performed, no imports are followed and no types are inferred, so eleven declarations of Handler are eleven candidates and not one answer. Every response that carries exact locations says this in notices, and a caller that needs THE definition has to disambiguate by repository, path or signature.
A name matched in more files than the configured ceiling DECLINES rather than returning thousands of candidates: locations is empty, declined is true, and a gap names the candidate count and the ceiling. That is not a failure and not an absence — Error is a candidate in over five thousand files on a modest estate, and a name that common cannot be resolved by matching it. Ask one repository instead, or search the estate with filters.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
symbol |
query | string | yes | The identifier to resolve. Matched case-sensitively, and as a WHOLE identifier: Walk does not match ErrTerminateWalk or walkDirectory. | ||
offset |
query | integer | no | 0 |
How many locations to skip before listing. With max_results it is a page: offset 0 is the first page, offset 50 with max_results 50 is the second. max_results is a hard maximum rather than a default, so this is the only way to reach what lies past it. It is an offset and not a cursor: each request is a fresh read, so a row added or removed between two pages may be seen twice or missed once. An offset past the end is an empty list and not an error, because total is the only way to know where the end was and it may move. It does not change total, which counts the whole set before either parameter is applied. | |
path |
query | string | no | The file you are asking from. Only files in its language, as scc names it from the file name, are matched, counted and held to the ceiling (.ts includes .d.ts). An extension scc cannot place filters nothing, and notices says so. | ||
language |
query | string | no | An scc language name (Go, TypeScript, Python; any case) for a caller with no file. WINS over path when both are given. Includes its own headers, typings or JSX. A name scc does not know filters nothing, and notices says so. | ||
max_results |
query | integer | no | 50 |
1000 | Cap on returned locations. It caps the PAYLOAD, not the ceiling: total counts every location found before this bit. A HARD MAXIMUM of 1000 rather than a default a caller raises: a larger value is clamped to it and the response says so in gaps. offset reaches the rest. |
Response fields:
| Field | Type | Description |
|---|---|---|
offset |
integer | The offset this page was read at, echoed so a paging caller can check what it got rather than what it asked for. |
locations |
array | The declarations, best-ranked first. |
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
repository_name |
string | The repository's own name, per location, because an estate-wide answer spans repositories and the identifier alone is not what to show a person. |
path |
string | Path relative to the repository root. |
line |
integer | 1-based line of the declaration. |
content |
string | The declaring line. |
ranges |
array | Where within content the symbol was found: [start, end) BYTE offsets, ascending, end exclusive. From the search that produced the line rather than from scanning it for the symbol, so a line naming both validate and valid marks the one that was matched. Empty is a real answer and means there is no offset to report — the line is still a location. |
language |
string | The language scc detected. |
kind |
string | What the declaration IS: function, method, constructor, class, interface, struct, enum, trait, type, module, property, macro, impl, constant or variable where a grammar read it, and the undifferentiated word "declaration" where the line-start heuristic classified it. It is the field to group, filter or disambiguate on when a name has several declarations: a class Router and a variable Router are not the same answer, and the declaring line only tells you which for a language you can read. Read it beside confidence rather than instead of it — confidence says how the answer was reached and kind says what was found. |
confidence |
string | exact when a grammar parsed this declaration, probable when the line-start heuristic did. exact is about what the declaration IS and never about which declaration you meant — see notices. |
total |
integer | Locations found before max_results truncation. |
truncated |
boolean | More locations matched than max_results returned. False means total is the whole answer rather than a lower bound. |
candidates |
integer | How many FILES held a candidate match across the visible estate, before the ceiling and before max_results. This is the number the ceiling is compared against. |
ceiling |
integer | The candidate-file ceiling in force, so a decline can be acted on rather than only observed. Operator configuration (search.symbols.crossRepoCeiling); it is not a parameter of this call. |
declined |
boolean | True when candidates exceeded ceiling: nothing was classified and locations is empty. Read it before reading locations — a declined answer and "nothing declares this" are opposite facts and an empty list cannot tell them apart. |
languages_searched |
array of string | The languages this answer was filtered to, from path or language. EMPTY MEANS EVERY LANGUAGE WAS SEARCHED — no filter was asked for, or the one asked for could not be resolved. notices says which, in words. |
notices |
array of string | Sentences that change what this answer MEANS, above the data rather than beside it. Present and empty on an answer that needs none. An answer carrying exact locations always carries the sentence saying that exact means parsed and not resolved; a declined answer says it declined; an empty one says the estate declares the name nowhere it could see. A language-filtered answer says which languages it was filtered to and how to widen it; an unfiltered one with locations says how many languages they span and how to narrow it. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: Where is RetryWithBackoff declared anywhere?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/definitions?symbol=RetryWithBackoff"
{"locations": [{"repository": "example__deploy-tool", "repository_name": "https://example.invalid/deploy-tool", "commit_sha": "4f0c...", "path": "retry.go", "line": 11, "content": "func RetryWithBackoff(ctx context.Context, attempts int) error {", "ranges": [[5, 21]], "language": "Go", "kind": "function", "confidence": "exact"}], "total": 1, "truncated": false, "candidates": 3, "ceiling": 1200, "declined": false, "languages_searched": [], "notices": ["NOT FILTERED BY LANGUAGE: The name was matched in every language, and the 1 location found is Go. Pass path (the file you are asking from) or language to keep only that language: ...", "EXACT MEANS PARSED, NOT RESOLVED: ..."], "gaps": []}
GET /api/v1/references
Where a symbol is used, across the whole visible estate
MCP tool: estate_references. Authentication: API key or OAuth access token
Find-references across every repository you may see. A match on a declaring line is a definition and is excluded here, so the two calls partition the matches rather than overlapping.
EVERY RESULT IS probable AND THAT IS STRUCTURAL, not a limitation of the current version. A reference is a name that appeared in a file. Get in client.Get() and Get in type Get struct are the same three characters, and nothing here distinguishes them: no binding resolution, no import following, no type inference. There is no configuration and no future grammar that makes a reference exact, so treat every location as a candidate to check rather than as an established use.
THE SAME REASONING IS WHY NO LOCATION HERE CARRIES A kind, where every declaration from GET /api/v1/definitions does. A kind is a claim about what a name refers to, this endpoint resolves nothing, and a guessed kind on an unresolved name would be exactly the false precision the paragraph above rules out.
A name matched in more files than the configured ceiling DECLINES, exactly as definitions does and for the same reason.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
symbol |
query | string | yes | The identifier to find uses of. Matched case-sensitively, and as a WHOLE identifier: Walk does not match ErrTerminateWalk or walkDirectory. Content search matches substrings; find-references does not. | ||
offset |
query | integer | no | 0 |
How many locations to skip before listing. With max_results it is a page: offset 0 is the first page, offset 50 with max_results 50 is the second. max_results is a hard maximum rather than a default, so this is the only way to reach what lies past it. It is an offset and not a cursor: each request is a fresh read, so a row added or removed between two pages may be seen twice or missed once. An offset past the end is an empty list and not an error, because total is the only way to know where the end was and it may move. It does not change total, which counts the whole set before either parameter is applied. | |
path |
query | string | no | The file you are asking from. Only files in its language, as scc names it from the file name, are matched, counted and held to the ceiling (.ts includes .d.ts). An extension scc cannot place filters nothing, and notices says so. | ||
language |
query | string | no | An scc language name (Go, TypeScript, Python; any case) for a caller with no file. WINS over path when both are given. Includes its own headers, typings or JSX. A name scc does not know filters nothing, and notices says so. | ||
max_results |
query | integer | no | 50 |
1000 | Cap on returned locations. It caps the PAYLOAD, not the ceiling. A HARD MAXIMUM of 1000 rather than a default a caller raises: a larger value is clamped to it and the response says so in gaps. offset reaches the rest. |
Response fields:
| Field | Type | Description |
|---|---|---|
offset |
integer | The offset this page was read at, echoed so a paging caller can check what it got rather than what it asked for. |
locations |
array | The uses, best-ranked first. |
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
repository_name |
string | The repository's own name, per location, because an estate-wide answer spans repositories. |
path |
string | Path relative to the repository root. |
line |
integer | 1-based line of the use. |
content |
string | The using line. |
ranges |
array | Where within content the symbol was found: [start, end) BYTE offsets, ascending, end exclusive. From the search that produced the line rather than from scanning it for the symbol, so a line naming both validate and valid marks the one that was matched. Empty is a real answer and means there is no offset to report — the line is still a location. |
language |
string | The language scc detected. |
confidence |
string | probable, always, on every result from this endpoint. A name match across an estate is a candidate and not a resolution. |
total |
integer | Locations found before max_results truncation. |
truncated |
boolean | More locations matched than max_results returned. |
candidates |
integer | How many FILES held a candidate match across the visible estate, before the ceiling and before max_results. |
ceiling |
integer | The candidate-file ceiling in force. Operator configuration (search.symbols.crossRepoCeiling); it is not a parameter of this call. |
declined |
boolean | True when candidates exceeded ceiling: nothing was classified and locations is empty. A declined answer and "nothing uses this" are opposite facts. |
languages_searched |
array of string | The languages this answer was filtered to, from path or language. EMPTY MEANS EVERY LANGUAGE WAS SEARCHED — no filter was asked for, or the one asked for could not be resolved. notices says which, in words. |
notices |
array of string | Sentences that change what this answer MEANS. Present and empty on an answer that needs none. A populated answer carries the sentence saying every location is a name match rather than a resolved use. A language-filtered answer says which languages it was filtered to and how to widen it; an unfiltered one with locations says how many languages they span and how to narrow it. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: Who uses RetryWithBackoff anywhere?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/references?symbol=RetryWithBackoff&path=cmd/deploy/main.go"
{"locations": [{"repository": "example__deploy-tool", "repository_name": "https://example.invalid/deploy-tool", "commit_sha": "4f0c...", "path": "deploy.go", "line": 42, "content": "\tif err := RetryWithBackoff(ctx, 3); err != nil {", "ranges": [[11, 27]], "language": "Go", "confidence": "probable"}], "total": 1, "truncated": false, "candidates": 3, "ceiling": 1200, "declined": false, "languages_searched": ["Go"], "notices": ["FILTERED BY LANGUAGE: only Go files were searched, because path names a Go file ...", "EVERY LOCATION IS A NAME MATCH: ..."], "gaps": []}
GET /api/v1/repos/{repo}/technologies
Which technologies one file uses, with the evidence
MCP tool: file_technologies. Authentication: API key or OAuth access token
The per-FILE half of the dependency index. find_usages answers "which files use boto3"; this answers "what does this file use", which is the question somebody reading the file has.
Evidence is the matched keyword and the line it was found on, so an attribution can be checked rather than trusted. Whole files are scanned, not just their first hundred lines, so a deep import is attributed.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
path | string | yes | Repository identifier, clone URL, owner/name, or a unique name suffix. |
||
path |
query | string | yes | File path relative to the repository root. |
Response fields:
| Field | Type | Description |
|---|---|---|
technologies |
array | One entry per attributed technology. |
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
technology |
string | The technology that was matched. |
path |
string | The file that was examined. |
language |
string | The language scc detected. |
line |
integer | 1-based line the evidence was found on. |
evidence |
string | The matched keyword, so the attribution can be checked rather than trusted. |
confidence |
string | exact or probable. |
version |
string | The resolved version, absent when no manifest could answer. An absent version means unknown rather than none. |
version_source |
string | lock, declared or inferred. A lock says what IS installed; a declared range says what MIGHT be, and only the first supports a vulnerability claim. |
version_confidence |
string | exact when a lockfile pinned it and the import-to-package mapping is known, probable when either half was a convention. Separate from confidence above, which is about whether this file uses the technology at all. |
manifest_path |
string | The manifest the version came from, relative to the repository root — "resolved from services/api/go.mod" can be opened and checked. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: What does this file use?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/repos/data-pipeline/technologies?path=app/deep_import.py"
{"technologies": [{"repository": "example__data-pipeline", "commit_sha": "7c9a...", "technology": "AWS SDK (Boto3)", "path": "app/deep_import.py", "language": "Python", "line": 154, "evidence": "boto3", "confidence": "probable"}], "gaps": []}
POST /api/v1/vulnerabilities
Which files use a vulnerable version of a dependency
MCP tool: find_vulnerable_usages. Authentication: API key or OAuth access token
The join nobody else can make from one index: the file that imports a package, the version the repository's manifests resolved it to, and the advisory that affects that version — answered from an advisory database that ships inside this binary and is NEVER fetched at runtime.
READ THE unknown ARRAY BEFORE CONCLUDING ANYTHING. An empty vulnerabilities array does NOT mean the estate is clean: a dependency whose version could not be resolved is reported as unknown, not as "not vulnerable", and telling somebody they are safe on the strength of an unresolved version is the failure this endpoint is shaped to prevent.
Two labels carry the uncertainty and both matter. match_confidence is exact only when a LOCKFILE pinned the version; a declared range says what MIGHT be installed and yields a probable match, which is a lead rather than a confirmation. advisory_database says how old the data is — an advisory published since then is not in this answer.
Each advisory carries the weakness classes its publisher recorded in cwe, in the same CWE-89 form a security finding from findings carries, so "the same weakness in our own code and in a dependency" is a comparison of two fields. It matches by exact identifier only. Only GitHub's advisories record CWEs; a GO, PYSEC or RUSTSEC advisory takes them from a record that shares one of its identifiers. An advisory with none reads cwe_state: unknown — the class was not recorded, which is not a claim that there is none — and notices says how many in prose.
This is estate context, not a replacement for Snyk or Dependabot. The differentiated claim is WHICH FILES, joined with ownership and findings; a dedicated scanner will always win on the breadth and freshness of the advisory data itself.
Request body:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
technology |
body | string | no | Only usages of this technology, as the detector names it — e.g. "Tokio". | ||
package |
body | string | no | Only this package, as the ECOSYSTEM names it — e.g. "tokio", "org.apache.logging.log4j:log4j-core". Different from technology, which is the detector's human-facing label. | ||
ecosystem |
body | string | no | Only dependencies in this ecosystem. Manifests are parsed for these six and no others. One of: python | go | rust | node | java | dotnet. | ||
severity |
body | string | no | Only advisories at this severity or worse. One of: critical | high | moderate | low | unknown. | ||
repo |
body | string | no | Restrict to one repository, by identifier or unique short name. | ||
group |
body | string | no | Restrict to one repository group — the ownership filter, so "everything the payments team owns" is one call. It can only narrow what the principal may already see. | ||
include_unknown |
body | boolean | no | true |
Return the dependencies whose version could not be resolved. Leaving it on is strongly advised: the count is reported either way, and turning it off is how a report comes to look cleaner than the estate is. | |
offset |
body | integer | no | 0 |
How many entries to skip before listing. With max_results it is a page: offset 0 is the first page, offset 50 with max_results 50 is the second. max_results is a hard maximum rather than a default, so this is the only way to reach what lies past it. It is an offset and not a cursor: each request is a fresh read, so a row added or removed between two pages may be seen twice or missed once. An offset past the end is an empty list and not an error, because total is the only way to know where the end was and it may move. It does not change total, which counts the whole set before either parameter is applied. | |
max_results |
body | integer | no | 50 |
1000 | Cap on returned vulnerable usages, and on the unknown list beside them. The totals are reported before the cap. A HARD MAXIMUM of 1000 rather than a default a caller raises: a larger value is clamped to it and the response says so in gaps. offset pages both lists together. |
Response fields:
| Field | Type | Description |
|---|---|---|
offset |
integer | The offset this page was read at, echoed so a paging caller can check what it got rather than what it asked for. |
vulnerabilities |
array | One entry per file that uses a package at a version an advisory affects. |
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
path |
string | Path relative to the repository root. |
language |
string | The language scc detected. |
line |
integer | 1-based line the import was found on. |
technology |
string | The detector's name for it. |
ecosystem |
string | One of the six. |
package |
string | The package as the ecosystem names it. |
state |
string | vulnerable, not_vulnerable or unknown. Never absent. |
match_confidence |
string | exact when a lockfile pinned the version, probable when the version came from a declared range or a heuristic name mapping. A probable match is a lead, not a confirmation. |
advisories |
array | The advisories affecting this version. |
id |
string | The advisory identifier — a CVE, GHSA or RUSTSEC id. |
aliases |
array of string | The same advisory's other identifiers. |
severity |
string | critical, high, moderate, low or unknown. |
summary |
string | One line describing the vulnerability. |
fixed_in |
string | The first version that is not affected. ABSENT when no fix is published, which is the finding that most needs acting on. |
cwe_state |
string | recorded or unknown, never absent. unknown means the advisory's publisher recorded no weakness class, or the bundle predates CWE data (advisory_database.cwe_recorded is false) — never that the vulnerability has no weakness. |
cwe |
array of string | The weakness classes the advisory's publisher recorded, as CWE-89 — the same identifiers a security finding's cwe uses, so a weakness in the estate's own code and in a dependency can be matched, by exact identifier only. An advisory may record several. From OSV database_specific.cwe_ids, or from a record sharing one of this advisory's identifiers. Present only when cwe_state is recorded. |
version |
string | The resolved version, absent when no manifest could answer. An absent version means unknown rather than none. |
version_source |
string | lock, declared or inferred. A lock says what IS installed; a declared range says what MIGHT be, and only the first supports a vulnerability claim. |
version_confidence |
string | exact when a lockfile pinned it and the import-to-package mapping is known, probable when either half was a convention. Separate from confidence above, which is about whether this file uses the technology at all. |
manifest_path |
string | The manifest the version came from, relative to the repository root — "resolved from services/api/go.mod" can be opened and checked. |
unknown |
array | Dependencies whose vulnerability status could NOT be determined. This is not the same as not vulnerable and must never be reported as safe. |
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
path |
string | Path relative to the repository root. |
line |
integer | 1-based line the import was found on. |
technology |
string | The detector's name for it. |
ecosystem |
string | The ecosystem, where the language named one. |
state |
string | Always "unknown". |
reason |
string | Why the status could not be determined. |
total |
integer | Vulnerable usages found before max_results truncation. |
unknown_total |
integer | Dependencies whose status could not be determined, before truncation. Reported even when include_unknown is off. |
by_severity |
object | Vulnerable usages keyed by severity, counting only repositories this principal may see. |
advisory_database |
object | The database that answered: when it was built, how old it is, whether it can be relied on as current coverage, and where it came from. The sentence a reader must see is in notices rather than here, so it is stated once. |
loaded |
boolean | False means no database is available at all. |
origin |
string | "embedded" or the side-loaded bundle's path. |
signed |
boolean | Whether the bundle's signature was verified. |
generated_at |
string | When the bundle was built, RFC 3339. |
age_days |
integer | How old it is, in whole days. |
advisories |
integer | How many advisories it holds. |
stale |
boolean | True when this database is not current coverage. A fixed definition an operator cannot change: no database at all, the compiled-in seed at any age, or a side-loaded bundle more than 30 days old. advisories.warnIfOlderThanDays governs the wording of the notice and never this field. |
stale_reason |
string | Why, when stale: "absent", "seed" or "age". Absent when the database is current coverage. |
cwe_recorded |
boolean | Whether the bundle carries CWE identifiers at all. False on a bundle built before they were kept, in which case every advisory's cwe_state is unknown. |
notices |
array of string | Sentences a consumer must show the reader — the staleness warning, the unknown-is-not-safe statement and, whenever any advisory is listed, what its cwe does and does not claim. They are prose rather than flags because a flag a client does not render reaches nobody. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: Everything the payments team owns that uses a vulnerable tokio
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
-X POST \
-H 'Content-Type: application/json' \
-d '{"group": "payments", "package": "tokio"}' \
"$SEARCHCODE_URL/api/v1/vulnerabilities"
{
"vulnerabilities": [
{
"repository": "example__rust-ledger",
"commit_sha": "1a2b3c4d…",
"path": "src/ledger.rs",
"language": "Rust",
"line": 3,
"technology": "Tokio",
"ecosystem": "rust",
"package": "tokio",
"state": "vulnerable",
"match_confidence": "exact",
"version": "1.20.0",
"version_source": "lock",
"version_confidence": "exact",
"manifest_path": "Cargo.lock",
"advisories": [
{"id": "RUSTSEC-2023-0001", "aliases": ["CVE-2023-22466", "GHSA-7rrj-xr53-82p7"],
"severity": "moderate",
"summary": "reject_remote_clients is ignored on Windows named pipes", "fixed_in": "1.23.1",
"cwe_state": "recorded", "cwe": ["CWE-665"]}
]
}
],
"unknown": [],
"total": 1,
"unknown_total": 0,
"by_severity": {"moderate": 1},
"advisory_database": {"loaded": true, "origin": "/etc/searchcode/advisories.bundle",
"signed": true, "generated_at": "2026-08-06T00:00:00Z", "age_days": 3,
"advisories": 41283, "stale": false, "cwe_recorded": true},
"notices": ["About the advisory CWEs: an advisory's cwe is the weakness class its publisher recorded ..."],
"gaps": []
}
GET /api/v1/dependency-licences
The licences of the estate's dependencies
MCP tool: licence_audit. Authentication: API key or OAuth access token
Every dependency the visible estate declares, with the licence its manifest recorded, rolled up by licence.
WHAT THIS CAN AND CANNOT ANSWER, because the difference is a legal one. A licence is reported only where a manifest in the repository states one: npm's lockfile records a licence per dependency from v2 onward, and package.json, Cargo.toml, pyproject.toml, pom.xml and .csproj each state the licence of the project THEY describe. Everything else is reported as unknown and counted, because guessing a dependency's licence from its name is a legal claim nobody could trace back.
Licence strings are reported exactly as the manifest wrote them. "MIT", "MIT OR Apache-2.0" and "Apache License 2.0" are three different strings and normalising the third onto an SPDX identifier would be a guess with consequences.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
query | string | no | Restrict to one repository. Repository identifier, clone URL, owner/name, or a unique name suffix. |
||
group |
query | string | no | Restrict to one repository group. It can only narrow what the principal may already see. | ||
ecosystem |
query | string | no | Only dependencies in this ecosystem. One of: python | go | rust | node | java | dotnet. | ||
licence |
query | string | no | Only dependencies carrying this licence string, matched exactly as the manifest wrote it. Pass "unknown" for the ones no manifest stated. | ||
offset |
query | integer | no | 0 |
How many dependency rows to skip before listing. With max_results it is a page: offset 0 is the first page, offset 50 with max_results 50 is the second. max_results is a hard maximum rather than a default, so this is the only way to reach what lies past it. It is an offset and not a cursor: each request is a fresh read, so a row added or removed between two pages may be seen twice or missed once. An offset past the end is an empty list and not an error, because total is the only way to know where the end was and it may move. It does not change total, which counts the whole set before either parameter is applied. | |
max_results |
query | integer | no | 50 |
1000 | Cap on returned dependency rows. The rollup counts everything regardless. A HARD MAXIMUM of 1000 rather than a default a caller raises: a larger value is clamped to it and the response says so in gaps. Page the dependencies list with offset. |
Response fields:
| Field | Type | Description |
|---|---|---|
offset |
integer | The offset this page was read at, echoed so a paging caller can check what it got rather than what it asked for. |
licences |
array | The rollup, most-declared first. Counts every dependency the principal may see and no others, over the same rows as total_dependencies and before max_results: the dependencies column sums to total_dependencies exactly, and the unknown row equals unknown_total. |
licence |
string | The licence string, or "unknown" where no manifest stated one. |
dependencies |
integer | Dependency declarations carrying this licence — the same unit total_dependencies counts, so these sum to it. |
distinct_packages |
integer | Distinct (ecosystem, package) pairs carrying this licence, which is the smaller number and the one to quote for "how many packages are GPL-3.0". It does NOT sum to total_dependencies or to anything else: a package stated MIT in one repository and stated nowhere in another is distinct on two rows. |
repositories |
integer | Repositories that declare at least one. |
dependencies |
array | The individual rows. |
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
ecosystem |
string | One of the six. |
package |
string | The package as the ecosystem names it. |
version |
string | The pin or declared range the manifest carried, absent when it named none. |
licence |
string | The licence, or "unknown". Never an empty string: unknown and unlicensed are different claims. |
manifest_path |
string | The manifest this row came from, so it can be opened and checked. |
declared_by |
string | dependency when the manifest recorded the licence of this package, project when it is the licence of the project the manifest describes. |
total_dependencies |
integer | Dependency declarations matching this query before max_results truncation — one per (repository, manifest, package), so the same package declared in three repositories counts three times. Named for its unit because the rollup beside it also counts distinct packages, and the two figures differ by more than a factor of two on a real estate. |
unknown_total |
integer | Dependency declarations whose licence no manifest stated, in the same unit as total_dependencies and equal to the rollup's unknown row. Reported separately because an audit with a large unknown count is a different conversation from a clean one, and unknown is never the stronger claim that a package is unlicensed. |
notices |
array of string | Sentences a consumer must show the reader, including which ecosystems can supply a per-dependency licence at all. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: What licences are in the estate?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/dependency-licences?max_results=2"
{"licences": [{"licence": "unknown", "dependencies": 40, "distinct_packages": 31, "repositories": 4}, {"licence": "MIT", "dependencies": 12, "distinct_packages": 9, "repositories": 3}], "dependencies": [{"repository": "example__polyglot-monorepo", "commit_sha": "4f0c…", "ecosystem": "node", "package": "lodash", "version": "4.17.20", "licence": "MIT", "manifest_path": "package-lock.json", "declared_by": "dependency"}], "total_dependencies": 52, "unknown_total": 40, "notices": ["40 dependencies of 52 carry no licence any manifest stated, and are reported as unknown rather than as unlicensed."], "gaps": []}
GET /api/v1/explain-access
Why can this principal reach this repository?
MCP tool: explain_access. Authentication: API key or OAuth access token
The audited answer to "why can Bob see this?", with the bindings that granted it or the absence of one. Available to administrators and auditors only: it discloses the policy, the repository catalogue and another principal's groups in one response.
Both outcomes are audited, including the refusal — who has been asking who can see what is itself an audit question.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
query | string | yes | Repository identifier, clone URL, owner/name, or a unique name suffix. |
||
user |
query | string | no | Whose access to explain: a subject id, a username, or an email address. Omit for the caller's own. An address nobody has signed in with yet is answered as that address with no groups, which is what that person would get on first sign-in through authz.bindings[].users. | ||
groups |
query | string | no | Comma-separated groups to evaluate against instead of the subject's. In proxy mode a subject's groups arrive on each request and are stored nowhere authoritative, so this is how the "what if they were in example-engineers" question is asked. | ||
permission |
query | string | no | Which permission to explain. Defaults to search. One of: search | view_file | view_history | view_blame | use_api | use_mcp. |
Response fields:
| Field | Type | Description |
|---|---|---|
allow |
boolean | Whether the subject holds the permission. |
reason |
string | The human-readable why. |
subject |
string | Whose access was explained. |
email |
string | The subject's email address as the authoriser saw it, which is what authz.bindings[].users and authz.adminUsers match. Empty when there is none. |
groups |
array of string | The identity groups the answer was computed from. |
repository |
string | The repository that was asked about. |
repo_groups |
array of string | The repository groups the target is in, so a denial reads without a second lookup. |
matched |
array | The bindings that granted. Empty on a denial. |
binding |
string | The binding's name. |
via_group |
string | The identity group that matched, or "*". Empty when the binding matched through via_user. |
via_user |
string | The binding's users entry that matched, when the grant names the person rather than one of their groups. Omitted otherwise. |
repo_group |
string | The repository group that carried the grant. |
repo_group_matched_by |
string | "literal", or the glob that selected it. |
role |
string | The role the binding granted. |
source |
string | declarative, anonymous, default or service — a rule somebody wrote, or one the product supplied. |
evaluated |
integer | How many rules were considered, including implicit ones. |
notices |
string | Why the repository is not in the catalogue, when that is the answer — chiefly that a code-host connection retired it, with the reason and when. "No repository with that identifier is known" is true and sends somebody hunting for a typo; this says the repository was there and what removed it. |
Example: Why can this person read billing-service?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/explain-access?repo=billing-service&user=a.person"
{"allow": true, "reason": "binding \"Auditors read everything with an audience\" grants viewer over example-group", "subject": "a.person", "email": "a.person@example.com", "groups": ["example-auditors"], "repository": "example__billing-service", "repo_groups": ["example-group"], "matched": [{"binding": "Auditors read everything with an audience", "via_group": "example-auditors", "repo_group": "example-group", "repo_group_matched_by": "literal", "role": "viewer", "source": "declarative"}], "evaluated": 4}
GET /api/v1/status
This instance's operational state
Not an MCP tool: operational rather than about the code: an agent asking how full the disk is has nothing useful to do with the answer, and the operator's surfaces are /api/status and /metrics Authentication: API key or OAuth access token
Configuration reload state, the licence ladder, ingestion progress, analysis coverage, the audit stream's health and disk capacity. This is where the estate's operational detail lives: the unauthenticated /api/status describes the PROCESS — build, readiness, authentication mode and which configuration file it loaded — and names no repository, no repository group and no secret fingerprint.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
detail |
query | string | no | summary |
How much of this report to build. summary, the default, omits the per-repository arrays (ingestion.repositories, index.repositories, analysis.repo_warnings), which are the great majority of the payload on a large estate and are not what a monitoring poll reads; full returns them. Nothing is deleted by the default: the omitted array on every response names exactly what the shape you asked for left out, and each of these blocks publishes a count at both details so an empty estate still says zero. One of: summary | full. |
Response fields:
| Field | Type | Description |
|---|---|---|
detail |
string | Which shape this response is: summary or full. Echoed so a consumer can tell a report that omitted its arrays from one whose estate is empty. |
omitted |
array of string | The field paths this shape does not carry, dotted from the root. Empty at detail=full and never null. It is what separates "not requested" from "there are none", which an absent key alone cannot say: ask again with detail=full to get them. |
version |
object | Build version, commit and build time. |
config |
object | Configuration reload state, including the last failure. |
license |
object | The licence ladder position and both meters. |
ingestion |
object | Per-repository last error, last success and failure count, plus filesSkippedByIngestion — what the INGESTION walk skipped, by reason. It is a SEPARATE number about a different walk and is NOT on searchcode_files_skipped: this walk prunes at an excluded directory and counts one event whose file count it never goes and finds out, where the analysis walk is offered every file underneath and counts each one. Read it as "what ingestion declined to hand on", never as a file count to compare with the metric. |
analysis |
object | Analysis coverage, pool depths and what the deep pass declined to read. files_skipped_by_analysis is what searchcode_files_skipped publishes — the metric is this one producer rather than a sum — and it is a LEVEL, being what the last whole-tree pass over each repository skipped, so an incremental refresh does not restate it. A repository ingestion has walked and the deep pass has not reached yet has NO series at all rather than a number in another walk's units. |
audit |
object | The audit stream's health. dropped is the field that matters. |
observability |
object | Disk capacity as last sampled, and whether this process makes any outbound connection. |
index |
object | The SEARCH index: which backend answers, per-repository coverage and warming progress. below_minimum and not_applicable are permanent, correct states rather than shortfalls. Note that /api/v1/admin's index block is a different thing — the analysis corpus — and that one is search_index there. |
authz |
object | The compiled policy's SHAPE, never its rules: compilations is what answers "is the policy I edited the policy that is running". |
auth |
object | Which authentication mode is active, and whether this instance has a login at all. |
credentials |
object | What this instance issues to agents, and whether the credential store is readable. A degraded store refuses every agent call. |
advisories |
object | The offline advisory data's age, origin and staleness, and why a bundle was refused if one was. |
Example: Is this instance healthy?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/status"
{"version": {"version": "1.0.0", "commit": "cafed00d"}, "config": {"reloads": 0}, "license": {"state": "community"}, "ingestion": {"repositories": []}, "analysis": {"coverage": {"repositories": 3, "analysed": 3}}, "audit": {"dropped": 0}, "observability": {"disk": {"used_percent": 41}}}
GET /api/v1/me
Who am I, and what do I reach?
Not an MCP tool: the agent-facing equivalent is /api/credentials/self, which answers for the credential rather than the person; this adds the instance-scoped roles the account page renders and would be one more operational tool in every model's context Authentication: API key or OAuth access token
The caller's own identity, groups, instance-scoped roles and the repository groups they reach. It answers about the CALLER and about nobody else: showing you your own grants discloses nothing, because listing repositories already tells you the same thing, while a form that explained why access to an arbitrary repository was denied would confirm that repository exists.
Available to every authenticated principal, including one with no repository access at all — a person who cannot find a repository can check their own groups here before asking anybody.
Response fields:
| Field | Type | Description |
|---|---|---|
subject |
string | The principal's stable identifier. |
username |
string | Display name, empty where the source supplies none. |
email |
string | Email address, empty where the source supplies none. |
type |
string | user, agent, service or anonymous. An agent is an MCP client acting for a person, which is what every API key and every OAuth token produces, so it is the value most callers of THIS endpoint see. |
source |
string | How this principal was established, and the one field that says WHICH of the credential mechanisms is in force. Five values, and which ones you can see depends on the listener: apikey and oauth on the API and MCP listener, proxy, local and open on the UI listener. It used to be described as "proxy, local or open", which named three of the five and excluded both values an agent can ever see on the endpoint an agent calls. /api/credentials/self answers the same question with the key id and client name beside it. |
anonymous |
boolean | True for the one anonymous principal auth.mode: open establishes. |
groups |
array of string | The identity groups this principal carries. In proxy mode they arrive on each request. |
roles |
object | The instance-scoped permissions this principal holds, one boolean per permission: administer, view_audit_log and view_analytics. They come from authz.adminGroups, authz.auditorGroups, authz.adminUsers and authz.auditorUsers and can never be reached from a binding. |
repository_groups |
array of string | The repository groups this principal reaches, derived from the repositories they can already see rather than from the policy — a directory of every declared group would name groups whose repositories are invisible to them. |
repositories |
integer | How many repositories this principal can see. |
Example: What am I allowed to see?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/me"
{"subject": "a.person", "username": "a.person", "email": "a.person@example.invalid", "type": "user", "source": "proxy", "anonymous": false, "groups": ["example-engineers"], "roles": {"administer": false, "view_analytics": false, "view_audit_log": false}, "repository_groups": ["example-group"], "repositories": 2}
GET /api/v1/admin
The operator's view of this instance
Not an MCP tool: operational rather than about the code, for the same reason /api/v1/status is: an agent asking how full the disk is has nothing useful to do with the answer Authentication: API key or OAuth access token
Licence state, seats, configuration reload state, per-repository index health, corpus size, disk capacity and every issued agent credential — the one screen that answers "is this healthy?" without reading logs.
Administrators and auditors, through the instance-scoped view_analytics permission. An auditor receives the same body with can_administer false: withholding panels from them would make the read-only view a different page, and a different page is one that drifts. The repository rows are filtered to the caller's own AllowedRepos.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
detail |
query | string | no | summary |
How much of this report to build. summary, the default, omits the per-repository arrays (repositories, credentials, analysis.repo_warnings), which are the great majority of the payload on a large estate and are not what a monitoring poll reads; full returns them. Nothing is deleted by the default: the omitted array on every response names exactly what the shape you asked for left out, and each of these blocks publishes a count at both details so an empty estate still says zero. One of: summary | full. |
Response fields:
| Field | Type | Description |
|---|---|---|
detail |
string | Which shape this response is: summary or full. Echoed so a consumer can tell a report that omitted its arrays from one whose estate is empty. |
omitted |
array of string | The field paths this shape does not carry, dotted from the root. Empty at detail=full and never null. It is what separates "not requested" from "there are none", which an absent key alone cannot say: ask again with detail=full to get them. |
version |
object | What this process is: the build, when it started, the analyser version it runs and the grammars it links. Everything searchcode version prints, without a shell on the box. |
version |
string | The version as git describe produced it at build time. |
described |
string | The version as searchcode version prints it: untagged when no tag was reachable and git describe fell back to the commit. |
commit |
string | The commit the binary was built from. |
build_time |
string | When the binary was built. |
started_at |
string | When this process started, RFC 3339 in UTC. |
uptime_seconds |
integer | Seconds since started_at when this response was built. |
analyser_version |
integer | The analyser version this binary compiled. A repository whose stored analysis is older is re-analysed; analysis.coverage is how far the estate has got. |
grammars |
array | The tree-sitter grammars linked into this binary. Empty means a build without the grammar tags, where every outline is the line-start heuristic. |
config |
object | Configuration reload state: which file, the hash of what was loaded, when it was loaded, how many reloads succeeded, the last failure, and whether the previous configuration is still serving. |
license |
object | The licence ladder position and both meters, as searchcode license show reports them. |
seats |
object | The user meter — the one limit in this product that refuses anybody — with its approach warning and the subjects it has recently turned away. |
index |
object | The ANALYSIS corpus: repositories, how many have been analysed, files, lines and files not read by reason. It is not the search index — that is search_index below, and the two have been confused before. The name is kept because it is a published response field. |
search_index |
object | The SEARCH index: which backend answers, and the estate partitioned by per-repository index state — indexed, building, failed, below_minimum and not_applicable, plus rebuilding as a subset of indexed. The backend is chosen per repository at query time, so a mixed estate is ordinary rather than transitional. below_minimum (under min_files) and not_applicable (the backend is off) are permanent, correct answers rather than shortfalls: brute force answers those repositories correctly and neither number is one to chase. documents and bytes count only the repositories actually serving an index. |
disk |
object | Capacity as last sampled, the warn threshold, and whether ingestion is paused and why. |
analysis |
object | Analysis coverage, pool depths and what the deep pass declined to read. |
audit |
object | The audit stream's health. dropped is the field that matters. |
repositories |
array | One row per repository the caller can see, with its state, lag, last error and size. |
id |
string | Stable identifier. |
name |
string | Human-facing name — the normalised clone URL. |
phase |
string | ready, cloning, pending, no_content, empty, failed or skipped. |
source |
string | git or directory. |
last_error |
string | The last clone or fetch failure, empty when there was none. |
consecutive_failures |
integer | How many attempts have failed in a row. |
last_success |
string | When it last ingested successfully, RFC 3339. |
lag_seconds |
number | Seconds since the last SUCCESSFUL ingest, not since the last attempt. |
files_indexed |
integer | How many files the corpus walk kept. A FILE count. |
files_skipped |
object | Files this repository's INGESTION walk did not index, by reason. The analysis pass keeps its own count; see the analysis block. |
size_bytes |
integer | Size on disk. |
files_skipped |
object | Skip EVENTS by reason, and NOT a file count — a skipped directory is one event whatever it holds, because its file count is unknown without walking the thing that was skipped. It therefore does not subtract from files_indexed: a repository of 38 files can report a ledger of 12 with nothing dropped silently. |
skip_reason |
string | Why a skipped repository was not ingested. |
search_index_state |
string | This repository's state in the search index: indexed, building, failed, below_minimum or not_applicable. Empty means the index registry has not been told about it yet, which is not the same as unindexed. |
search_index_reason |
string | Why it is in that state — below the size threshold, or which way a build failed. Empty for a repository that is simply indexed. |
capabilities |
object | What this source can answer: blame, history, coupling. |
credentials |
array | Every agent credential this instance has issued, with its owner. Dormant keys are the ones to prune and this is what makes pruning happen. |
id |
string | The credential's identifier, which is what a revoke names. |
client_name |
string | The client it was created for. |
subject |
string | The owning principal's identifier. |
owner |
string | The owner's display name, from the subject record. |
email |
string | The owner's email, from the subject record. |
groups |
array of string | The groups frozen onto the key at issue. |
issued_at |
string | When it was created, RFC 3339. |
expires_at |
string | When it expires, empty for a key with no expiry. |
last_used |
string | When it last authenticated, empty if never. |
revoked_at |
string | When it was revoked, empty while it is live. |
can_administer |
boolean | Whether the caller may take an administrative action. An auditor is read-only, and the renderer omits every action control rather than rendering one it would reject. |
Example: Is this instance healthy?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/admin"
{"version": {"version": "1.0.0"}, "config": {"reloads": 2, "failures": 1, "last_error": "unknown key authz.exceptGroups", "previous_config_still_serving": true}, "license": {"state": "valid"}, "seats": {"active": 4, "limit": 25, "recently_refused": []}, "index": {"repositories": 3, "analysed": 3, "files": 412}, "search_index": {"backend": "caisson", "enabled": true, "repositories": 3, "indexed": 1, "building": 1, "failed": 0, "below_minimum": 1, "not_applicable": 0, "min_files": 1000}, "disk": {"used_percent": 41, "warn": false}, "repositories": [{"id": "9f2c", "name": "example/billing-service", "phase": "ready", "lag_seconds": 91, "consecutive_failures": 0, "search_index_state": "indexed"}], "credentials": [{"id": "k_7f1", "client_name": "Claude Code", "subject": "a.person", "owner": "A Person"}], "can_administer": true}
GET /api/v1/usage
How this instance is being used
Not an MCP tool: the operator's own view of adoption; an agent has nothing to do with it Authentication: API key or OAuth access token
Calls by surface and tool, denials, the queries that returned nothing, and agent-instruction adoption WITH the list of repositories missing the reference.
Every repository-attributed aggregate, including the totals, is summed over the caller's own AllowedRepos. A call that touched no repository — a zero-result search, a refusal, an estate rollup — is reported in a separate unattributed block rather than folded into a total whose scope the reader would then be wrong about.
There is no per-user breakdown and there is no time series. The first is a legal position: works-council rules in Germany and France prohibit developer monitoring, and a product that ships one by default is one those customers cannot deploy. The second belongs to Prometheus — searchcode_searches_total and searchcode_mcp_calls_total are on /metrics precisely so the history survives a restart.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
detail |
query | string | no | summary |
How much of this report to build. summary, the default, omits the per-repository arrays (repositories, agent_instruction_coverage.missing), which are the great majority of the payload on a large estate and are not what a monitoring poll reads; full returns them. Nothing is deleted by the default: the omitted array on every response names exactly what the shape you asked for left out, and each of these blocks publishes a count at both details so an empty estate still says zero. One of: summary | full. |
Response fields:
| Field | Type | Description |
|---|---|---|
detail |
string | Which shape this response is: summary or full. Echoed so a consumer can tell a report that omitted its arrays from one whose estate is empty. |
omitted |
array of string | The field paths this shape does not carry, dotted from the root. Empty at detail=full and never null. It is what separates "not requested" from "there are none", which an absent key alone cannot say: ask again with detail=full to get them. |
since |
string | When this process started counting, RFC 3339. Nothing here is persisted. |
totals |
object | Calls, results and denials summed over the caller's repositories ONLY. |
repositories |
array | Per-repository usage, busiest first. |
repository |
string | The repository identifier. |
name |
string | Its human-facing name. |
calls |
integer | How many calls read it. |
results |
integer | How many results those calls returned. |
by_tool |
object | The same count broken down by tool. |
unattributed |
object | Calls that named no repository at all. Labelled rather than folded into totals. |
calls_by_surface |
object | Instance-wide call counts by surface: api, mcp or ui. Not repository-attributed, and named so. |
calls_by_tool |
object | Instance-wide call counts by tool. |
denials_by_surface |
object | Authorisation refusals by surface. |
invalid_calls_by_tool |
object | Calls refused as malformed — an unknown or unparseable parameter — by tool. The direct measure of a parameter the tool descriptions failed to teach. |
tools_never_called |
array of string | Every catalogue tool with no call since since, stated rather than left to be noticed missing from calls_by_tool. |
first_tool_of_session |
object | The first tool called in each MCP session, counted by tool and summed across sessions. REST calls have no session and are not in it. |
tool_transitions |
array | Which tool followed which within an MCP session, most frequent first, summed across sessions. search followed by search is the over-searching the search tool's description warns against; search followed by read_file is the ladder working. |
from |
string | The earlier call's tool. |
to |
string | The next call's tool, in the same session. |
count |
integer | How often. |
tool_transitions_total |
integer | How many distinct transitions exist; tool_transitions carries the top 25. |
sessions_untracked |
integer | MCP sessions the tracking bound turned away. Non-zero means first_tool_of_session and tool_transitions are partial. |
zero_result_queries |
array | Queries that matched nothing, most frequent first. Every one is either a coverage gap or a ranking gap and both are fixable. |
query |
string | The query text. |
count |
integer | How often it returned nothing. |
zero_result_total |
integer | Every miss, including ones whose text was not retained. |
queries_retained |
boolean | audit.logSearchQueries. False is why the table above may be empty while the total is not. |
agent_instruction_coverage |
object | The proportion of the caller's repositories whose agent-instruction file names searchcode, WITH the ones that do not. A ratio with no list tells an operator they have a problem and not what to do about it. |
repositories |
integer | How many were sampled. |
covered |
integer | How many reference searchcode. |
ratio |
number | covered divided by repositories; zero on an empty estate. |
missing |
array of string | The repositories to edit. |
per_user_breakdown |
boolean | Whether this instance reports work per PERSON. False unless the operator has enabled author attribution (analytics.attributeAuthors); see that setting. It is a LIVE READ of the switch rather than a constant, so what you read here is a fact about this instance today and not a promise about the product. False on every default install. |
Example: What are people failing to find?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/usage"
{"since": "2026-01-01T00:00:00Z", "totals": {"calls": 41, "results": 388, "denied": 0}, "repositories": [{"repository": "9f2c", "name": "example/billing-service", "calls": 30, "results": 300, "by_tool": {"search": 28, "read_file": 2}}], "unattributed": {"calls": 6, "results": 0, "denied": 2}, "calls_by_surface": {"api": 12, "mcp": 25, "ui": 10}, "calls_by_tool": {"search": 38}, "denials_by_surface": {"api": 2}, "zero_result_queries": [{"query": "circuitbreaker", "count": 4}], "zero_result_total": 6, "queries_retained": true, "agent_instruction_coverage": {"repositories": 3, "covered": 1, "ratio": 0.3333, "missing": ["example/billing-service", "example/docs-drop"]}, "per_user_breakdown": false}
GET /api/v1/openapi.json
The OpenAPI document for this API
Not an MCP tool: the specification of the surface rather than a call against it Authentication: None — an integrator evaluating the product has no credential yet, and a reference they cannot read is not a reference; it is generated from route definitions and discloses nothing about the estate
Generated from the same definitions the handlers and the MCP tool catalogue are generated from, so the three cannot disagree. Import it into a client generator, Postman, Insomnia or Bruno.
Unauthenticated, because the reader is deciding whether to integrate and does not have a credential yet. It is generated from route and schema definitions rather than from the estate, so it names no repository, no user and no finding.
Response fields:
| Field | Type | Description |
|---|---|---|
openapi |
string | The OpenAPI version this document conforms to. |
info |
object | Title, version and description of the API. |
paths |
object | One entry per endpoint. |
components |
object | The shared error schema and the gaps schema. |
Example: Fetch the spec without a credential
curl -sS \
"$SEARCHCODE_URL/api/v1/openapi.json"
{"openapi": "3.0.3", "info": {"title": "searchcode", "version": "1"}, "paths": {"/api/v1/search": {"post": {"operationId": "search"}}}}
GET /api/v1/docs
The rendered API reference
Not an MCP tool: a page rather than a call Authentication: None — the reader is deciding whether to integrate and has no credential yet; it is rendered from route definitions and discloses nothing about the estate
The human-readable reference, server-rendered from the same in-memory specification openapi.json is generated from. No CDN, no viewer bundle and nothing fetched at page load: a reference that needs the internet is a blank page in an air-gapped install. It works with JavaScript disabled, because every endpoint, parameter and example is in the served HTML rather than assembled in the browser.
Response fields:
| Field | Type | Description |
|---|---|---|
body |
string | The rendered HTML reference. |
Example: Read the reference in a browser
curl -sS \
"$SEARCHCODE_URL/api/v1/docs"
<h1>searchcode API</h1> … one section per endpoint, every parameter described, every example worked.
Files
GET /api/v1/repos/{repo}/file
Read a file, or a range of lines from one
MCP tool: read_file. Authentication: API key or OAuth access token
Returns the file's lines as JSON, with the total length so a caller reading a range still knows how much it did not read. Use start and end to read around a search match rather than pulling a whole file into a context window.
Pass ref to read the file as it stood at one commit. Omitting it reads the checked-out revision, whose SHA every response carries — so a result can be re-read exactly later rather than approximately.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
path | string | yes | Repository identifier, clone URL, owner/name, or a unique name suffix. |
||
path |
query | string | yes | File path relative to the repository root. | ||
start |
query | integer | no | First line to return, 1-based and inclusive. Omit for the start of the file. | ||
end |
query | integer | no | Last line to return, 1-based and inclusive. Omit for the end of the file. | ||
ref |
query | string | no | Read the file at this commit rather than at the checked-out revision. A 7-to-40 character hexadecimal SHA; a branch or tag name is refused, because a link that pins a branch drifts to different lines as the branch moves. A source with no commits, or a revision a shallow clone never fetched, reports a gap and returns the current content rather than failing. |
Response fields:
| Field | Type | Description |
|---|---|---|
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
repository_name |
string | The repository's own name — its normalised clone URL, or its path for a directory source. This is what to show a person; repository is the identifier to pass back. |
path |
string | The path that was read. |
language |
string | The language scc detected. |
lines |
array of string | The returned lines, without their terminators. |
start |
integer | 1-based line number of the first returned line. |
total_lines |
integer | The file's full length. |
truncated |
boolean | The file was longer than the reader would return. |
pinned |
boolean | The ref that was asked for is a past revision, not the checked-out one, so the content is not what the repository holds now. A caller cannot infer this from commit_sha, which reports the ref that was read either way. It says nothing about where the lines came from: a read at the checked-out revision is false and is as exact as any other. Read immutable for that. |
immutable |
boolean | The lines were read out of the commit object commit_sha names, so they are exactly that commit's content and cannot change. This is the field to cite a range by. False for a source with no commits, for a symbolic link, and for a read that could not reach the commit object, which says why in gaps. |
notices |
array of string | Sentences to read before the lines. Empty for most files; carries the risk advisory when this file is in its repository's top 10% for complexity, churn, distinct authors or production findings, or when one of those could not be measured. It states care, never consequence. |
risk |
object | Present only beside a risk advisory: the factors behind it, whole-file figures from the table built after the last index. Absent means nothing here is elevated or unknown, not that nothing was measured. |
available |
boolean | False when no factors exist for this file yet; every other member is then absent. |
commit_sha |
string | The commit the analysis described, or "unavailable" for a source with no commits. |
scope |
string | Always file: every figure is about the whole file, never a symbol in it. |
complexity |
integer | The file's cyclomatic complexity, from scc. |
churn |
integer | Commits touching the file inside the window. Null when UNKNOWN, never zero. |
authors |
integer | DISTINCT authors of those commits — a count, never a name. Null when UNKNOWN, and null beside authors_band when the count is below the anonymity floor. |
authors_band |
string | few when the distinct count is below the instance's anonymity floor; absent otherwise. |
findings |
integer | Findings in production code. Null when UNKNOWN, never zero. |
test_findings |
integer | Findings in test files, reported beside and never counted in findings. |
percentiles |
object | Each KNOWN factor's percentile within its repository, 0 to 100: the share of files with a strictly lower value. An unknown factor has no entry. |
score |
number | The mean of the known percentiles. The order, not a measurement. |
elevated |
array of string | Factors this file is in its repository's top 10% for. |
unknown |
array of string | Factors that could not be measured. Never read an unknown factor as low. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: Read around a match
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/repos/deploy-tool/file?path=retry.go&start=10&end=14"
{"repository": "example__deploy-tool", "commit_sha": "4f0c...", "repository_name": "https://example.invalid/deploy-tool", "path": "retry.go", "language": "Go", "lines": ["// RetryWithBackoff retries with exponential backoff.", "func RetryWithBackoff(ctx context.Context, attempts int) error {"], "start": 10, "total_lines": 48, "truncated": false, "pinned": false, "immutable": true, "notices": [], "gaps": []}
POST /api/v1/repos/{repo}/files
Read several files from one repository in one call
MCP tool: read_files. Authentication: API key or OAuth access token
The batch form of read_file, bounded. One round trip instead of ten is the difference between an agent that reads what it needs and one that reads the first file and guesses.
Each entry reports its own error rather than failing the batch: a path that does not exist leaves the other files readable.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
path | string | yes | Repository identifier, clone URL, owner/name, or a unique name suffix. |
Request body:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
paths |
body | array of string | yes | The file paths to read, relative to the repository root. Bounded at 50 per call. | ||
start |
body | integer | no | First line to return from each file, 1-based and inclusive. | ||
end |
body | integer | no | Last line to return from each file, 1-based and inclusive. |
Response fields:
| Field | Type | Description |
|---|---|---|
files |
array | One entry per requested path, in the order asked. |
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
path |
string | The requested path. |
language |
string | The language scc detected. |
lines |
array of string | The returned lines. |
start |
integer | 1-based line number of the first returned line. |
total_lines |
integer | The file's full length. |
truncated |
boolean | The file was longer than the reader would return. |
pinned |
boolean | Always false here: this endpoint reads the checked-out revision. Present because each entry has the file endpoint's shape. |
immutable |
boolean | The lines were read out of the commit object commit_sha names, so they are exactly that commit's content. False for a source with no commits, a symbolic link, or a read that could not reach the commit object. |
error |
string | Why this one path could not be read. Present only on failure, and it does not fail the other entries. |
error_code |
string | The semantic class of this entry's failure, from the same set as a top-level error_code. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: Read the two files a search pointed at
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
-X POST \
-H 'Content-Type: application/json' \
-d '{"paths": ["retry.go", "platform.go"]}' \
"$SEARCHCODE_URL/api/v1/repos/deploy-tool/files"
{"files": [{"repository": "example__deploy-tool", "commit_sha": "4f0c...", "path": "retry.go", "language": "Go", "lines": ["package platform"], "start": 1, "total_lines": 48, "truncated": false}, {"repository": "example__deploy-tool", "commit_sha": "4f0c...", "path": "platform.go", "language": "Go", "lines": ["package platform"], "start": 1, "total_lines": 30, "truncated": false}], "gaps": []}
GET /api/v1/repos/{repo}/raw
Fetch a file's bytes unrendered
Not an MCP tool: a browser affordance rather than an agent one; an agent wants read_file's structured lines, and a tool returning arbitrary undecoded bytes into a context window is a hazard rather than a feature Authentication: API key or OAuth access token
The browser affordance behind "view raw". It returns the file's bytes with the DECLARED content type text/plain and never the detected one.
Every repository in the estate is untrusted input by definition — the product ingests whatever the estate contains — so any HTML or SVG among it is stored XSS against every user of the instance if it is served as its apparent type. The response therefore always carries text/plain, X-Content-Type-Options: nosniff, Content-Disposition: attachment and a Content-Security-Policy of default-src 'none'; sandbox. There is no exception list, because the exception list is the vulnerability.
Prefer read_file for anything programmatic: it returns structured lines and the file's length.
A ref this endpoint cannot honour is REFUSED rather than answered with the checked-out revision's bytes. Every other endpoint reports a pin it could not resolve in gaps beside the content it could still serve; a response that IS bytes has nowhere to put one, and a download that is silently a different revision from the one the URL names is the failure this parameter exists to remove.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
path | string | yes | Repository identifier, clone URL, owner/name, or a unique name suffix. |
||
path |
query | string | yes | File path relative to the repository root. | ||
ref |
query | string | no | Download the bytes as they stood at this commit rather than at the checked-out revision. A 7-to-40 character hexadecimal SHA; a branch or tag name is refused, for the reason read_file gives. Unlike read_file, a revision that cannot be read is a 404 rather than the current content with a gap: the response is bytes and has nowhere to say which revision they came from, so serving the wrong ones would be indistinguishable from serving the right ones. |
Response fields:
| Field | Type | Description |
|---|---|---|
body |
string | The file's bytes, served as text/plain with nosniff and an attachment disposition regardless of the file's extension or content. |
Example: Download a file unrendered
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/repos/untrusted-content/raw?path=payload.html"
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Content-Disposition: attachment; filename="payload.html"
Content-Security-Policy: default-src 'none'; sandbox
<script>alert(1)</script>
GET /api/v1/repos/{repo}/tree
List a directory inside a repository
MCP tool: file_tree. Authentication: API key or OAuth access token
One level by default, or the whole subtree with recursive=true. Cheaper than reading files and the right way to find out what a repository contains before deciding what to read.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
path | string | yes | Repository identifier, clone URL, owner/name, or a unique name suffix. |
||
path |
query | string | no | Directory to list, relative to the repository root. Omit for the root. | ||
recursive |
query | boolean | no | false |
Descend the whole subtree rather than listing one level. | |
max_entries |
query | integer | no | 10000 |
Cap on returned entries. Truncated says whether the cap was reached. |
Response fields:
| Field | Type | Description |
|---|---|---|
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
repository_name |
string | The repository's own name — its normalised clone URL, or its path for a directory source. This is what to show a person; repository is the identifier to pass back. |
path |
string | The directory that was listed. |
entries |
array | The listing, sorted by path. |
path |
string | Path relative to the REPOSITORY root, not to the listed directory. |
dir |
boolean | True for a directory. |
size |
integer | Size in bytes, zero for a directory. |
truncated |
boolean | The listing hit max_entries. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: What is in this repository?
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/repos/billing-service/tree"
{"repository": "example__billing-service", "commit_sha": "9b1c...", "repository_name": "https://example.invalid/billing-service", "path": "", "entries": [{"path": "billing.go", "dir": false, "size": 1820}, {"path": "go.mod", "dir": false, "size": 64}], "truncated": false, "gaps": []}
GET /api/v1/repos/{repo}/symbols
The declaration outline of one file, or of a whole repository
MCP tool: symbols. Authentication: API key or OAuth access token
The functions, types and other declarations a file defines, with their lines. OMIT path AND YOU GET THE WHOLE REPOSITORY'S declarations instead, in path then line order — one call rather than one per file.
The two scopes answer from different places and the difference is visible in the gaps. One file is opened, so a language no grammar in this build covers is still answered by the line-start heuristic. A repository is not read file by file: the repository-wide outline is the parsed table the analysis pass left behind, so a file the grammars do not cover contributes nothing and is COUNTED IN A GAP rather than quietly omitted, and a repository whose deep pass has not run yields a gap and no declarations rather than an empty outline.
Every symbol carries a confidence, and it means exactly one thing. "exact" is a declaration a tree-sitter grammar resolved during the analysis pass, and it carries a real kind — method, class, interface, constructor. "probable" is a line-start heuristic, and its kind is the undifferentiated "declaration". There is no third value.
Which languages parse is a property of this build rather than of the request: ten grammars ship, and a file in any other language is answered by the heuristic with a gap saying what it could not resolve. Nothing is parsed while you wait — the parse happens once, at ingest, so a repository whose analysis pass has not finished is answered by the heuristic too.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
path | string | yes | Repository identifier, clone URL, owner/name, or a unique name suffix. |
||
path |
query | string | no | File path relative to the repository root. OMIT IT for every declaration in the repository, in path then line order. | ||
offset |
query | integer | no | 0 |
How many declarations to skip before listing. With max_results it is a page: offset 0 is the first page, offset 50 with max_results 50 is the second. max_results is a hard maximum rather than a default, so this is the only way to reach what lies past it. It is an offset and not a cursor: each request is a fresh read, so a row added or removed between two pages may be seen twice or missed once. An offset past the end is an empty list and not an error, because total is the only way to know where the end was and it may move. It does not change total, which counts the whole set before either parameter is applied. | |
max_results |
query | integer | no | 2000 |
5000 | Cap on returned declarations. The default is generous because one file's outline is a complete answer at any sane length and a repository's is not; total says how many there were before it bit. It is a HARD MAXIMUM of 5000 rather than a default a caller raises — five times every other endpoint's, for the same reason the default is — and offset is what reaches a repository declaring more. |
Response fields:
| Field | Type | Description |
|---|---|---|
offset |
integer | The offset this page was read at, echoed so a paging caller can check what it got rather than what it asked for. |
symbols |
array | The declarations, in line order. |
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
name |
string | The declared name. |
kind |
string | What was declared. A parsed symbol carries a real kind — function, method, constructor, class, interface, struct, enum, trait, type, module, property, macro, impl, constant or variable. A heuristic guess carries "declaration", which is the one kind it can tell you and is why the confidence beside it is probable. |
path |
string | The file the declaration is in. |
line |
integer | 1-based line of the declaration's NAME. |
end_line |
integer | 1-based last line of the declaration. PRESENT ONLY WHEN IT WAS MEASURED, which means a grammar read the file — so a probable symbol never carries one. It is omitted rather than sent as zero: a zero reads as a measurement, and a caller ranking by length would sort every unmeasured declaration to the top of the shortest list. |
lines |
integer | How long the declaration is, from its name to its end. Omitted alongside end_line for the same reason. It is one short of the source text for a form that carries something ABOVE its name, such as a decorated Python method, because line is where the name is. |
language |
string | The language scc detected. |
confidence |
string | exact when a parser resolved it, probable when a heuristic did. There is no third value: "unknown" would be a way of avoiding the decision. |
total |
integer | Declarations found before max_results truncation. |
truncated |
boolean | The list was cut at max_results, so total is larger than what is here. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: Outline before reading
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/repos/billing-service/symbols?path=billing.go"
{"symbols": [{"repository": "example__billing-service", "commit_sha": "9b1c...", "name": "Charge", "kind": "function", "path": "billing.go", "line": 14, "language": "Go", "confidence": "probable"}], "total": 1, "truncated": false, "gaps": []}
History
GET /api/v1/repos/{repo}/blame
Per-line authorship for one file
MCP tool: blame. Authentication: API key or OAuth access token
Who last touched each line, and when.
Use start and end to blame a range rather than a whole file — the question is nearly always "who wrote this bit", and they are the same two parameters read_file takes, with the same 1-based inclusive meaning. The line numbers on the rows are the file's own either way, so a range narrows the answer without renumbering it.
A directory source has no commits, so blame over one returns HTTP 200 with an empty lines array and a POPULATED gaps array — never a 500 and never a bare empty result. "No blame data available: this source has no history" is a different sentence from "nobody has touched this file".
Pass ref to read authorship as it stood at one commit. Omitting it blames the checked-out revision. A ref this clone does not have, or one the file did not exist at, reports a gap and no lines rather than the current revision's authorship — which would attribute these lines to the wrong commits and the wrong people.
There is no search over history: no pickaxe, no date range. blame says when a line LAST changed, not when it first appeared. A line introduced in one commit and reformatted in a later one blames to the later one, with nothing on this response saying so, so this is not a date-of-first-appearance and commit_log cannot make it into one. Ask your code host for that.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
path | string | yes | Repository identifier, clone URL, owner/name, or a unique name suffix. |
||
path |
query | string | yes | File path relative to the repository root. | ||
start |
query | integer | no | First line to return, 1-based and inclusive. Omit for the start of the file. | ||
end |
query | integer | no | Last line to return, 1-based and inclusive. Omit for the end of the file. | ||
ref |
query | string | no | Compute authorship at this commit rather than at the checked-out revision. A 7-to-40 character hexadecimal SHA; a branch or tag name is refused, for the reason read_file gives. A commit this clone does not have reports a gap and no lines. |
Response fields:
| Field | Type | Description |
|---|---|---|
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
path |
string | The file that was blamed. |
lines |
array | One entry per line, in order. |
line |
integer | 1-based line number. |
commit_sha |
string | The commit that last touched the line. |
author |
string | The author of that commit. |
when |
string | RFC 3339 timestamp of that commit. |
pinned |
boolean | Authorship was computed at the ref that was asked for rather than at the checked-out revision. A caller cannot infer this from commit_sha, which reports the revision that was blamed either way. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: Blame over a directory source reports a gap, not an error
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/repos/docs-drop/blame?path=README.md"
{"repository": "example__docs-drop", "commit_sha": "unavailable", "path": "README.md", "lines": [], "pinned": false, "gaps": [{"capability": "history", "reason": "source type 'directory' has no history", "sources": ["example__docs-drop"], "source_count": 1, "kind": "unanswered"}]}
GET /api/v1/repos/{repo}/log
Commits touching a file, newest first
MCP tool: commit_log. Authentication: API key or OAuth access token
The change history of one path, newest first.
REQUIRES HISTORY. A shallow clone has one commit and says so through gaps; a directory source has none and says so the same way. Neither returns a short history and leaves the caller to read it as a quiet file — "this clone holds one commit" and "this path has changed once" are different statements, and only the second is about the code.
It lists the commits that TOUCHED the path, never which one introduced a given line. There is no search over history: no pickaxe, no date range. blame says when a line LAST changed, not when it first appeared. A line introduced in one commit and reformatted in a later one blames to the later one, so the two endpoints do not compose into a date-of-first-appearance. Ask your code host for that.
Parameters:
| Name | In | Type | Required | Default | Maximum | Description |
|---|---|---|---|---|---|---|
repo |
path | string | yes | Repository identifier, clone URL, owner/name, or a unique name suffix. |
||
path |
query | string | no | File path relative to the repository root. Omit for the repository's own history. | ||
limit |
query | integer | no | 50 |
1000 | How many commits to return. |
offset |
query | integer | no | 0 |
How many commits to skip before listing. With max_results it is a page: offset 0 is the first page, offset 50 with max_results 50 is the second. max_results is a hard maximum rather than a default, so this is the only way to reach what lies past it. It is an offset and not a cursor: each request is a fresh read, so a row added or removed between two pages may be seen twice or missed once. An offset past the end is an empty list and not an error, because total is the only way to know where the end was and it may move. It does not change total, which counts the whole set before either parameter is applied. |
Response fields:
| Field | Type | Description |
|---|---|---|
repository_name |
string | The repository's own name — its normalised clone URL, or its path for a directory source. This is what to show a person; repository is the identifier to pass back. |
commits |
array | The commits, newest first. |
repository |
string | The repository identifier this result came from. |
commit_sha |
string | The commit the result was read at, or "unavailable" for a directory source, which has no commits. Never an empty string: a missing SHA is a defect, an unavailable one is a property of the source. |
author |
string | The commit's author. |
message |
string | The commit subject line. |
when |
string | RFC 3339 timestamp. |
total |
integer | Commits touching this path before the limit was applied, so a page of commits states what it is a page OF. It is NOT the length of commits — compare the two to see how much the limit left out. Absent, rather than zero, in the rare case the count could not be established; a history nobody could count is not a history of no changes. |
truncated |
boolean | More commits touch this path than limit returned. False means commits holds the whole history of this path, which is the only state in which "last changed" or "how often does this churn" can be answered from it. |
offset |
integer | The offset this page was read at, echoed so a paging caller can check what it got rather than what it asked for. |
gaps |
array | Capabilities that could not be answered for part of this request. An empty results array with an empty gaps array means nothing matched; an empty results array with a populated gaps array means the question could not be answered. Always check it. |
capability |
string | What could not be answered: history, blame, dependencies, findings, symbols. |
reason |
string | Why, in prose an operator can act on — e.g. "source type 'directory' has no history". |
sources |
array of string | What the gap is true of: repository identifiers, ascending. One gap stands for every repository the same capability failed for the same reason, so this names them — capped at the first few, because an estate of two thousand directory sources would otherwise put two thousand identifiers in every response. Read source_count for how many there actually are; it is exact even when this list is cut. |
source_count |
integer | How many sources this gap is true of, EXACT and never capped. Larger than the length of sources means the list was cut and this is the real total — never derive the scale by counting sources. |
kind |
string | "unanswered" when the answer is incomplete and something could make it complete — a shallow clone, an analysis pass that has not finished. "inapplicable" when the answer is complete and the question does not apply to this input, such as declarations in a Markdown file. Both are reported; neither is an empty result. |
Example: Recent changes to a file
curl -sS \
-H "Authorization: Bearer $SEARCHCODE_TOKEN" \
"$SEARCHCODE_URL/api/v1/repos/deploy-tool/log?path=orders.go&limit=2"
{"repository_name": "https://example.invalid/deploy-tool", "commits": [{"repository": "example__deploy-tool", "commit_sha": "b2f1...", "author": "Fixture Author", "message": "add currency", "when": "2021-02-01T00:00:00Z"}], "total": 9, "truncated": true, "gaps": []}
MCP tools
These 27 MCP tools are what tools/list returns, to every principal. Each one calls the endpoint beside it, with the same authorisation, limits and audit record.
| Tool | Endpoint |
|---|---|
analyze |
GET /api/v1/repos/{repo}/analyze |
blame |
GET /api/v1/repos/{repo}/blame |
change_coupling |
GET /api/v1/repos/{repo}/coupling |
commit_log |
GET /api/v1/repos/{repo}/log |
complexity_trend |
GET /api/v1/repos/{repo}/complexity-trend |
definitions |
GET /api/v1/repos/{repo}/definitions |
estate_definitions |
GET /api/v1/definitions |
estate_references |
GET /api/v1/references |
estate_summary |
GET /api/v1/estate |
explain_access |
GET /api/v1/explain-access |
file_complexity |
GET /api/v1/repos/{repo}/complexity |
file_dependencies |
GET /api/v1/repos/{repo}/dependencies |
file_technologies |
GET /api/v1/repos/{repo}/technologies |
file_tree |
GET /api/v1/repos/{repo}/tree |
find_usages |
POST /api/v1/find-usages |
find_vulnerable_usages |
POST /api/v1/vulnerabilities |
findings |
GET /api/v1/findings |
hotspot_trend |
GET /api/v1/hotspot-trend |
hotspots |
GET /api/v1/repos/{repo}/hotspots |
licence_audit |
GET /api/v1/dependency-licences |
list_repositories |
GET /api/v1/repos |
read_file |
GET /api/v1/repos/{repo}/file |
read_files |
POST /api/v1/repos/{repo}/files |
references |
GET /api/v1/repos/{repo}/references |
risk_ranking |
GET /api/v1/risk |
search |
POST /api/v1/search |
symbols |
GET /api/v1/repos/{repo}/symbols |
Errors
Every error carries error and error_code, and the response carries an X-Request-Id header. Quote it in a support conversation: the log line for the same request has the detail that is not in the body.
| Status | error_code | Meaning |
|---|---|---|
| 400 | invalid_params |
A parameter was missing, malformed or not recognised. Unknown parameters are REFUSED rather than ignored: silently accepting one is how a caller comes to believe it is tuning something. |
| 401 | unauthenticated |
No credential was presented, or the credential did not resolve to a principal. Present an API key or an OAuth access token; this listener does not accept session cookies. |
| 403 | forbidden |
The principal can see the repository and does not hold the permission this call needs. A repository the principal cannot see returns repo_not_found instead, because a 403 would confirm that it exists. |
| 404 | repo_not_found |
No repository of that name is available to this principal. This is also the answer for a repository that exists and is invisible, deliberately. |
| 404 | wrong_origin |
Raw file content is served only from the hostname api.rawContentOrigin names, and this request arrived at a different one. Nothing is wrong with the repository, the file or the credential: the same content is available as structured lines from the file endpoint on any origin. |
| 404 | file_not_found |
The repository is visible and holds no file at that path. On the raw endpoint it is also the answer for a ref whose bytes could not be read: that response is a file and has nowhere to report a gap, so a revision it cannot serve is refused rather than answered with the checked-out one. |
| 429 | rate_limited |
The credential exceeded its request ceiling. Retry-After carries the interval, and retry_after_seconds repeats it in the body. |
| 500 | internal_error |
Something failed inside the instance. The log line for the request carries the detail; quote the X-Request-Id header from the response. |
| 499 | client_closed |
The caller closed the connection before the instance finished answering. Nothing about the request was rejected and nothing failed inside the instance: this is reported so that an abandoned request is not counted as a malformed one. A client that times out on slow searches is the usual cause. |
| 422 | repo_empty |
The repository cloned successfully and has no commits, so it has no tree to read. Nothing about the request was wrong and nothing failed inside the instance; the repository is checked again on its ordinary refresh interval and becomes readable once something is pushed to it. |
| 503 | upstream_unavailable |
The code host refused or dropped this repository's clone — throttling, a server error, a reset connection or a truncated pack — so it has never been ingested. This is the code host declining, not a fault in the repository or the instance, and ingestion retries on its own backoff. An authentication failure is NOT reported this way, because a missing, a private and a throttled repository can answer identically. |