REST API

One HTTPS endpoint per tool, a bearer key, and JSON both ways — including the errors. Everything the MCP server exposes is here, for agents and programs that are not chat clients.

Authentication

Every call needs an API key. Create one under API Keys on your account page — register free first if you have not, and verify your email address, because keys cannot be created until you have. The key starts ctm_live_ and is shown once.

Header
Authorization: Bearer ctm_live_YOUR_KEY

A ?key= query parameter is accepted as well, for clients that cannot set a header. Prefer the header: a query string lands in access logs and proxy logs, and a header does not.

There is no anonymous access. /openapi.json answers without a key because it describes the interface; nothing that returns a company, a score or a filing does.

Endpoints

RouteMethodNotes
/api/v1/<tool_name>GETArguments in the query string.
/api/v1/<tool_name>POSTArguments as a JSON body. Use this for any argument that is a list of objects.
/openapi.jsonGETThe OpenAPI document. Public, no key.
/.well-known/mcp.jsonGETThe MCP server descriptor. Public, no key.

One endpoint per tool, named after the tool. Anything other than GET or POST answers 405 with Allow: GET, POST. Every response is JSON, including every error — never HTML, never a bare string.

! An argument the tool did not declare is rejected rather than ignored. A misspelt name gets a readable bad_request and one retry, instead of a plausible answer to a question you did not ask.

A call that works

Put your key in CTM_KEY and run this. It calls search_companies: find covered companies by name or ticker.

curl
> curl -s 'https://crushthemarket.com/api/v1/search_companies?query=Apple' \  -H "Authorization: Bearer $CTM_KEY"

The tools

11 tools, each one an endpoint. The arguments, the response fields and the failures below are the same ones /openapi.json and the MCP server report, because all three are generated from one registry.

search_companies

Free

Resolves a company name or ticker to the symbols Crush The Market covers. Search reads the profile table, so a company appears here even if it has not been scored yet -- call get_company to find out. ETFs are never returned: nothing feeds us fund holdings, so they cannot be scored.

Arguments

query string, required — A company name or ticker. Matched as a prefix against both.

limit number, optional — How many results to return, 1-25. Defaults to 10.

Returns

query, count, results, error, as_of, source, disclaimer

Name search; the exact ticker sorts first when one matches.
> curl -s https://crushthemarket.com/api/v1/search_companies \  -H "Authorization: Bearer $CTM_KEY" \  -H 'Content-Type: application/json' \  -d '{"query":"Apple"}'

Can answer

bad_request — `query` is empty, or is only punctuation that gets stripped.

Related: get_company, screen_stocks

get_company

Free

Returns the stored profile, the six composite scores (value, growth, financial health, dividend, management, total, each 0-100), the ranked strengths and risks, the one-line verdict per section, and a coverage block naming where the data came from and whether the filing tools will return anything for this symbol. A score of null means too few of its legs could be computed -- it is NOT a bad score.

Arguments

symbol string, required — The ticker as Crush The Market spells it. London listings carry a `.L` suffix.

Returns

symbol, profile, scores, readings, coverage, error, as_of, source, disclaimer

A US filer: EDGAR statements and filings both available.
> curl -s https://crushthemarket.com/api/v1/get_company \  -H "Authorization: Bearer $CTM_KEY" \  -H 'Content-Type: application/json' \  -d '{"symbol":"AAPL"}'

Can answer

bad_request — `symbol` is missing or empty.

unknown_symbol — No profile is held for the symbol at all.

not_covered — The symbol is an ETF. No feed supplies fund holdings, so ETFs are never scored.

no_analysis — The company is covered but has no scored row yet.

Related: search_companies, explain_metric, screen_stocks

explain_metric

Free

Explains a criterion id: its human label, the operators it accepts, the JSON type its value must have, and the written definition from the matching documentation page. Use it before screen_stocks when you are unsure what a criterion measures or how to phrase a filter. Not every criterion has a documentation page; `definition` and `explanation` are null when it does not.

Arguments

criterion_id string, required — A screening criterion id, exactly as screen_stocks accepts it.

Returns

criterion, definition, explanation, doc_url, error, as_of, source, disclaimer

A numeric criterion with More Than / Less Than.
> curl -s https://crushthemarket.com/api/v1/explain_metric \  -H "Authorization: Bearer $CTM_KEY" \  -H 'Content-Type: application/json' \  -d '{"criterion_id":"priceToEarningsRatio"}'

Can answer

bad_request — `criterion_id` is missing or empty.

unknown_criterion — The id is not one of the published screening criteria.

Related: screen_stocks, get_company

screen_stocks

Free

Runs the same screen the website runs. `criteria` is a list of `{ id, operator, value }`, where a numeric operator is `>`, `<`, `>=` or `<=` (the word forms `More Than` / `Less Than` also work); call explain_metric for what an id means and which operators it accepts. Every response carries `universe_size` -- how many symbols in the selected markets had a scored row to filter at all. Read it before reporting an empty result: zero matches out of zero scored symbols is a coverage gap, not a finding about the market.

Arguments

criteria object[], required — Filters, each `{ "id": <criterion id>, "operator": <operator>, "value": <value> }`. A numeric criterion takes `>`, `<`, `>=` or `<=`; the word forms `More Than` and `Less Than` are accepted too, and case and surrounding spaces do not matter. `>=` and `<=` include the boundary value, `>` and `<` exclude it -- ask for the one you mean rather than nudging the number. A list criterion takes `in` or `not in` and a list of strings; a boolean criterion takes `True` or `False` and no `value`. Call explain_metric for the operators a given id accepts.

markets string[], optional — Which universes to screen. One or more of: sp500, ftse100, russell1000, ftseallshare, djia, nasdaq100. Defaults to sp500 and ftse100.

limit number, optional — How many rows to return, 1-100, highest total score first. Defaults to 25. The full match count is always in `count`.

Returns

count, universe_size, markets, results, note, query, url, url_note, error, as_of, source, disclaimer

A single numeric filter over the S&P 500. `<` and `>` are the operators; `Less Than` and `More Than` mean the same thing.
> curl -s https://crushthemarket.com/api/v1/screen_stocks \  -H "Authorization: Bearer $CTM_KEY" \  -H 'Content-Type: application/json' \  -d '{"criteria":[{"id":"priceToEarningsRatio","operator":"<","value":15}],"markets":["sp500"]}'

Can answer

bad_request — `criteria` is empty or malformed, a criterion id is not published, an operator is not one the criterion accepts (`>=` needs a criterion that compares numerically, so a boolean or list criterion rejects it), or two criteria filter the same column.

Related: explain_metric, get_company, backtest_screen

backtest_screen

Pro

Rebuilds the screen once per year of stored history, holds the cohort for `min_hold_years`, and reports the compound annual growth rate of the equal-weighted strategy against the equal-weighted market over the same years. Criteria and markets take the same shape as screen_stocks. `cohort_sizes` is one entry per ENTRY year, and `years` carries one more entry than that -- the trailing year the last cohort is sold in -- so do not zip the two. The figures are a property of the rows we hold, not a forecast, and a small `matched_symbols` means the result describes a handful of names.

Arguments

criteria object[], required — Filters, each `{ "id": <criterion id>, "operator": <operator>, "value": <value> }`. A numeric criterion takes `>`, `<`, `>=` or `<=`; the word forms `More Than` and `Less Than` are accepted too, and case and surrounding spaces do not matter. `>=` and `<=` include the boundary value, `>` and `<` exclude it -- ask for the one you mean rather than nudging the number. A list criterion takes `in` or `not in` and a list of strings; a boolean criterion takes `True` or `False` and no `value`. Call explain_metric for the operators a given id accepts.

markets string[], optional — Which universes to screen. One or more of: sp500, ftse100, russell1000, ftseallshare, djia, nasdaq100. Defaults to sp500 and ftse100.

min_hold_years number, optional — Whole years to hold a name before it can be dropped, 1-10. 1 is a classic yearly rebalance. Defaults to 1.

Returns

strategy_cagr, market_cagr, years, min_hold_years, cohort_sizes, matched_symbols, universe_size, markets, note, query, url, url_note, error, as_of, source, disclaimer

Yearly rebalance of everything scoring above 70. `>`, `<`, `>=` and `<=` are the numeric operators; `More Than` and `Less Than` mean the same as `>` and `<`.
> curl -s https://crushthemarket.com/api/v1/backtest_screen \  -H "Authorization: Bearer $CTM_KEY" \  -H 'Content-Type: application/json' \  -d '{"criteria":[{"id":"totalScore","operator":">","value":70}],"markets":["sp500"]}'

Can answer

requires_pro — The caller is not on an active subscription.

bad_request — `criteria` is empty or malformed, a criterion id is not published, or an operator is not one the criterion accepts (`>=` needs a criterion that compares numerically, so a boolean or list criterion rejects it).

Related: screen_stocks, explain_metric

help

Free

Long-form guidance, in topics. Call it with no argument for the essentials and the topic list, or with `topic` for one of them. Reach for it when you are unsure which tool answers a question, whether the data supports the question at all, or what an error means. It returns no company data and costs nothing but the call.

Arguments

topic string, optional — One of: start, answerable, screening, filings, scores, limits, errors, reporting. Omit for the essentials plus the topic list.

Returns

topic, title, text, topics, as_of, source, disclaimer

How to read an annual report without drowning in it
> curl -s https://crushthemarket.com/api/v1/help \  -H "Authorization: Bearer $CTM_KEY" \  -H 'Content-Type: application/json' \  -d '{"topic":"filings"}'

Can answer

bad_request — the `topic` argument is not a string

Related: explain_metric, get_company

list_filings

Free

Lists a company's filings from the SEC EDGAR submissions index, newest first, each with its accession number -- the id every other filing tool takes. `latest_annual` names the report the other tools default to. An empty list is NEVER bare: `reason` distinguishes "no SEC filer is mapped to this symbol" from "EDGAR gave us something we could not read", and `adr_symbol` names the US-listed ADR to retry against where one exists. Read `reason` before telling anyone a company files nothing.

Arguments

symbol string, required — The ticker as Crush The Market spells it. London listings carry a `.L` suffix.

form string, optional — Return only this form, matched exactly: 10-K, 10-Q, 8-K, 20-F, 40-F.

limit number, optional — How many filings to return, 1-100, newest first. Defaults to 20. The full match count is always in `total`.

Returns

symbol, count, total, filings, reason, reason_note, adr_symbol, latest_annual, note, error, as_of, source, disclaimer

Every 10-K Apple has filed, newest first.
> curl -s https://crushthemarket.com/api/v1/list_filings \  -H "Authorization: Bearer $CTM_KEY" \  -H 'Content-Type: application/json' \  -d '{"symbol":"AAPL","form":"10-K"}'

Can answer

bad_request — `symbol` is missing or empty.

Related: get_filing_outline, read_filing_section, score_evidence

get_filing_outline

Free

The scan tool: call it before reading anything. Returns every addressable section of one filing -- by default the latest annual report -- across two lanes, each entry with a `section_id` for read_filing_section and a measured size so you can choose what to spend. The notes lane comes from EDGAR's own per-note rendering; read `notes.label` for what that category contains, because it is wider than the accounting notes alone. The narrative lane is regex-sectioned from the primary document and is SECOND CLASS: it carries `confidence`, `failure_reason` and `notice` at document level and `confidence`, `flags` and `notice` per section. Some filers incorporate MD&A by reference and print only a pointer where the disclosure should be; that section is flagged `incorporated_by_reference` and must not be read as the disclosure itself.

Arguments

symbol string, required — The ticker as Crush The Market spells it.

accession string, optional — A specific filing, as list_filings spells it. Defaults to the latest annual report (10-K, 20-F or 40-F).

sizes boolean, optional — Measure every note by fetching it, so `chars` and `tokens` are real rather than null. Defaults to true. Pass false for a cheaper outline when you do not need to budget.

Returns

symbol, filing, notes, narrative, note, error, as_of, source, disclaimer

Both lanes of Apple's latest 10-K, every note sized.
> curl -s https://crushthemarket.com/api/v1/get_filing_outline \  -H "Authorization: Bearer $CTM_KEY" \  -H 'Content-Type: application/json' \  -d '{"symbol":"AAPL"}'

Can answer

bad_request — `symbol` is missing, or `accession` is not in the form 0000000000-00-000000.

no_sec_filer — No SEC filer is mapped to the symbol. `adr_symbol` from list_filings may name one that is.

no_annual_report_yet — EDGAR lists filings for the issuer but no annual report among them.

unknown_filing — The accession is not one EDGAR lists for this symbol.

filing_summary_missing — EDGAR has no FilingSummary.xml for the filing, so the notes lane cannot be built.

edgar_unavailable — EDGAR did not answer, or answered with an error, after retries.

Related: list_filings, read_filing_section, search_filing, score_evidence

read_filing_section

Free

Returns the text of the section named by `section_id`, from either lane -- ids starting `note-` come from the notes lane, `item-` (or `whole-document`) from the narrative lane. THE WHOLE SECTION IS RETURNED BY DEFAULT: there is no token cap and no truncation you did not ask for. `total_chars` and `total_tokens` always describe the whole section, `chars` and `tokens` describe what came back, and `complete` says whether they are the same thing. `offset` and `limit` are opt-in; pages concatenate back into the section character for character. On a narrative section, read `section.confidence`, `section.flags` and `section.notice` before quoting -- a section flagged `incorporated_by_reference` is a pointer to another document, not the disclosure.

Arguments

symbol string, required — The ticker as Crush The Market spells it.

section_id string, required — A section id from get_filing_outline, e.g. `note-income-taxes` or `item-1a`.

accession string, optional — A specific filing. Defaults to the latest annual report.

offset number, optional — Opt-in pagination: start this many characters into the section. Omit for the whole thing.

limit number, optional — Opt-in pagination: return at most this many characters. Omit for the whole section, which is the default and the recommended call.

Returns

symbol, filing, lane, section, text, chars, tokens, total_chars, total_tokens, offset, limit, complete, truncated, page, pages_total, next_offset, is_last_page, token_estimate, note, error, as_of, source, disclaimer

One whole note from EDGAR's own rendering.
> curl -s https://crushthemarket.com/api/v1/read_filing_section \  -H "Authorization: Bearer $CTM_KEY" \  -H 'Content-Type: application/json' \  -d '{"symbol":"AAPL","section_id":"note-income-taxes"}'

Can answer

bad_request — `symbol` or `section_id` is missing, `section_id` belongs to neither lane, or `offset`/`limit` are not sane numbers.

report_not_found — The notes lane has no section with that id in this filing; the message lists the ids it does have.

section_not_found — The narrative lane has no section with that id in this filing.

no_sections — The filing could not be sectioned at all, so no narrative id can be addressed. Use `whole-document`.

edgar_unavailable — EDGAR did not answer after retries.

Related: get_filing_outline, search_filing, score_evidence

search_filing

Free

Literal, case-insensitive search across both lanes of one filing -- by default the latest annual report. Returns at most 10 hits, each about 400 characters of context and each tagged with the `section_id` it came from, so you can call read_filing_section and read the whole section rather than reasoning from a snippet. `total_matches` reports how many occurrences there were, so a returned list of ten is never mistaken for the complete set. A narrative hit carries the `confidence` and `notice` of the section it came from.

Arguments

symbol string, required — The ticker as Crush The Market spells it.

query string, required — The phrase to find, at least 3 characters. Matched literally and case-insensitively.

accession string, optional — A specific filing. Defaults to the latest annual report.

lane string, optional — Which lane to search: notes, narrative, or both. Defaults to both.

Returns

symbol, filing, query, lane, sections_searched, total_matches, count, hits, note, error, as_of, source, disclaimer

Both lanes of Apple's latest 10-K.
> curl -s https://crushthemarket.com/api/v1/search_filing \  -H "Authorization: Bearer $CTM_KEY" \  -H 'Content-Type: application/json' \  -d '{"symbol":"AAPL","query":"effective tax rate"}'

Can answer

bad_request — `symbol` is missing, `query` is shorter than three characters, or `lane` is not one of notes/narrative/both.

no_sec_filer — No SEC filer is mapped to the symbol.

edgar_unavailable — Neither lane could be read from EDGAR.

Related: get_filing_outline, read_filing_section

score_evidence

Free

Takes one of the six scores and returns every leg that makes it up -- the leg, its weight or axis, its value where the scoring run stored one, and the stored measurements that feed it -- each paired with the note or notes from the company's own latest annual report whose subject covers that leg, quoted inline with a `section_id` for reading the whole note. THE TOOL DOES NOT ASSERT THAT THE TEXT EXPLAINS THE NUMBER: it states where the filer writes about the subject, and the inference is yours. A leg with no sensible note returns NO note and says why in `unmapped_because` -- a forced match would have you cite an irrelevant disclosure with total confidence. The quote is capped; `section_chars` is the whole note's size and read_filing_section returns it uncapped. Where a company has no SEC filing, the legs still come back and `note` says why nothing is quoted.

Arguments

symbol string, required — The ticker as Crush The Market spells it.

score string, required — Which score to break down. One of: value_score, growth_score, financial_score, dividend_score, management_score, total_score.

accession string, optional — Take the notes from this filing rather than the latest annual report.

Returns

symbol, score, score_label, score_value, score_column, filer_family, filing, legs, leg_count, legs_with_notes, legs_without_notes, quote_chars_max, token_estimate, note, error, as_of, source, disclaimer

Four weighted legs; goodwill discipline pairs with the goodwill note, capital allocation with the equity note.
> curl -s https://crushthemarket.com/api/v1/score_evidence \  -H "Authorization: Bearer $CTM_KEY" \  -H 'Content-Type: application/json' \  -d '{"symbol":"AAPL","score":"management_score"}'

Can answer

bad_request — `symbol` is missing, or `score` is not one of the six score ids.

unknown_symbol — No profile is held for the symbol at all.

not_covered — The symbol is an ETF, which is never scored.

no_analysis — The company is covered but has no scored row yet.

Related: get_company, get_filing_outline, read_filing_section

Errors

Every error is JSON in the same shape, with a stable code to branch on and a sentence for a human or a model to read:

401
{  "error": {    "code": "invalid_key",    "message": "That API key is not valid, or it has been revoked. Create a new one from your account page, or start at https://crushthemarket.com/subscribe"  }}
StatuscodeWhenWhat to do
400bad_requestUnknown tool, missing argument, wrong type, undeclared argument.Fix the call. Retrying it unchanged will not help.
400unknown_criterionA screening criterion id we do not publish.Look the id up with explain_metric.
401missing_keyNo key on the request.Send the Authorization header.
401invalid_keyUnknown, mistyped or revoked key.Create a new key on your account page.
402requires_proThe tool needs a Pro subscription.Upgrade, or use a free tool.
403email_unverifiedThe account's email address is not verified.Request a new verification email from your account page.
404unknown_symbol / not_covered / no_analysisThe lookup missed: no such symbol, not covered, or covered but not yet scored.Resolve the symbol with search_companies first.
429quota_exhaustedThe free allowance is spent.Wait for reset_at, or upgrade.
429rate_limitedToo fast for the pace limit.Wait Retry-After seconds and retry.
500internalThe server failed.Retry once. The message never carries a stack or an upstream URL.

! The two 429s share a status and are told apart by code. quota_exhausted means upgrade or wait for the window; rate_limited means slow down. Retrying a quota_exhausted at any speed will not work.

Allowance, pace limit and headers

  • —Free — 12 agent calls per 16 hours. Its own allowance: the 12 screening and search actions the website gives you are counted separately.
  • —Pro — no call limit.
  • —Pace limit — 60 calls a minute and 5,000 a day per key, refilling continuously. It is not an allowance and Pro does not lift it. A separate, looser limit applies per IP address.

Both are readable from the response headers, so a client never has to guess:

HeaderMeans
X-Quota-UsedAgent calls spent in the current 16-hour window.
X-Quota-LimitThe allowance. Absent on Pro, which has none.
X-Quota-RemainingWhat is left. Absent on Pro.
X-Quota-ResetISO 8601 timestamp the window resets at. Absent on Pro.
X-RateLimit-LimitPace limit capacity.
X-RateLimit-RemainingCapacity left right now.
X-RateLimit-ResetWhen the bucket is full again.
Retry-AfterSeconds to wait. Sent only with a rate_limited 429.

The same figures ride in the body too, in a quota block, because headers are dropped by the time a response reaches a model. With two calls or fewer left, the body also carries a notice sentence saying so.

Every successful response also carries as_of, source and disclaimer. Nothing here is a recommendation: data, not advice.

Machine-readable

Using a chat client rather than writing code? The MCP guide sets the same tools up in one command.

More for agents

AgentsWhat exists, and the three-step quickstart.
MCPConnect Claude Code or claude.ai.
REST API — you are hereBearer auth, endpoints, errors and limits.
ToolsOne page per tool: arguments, a worked call, what it can fail with.

Data, not advice. Do your own diligence.

Noticed an error with something on our site?

Data, not advice. Do your own diligence.

We Use Cookies