{"openapi":"3.0.2","info":{"title":"NewsMCP REST API","description":"\nRead-only news search that returns **events** instead of a flat list of articles.\n\nAn event is a group of related articles covering the same story, returned with a few\nrepresentative citations — headline, summary, link, date, theme, and sentiment.\n\nAuthentication is the `x-api-token` header. Requests without a token are still served, under\nreduced limits, and the response includes `keyless_notices` describing what was narrowed.\n","version":"0.4.0"},"paths":{"/v0/search_news":{"get":{"tags":["Search"],"summary":"Search news, grouped into events","description":"Search news and return **events** — groups of related articles covering the same story —\nrather than a flat list of articles.\n\nEach event carries up to a few representative article citations: headline, summary, link,\ndate, theme, and sentiment.\n\nHow a request is served:\n\n1. The query is parsed into an Elasticsearch query.\n2. `from_` selects the recent or historic index; a range spanning both uses historic.\n3. Hidden quality filters are always applied (see the service README).\n4. A sample of matching articles is fetched and clustered by embedding similarity.\n5. Clusters become events, ordered according to `sort_by`, and capped to `event_size`.\n\nTwo things that commonly surprise callers:\n\n* `events_count` is the number of clusters found in the sample before the `event_size` cap.\n* `event_id` is derived from cluster membership, so it changes between requests for the same\n  story. It is not a durable identifier.\n\nAuthentication: send `x-api-token`. Without a token, requests are still served under reduced\nlimits (English only, last 24 hours, at most 8 events) and the response includes\n`keyless_notices` explaining what was narrowed.","operationId":"get_search_news_v0_search_news_get","parameters":[{"description":"Search query. Lucene-like syntax: boolean operators `AND`, `OR`, `NOT` (aliases `&&`, `||`, `!`), exact phrases in double quotes, grouping with parentheses, and `*` as a wildcard. It also supports two extended operators: `NEAR(term1, term2, distance)` for proximity search, and `MULTIPLE(term, count)` to require a term at least `count` times. Must not be blank. Example: `(OpenAI OR Anthropic) AND model`.","required":true,"schema":{"title":"Q","type":"string","description":"Search query. Lucene-like syntax: boolean operators `AND`, `OR`, `NOT` (aliases `&&`, `||`, `!`), exact phrases in double quotes, grouping with parentheses, and `*` as a wildcard. It also supports two extended operators: `NEAR(term1, term2, distance)` for proximity search, and `MULTIPLE(term, count)` to require a term at least `count` times. Must not be blank. Example: `(OpenAI OR Anthropic) AND model`."},"example":"artificial intelligence","name":"q","in":"query"},{"description":"Which article fields the query matches against. `title` (default) searches headlines only; `title_content` searches headline and body text.","required":false,"schema":{"title":"Search In","type":"string","description":"Which article fields the query matches against. `title` (default) searches headlines only; `title_content` searches headline and body text.","default":"title"},"name":"search_in","in":"query"},{"description":"Restrict results to these news sources, as domains or subdomains (e.g. `bbc.co.uk`, `edition.cnn.com`). Accepts a repeated parameter (`?sources=a.com&sources=b.com`) or one comma-separated string (`?sources=a.com,b.com`). NOTE: supplying `sources` overrides the default trusted-publisher filter, so you get exactly the sources you asked for.","required":false,"schema":{"title":"Sources","type":"array","items":{"type":"string"},"description":"Restrict results to these news sources, as domains or subdomains (e.g. `bbc.co.uk`, `edition.cnn.com`). Accepts a repeated parameter (`?sources=a.com&sources=b.com`) or one comma-separated string (`?sources=a.com,b.com`). NOTE: supplying `sources` overrides the default trusted-publisher filter, so you get exactly the sources you asked for."},"name":"sources","in":"query"},{"description":"Top-N sources with optional language, topic, and country facets, for example: 'Top 10 English Finance US'.","required":false,"schema":{"title":"Source Groups","type":"string","description":"Top-N sources with optional language, topic, and country facets, for example: 'Top 10 English Finance US'."},"name":"source_groups","in":"query"},{"description":"Restrict results to these languages, as two-letter ISO 639-1 codes (e.g. `en`, `de`). Repeated or comma-separated. Rejected with `499` if a code is not in the supported set. Without an API token, requests are restricted to `en` and any other value is reset to `en` with a notice in `keyless_notices`.","required":false,"schema":{"title":"Lang","type":"array","items":{"type":"string"},"description":"Restrict results to these languages, as two-letter ISO 639-1 codes (e.g. `en`, `de`). Repeated or comma-separated. Rejected with `499` if a code is not in the supported set. Without an API token, requests are restricted to `en` and any other value is reset to `en` with a notice in `keyless_notices`."},"name":"lang","in":"query"},{"description":"Restrict results to these publication countries, as two-letter ISO 3166-1 alpha-2 codes (e.g. `US`, `GB`). Case-insensitive on input; normalised to uppercase. Repeated or comma-separated.","required":false,"schema":{"title":"Countries","type":"array","items":{"type":"string"},"description":"Restrict results to these publication countries, as two-letter ISO 3166-1 alpha-2 codes (e.g. `US`, `GB`). Case-insensitive on input; normalised to uppercase. Repeated or comma-separated."},"name":"countries","in":"query"},{"description":"Inclusive lower bound on publication date. Accepts an ISO 8601 datetime (`2026-07-01T00:00:00`) or natural language (`7 days ago`, `yesterday`, `now`). Defaults to 7 days ago at 00:00 UTC. All dates are treated as UTC. Without an API token, lookback is limited to the last 24 hours; older values are reset with a notice in `keyless_notices`.","required":false,"schema":{"title":"From ","type":"string","description":"Inclusive lower bound on publication date. Accepts an ISO 8601 datetime (`2026-07-01T00:00:00`) or natural language (`7 days ago`, `yesterday`, `now`). Defaults to 7 days ago at 00:00 UTC. All dates are treated as UTC. Without an API token, lookback is limited to the last 24 hours; older values are reset with a notice in `keyless_notices`."},"example":"1 day ago","name":"from_","in":"query"},{"description":"Inclusive upper bound on publication date. Same formats as `from_`. Defaults to the current UTC time. Must not be earlier than `from_` (rejected with `499`).","required":false,"schema":{"title":"To ","type":"string","description":"Inclusive upper bound on publication date. Same formats as `from_`. Defaults to the current UTC time. Must not be earlier than `from_` (rejected with `499`)."},"example":"now","name":"to_","in":"query"},{"description":"Controls both the Elasticsearch article sample order and the order of returned events. `relevancy` (default): best text match first in the sample; events ordered by `event_score` descending. `cluster_size`: same sample as relevancy; events ordered by `article_count` descending. `date`: newest articles first in the sample; events ordered by the latest `published_date` in each cluster (newest event first). `-date`: oldest articles first in the sample; events ordered by the latest `published_date` in each cluster (oldest event first). Note: for date sorts the leading minus on `-date` follows the V3 convention (ascending), which is the opposite of the usual `-field` meaning.","required":false,"schema":{"title":"Sort By","type":"string","description":"Controls both the Elasticsearch article sample order and the order of returned events. `relevancy` (default): best text match first in the sample; events ordered by `event_score` descending. `cluster_size`: same sample as relevancy; events ordered by `article_count` descending. `date`: newest articles first in the sample; events ordered by the latest `published_date` in each cluster (newest event first). `-date`: oldest articles first in the sample; events ordered by the latest `published_date` in each cluster (oldest event first). Note: for date sorts the leading minus on `-date` follows the V3 convention (ascending), which is the opposite of the usual `-field` meaning.","default":"relevancy"},"name":"sort_by","in":"query"},{"description":"Restrict results to these theme labels. Repeated or comma-separated. Allowed values: Automotive, Business, Crime, Economics, Entertainment, Finance, Financial Crime, General, Health, Lifestyle, Politics, Science, Sports, Tech, Travel, Weather.","required":false,"schema":{"title":"Theme","type":"array","items":{"type":"string"},"description":"Restrict results to these theme labels. Repeated or comma-separated. Allowed values: Automotive, Business, Crime, Economics, Entertainment, Finance, Financial Crime, General, Health, Lifestyle, Politics, Science, Sports, Tech, Travel, Weather."},"name":"theme","in":"query"},{"description":"Restrict results to these sentiment labels. Allowed values: `Negative`, `Neutral`, `Positive` — case-insensitive on input (`negative` works) and normalised to capitalised form in the response. Repeated or comma-separated.","required":false,"schema":{"title":"Sentiment","type":"array","items":{"type":"string"},"description":"Restrict results to these sentiment labels. Allowed values: `Negative`, `Neutral`, `Positive` — case-insensitive on input (`negative` works) and normalised to capitalised form in the response. Repeated or comma-separated."},"name":"sentiment","in":"query"},{"description":"How aggressively similar articles are grouped into one event, via the cosine-similarity threshold used by clustering. `low` (default) = threshold 0.7, loosest grouping, fewer and broader events. `medium` = 0.8. `high` = 0.9, strictest, more and tighter events (more articles end up as single-article events).","required":false,"schema":{"title":"Grouping","type":"string","description":"How aggressively similar articles are grouped into one event, via the cosine-similarity threshold used by clustering. `low` (default) = threshold 0.7, loosest grouping, fewer and broader events. `medium` = 0.8. `high` = 0.9, strictest, more and tighter events (more articles end up as single-article events).","default":"low"},"name":"grouping","in":"query"},{"description":"Maximum number of events to return, from 1 to 100. Default 8. This caps EVENTS, not the number of articles fetched and grouped, so lowering it does not make the request cheaper. There is no pagination cursor — one page of events only. Without an API token, requests are capped at 8 regardless of this value, with a notice in `keyless_notices`.","required":false,"schema":{"title":"Event Size","type":"integer","description":"Maximum number of events to return, from 1 to 100. Default 8. This caps EVENTS, not the number of articles fetched and grouped, so lowering it does not make the request cheaper. There is no pagination cursor — one page of events only. Without an API token, requests are capped at 8 regardless of this value, with a notice in `keyless_notices`.","default":8},"name":"event_size","in":"query"},{"description":"NewsMCP API token","required":false,"schema":{"title":"X-Api-Token","type":"string","description":"NewsMCP API token"},"name":"x-api-token","in":"header"}],"responses":{"200":{"description":"Matching events, most prominent first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchNewsResponse"}}}},"401":{"description":"Missing or invalid `x-api-token` (and keyless access is off)."},"403":{"description":"Keyless request blocked by abuse heuristics."},"422":{"description":"Unparseable query syntax in `q` (`error_code`: `invalid_query`)."},"429":{"description":"Keyless rate limit reached. Includes `error_code`, `Retry-After` header, and `retry_after_seconds` in the body."},"499":{"description":"Request validation failed. Returns `error_code` and an `errors` array with per-field details."},"503":{"description":"The requested date range is not currently available."}}},"post":{"tags":["Search"],"summary":"Search news, grouped into events","description":"Search news and return **events** — groups of related articles covering the same story —\nrather than a flat list of articles.\n\nEach event carries up to a few representative article citations: headline, summary, link,\ndate, theme, and sentiment.\n\nHow a request is served:\n\n1. The query is parsed into an Elasticsearch query.\n2. `from_` selects the recent or historic index; a range spanning both uses historic.\n3. Hidden quality filters are always applied (see the service README).\n4. A sample of matching articles is fetched and clustered by embedding similarity.\n5. Clusters become events, ordered according to `sort_by`, and capped to `event_size`.\n\nTwo things that commonly surprise callers:\n\n* `events_count` is the number of clusters found in the sample before the `event_size` cap.\n* `event_id` is derived from cluster membership, so it changes between requests for the same\n  story. It is not a durable identifier.\n\nAuthentication: send `x-api-token`. Without a token, requests are still served under reduced\nlimits (English only, last 24 hours, at most 8 events) and the response includes\n`keyless_notices` explaining what was narrowed.","operationId":"post_search_news_v0_search_news_post","parameters":[{"description":"NewsMCP API token","required":false,"schema":{"title":"X-Api-Token","type":"string","description":"NewsMCP API token"},"name":"x-api-token","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchNewsRequest"}}},"required":true},"responses":{"200":{"description":"Matching events, most prominent first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchNewsResponse"}}}},"401":{"description":"Missing or invalid `x-api-token` (and keyless access is off)."},"403":{"description":"Keyless request blocked by abuse heuristics."},"422":{"description":"Unparseable query syntax in `q` (`error_code`: `invalid_query`)."},"429":{"description":"Keyless rate limit reached. Includes `error_code`, `Retry-After` header, and `retry_after_seconds` in the body."},"499":{"description":"Request validation failed. Returns `error_code` and an `errors` array with per-field details."},"503":{"description":"The requested date range is not currently available."}}}},"/health":{"get":{"tags":["Health"],"summary":"Health","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Health Health Get","type":"string"}}}}}}}},"components":{"schemas":{"ArticleResponse":{"title":"ArticleResponse","type":"object","properties":{"title":{"title":"Title","type":"string","description":"Article headline."},"summary":{"title":"Summary","type":"string","description":"Short summary of the article. Null when no summary is available."},"link":{"title":"Link","type":"string","description":"Canonical URL of the article on the publisher's site."},"published_date":{"title":"Published Date","anyOf":[{"type":"string","format":"date-time"},{"type":"string"}],"description":"Publication date/time in UTC."},"theme":{"title":"Theme","anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Theme label(s) assigned to the article. May be a single string or a list."},"sentiment":{"title":"Sentiment","type":"string","description":"Sentiment label for the article: `Negative`, `Neutral`, or `Positive`."}},"description":"One article citation inside an event."},"EventResponse":{"title":"EventResponse","required":["event_id","article_count","event_score","articles"],"type":"object","properties":{"event_id":{"title":"Event Id","type":"string","description":"Stable identifier for this event: a SHA-256 hash of its sorted member article IDs. It is derived from cluster membership, so the same real-world story yields a DIFFERENT `event_id` across requests whenever the fetched sample changes. Do not use it as a long-lived key."},"article_count":{"title":"Article Count","type":"integer","description":"Number of articles grouped into this event within the fetched sample — which may exceed the number of entries in `articles`, since at most 3 top article citations are returned per event. It is a sample-local count, not a global count across all matching articles."},"event_score":{"title":"Event Score","type":"number","description":"Mean Elasticsearch `_score` across **every** article in this event's cluster within the fetched sample — not just the citations returned in `articles`. Formula: `sum(_score) / article_count`; missing `_score` counts as `0.0`. Reflects how strongly the cluster's members matched the query on average (text relevance plus ES rank signals). Two events in the same response can differ (e.g. 1.9 vs 1.2) because their clusters had different average match quality — not because of `article_count` alone. Used as the primary event rank when `sort_by=relevancy`. Comparable between events in the SAME response only — not across requests; not a probability or normalized 0–1 score."},"articles":{"title":"Articles","type":"array","items":{"$ref":"#/components/schemas/ArticleResponse"},"description":"Up to 3 representative article citations for this event — headline, summary, link, date, theme, and sentiment."}},"description":"A group of semantically similar articles covering one story."},"SearchNewsRequest":{"title":"SearchNewsRequest","required":["q"],"type":"object","properties":{"q":{"title":"Q","minLength":1,"type":"string","description":"Search query. Lucene-like syntax: boolean operators `AND`, `OR`, `NOT` (aliases `&&`, `||`, `!`), exact phrases in double quotes, grouping with parentheses, and `*` as a wildcard. It also supports two extended operators: `NEAR(term1, term2, distance)` for proximity search, and `MULTIPLE(term, count)` to require a term at least `count` times. Must not be blank. Example: `(OpenAI OR Anthropic) AND model`.","example":"artificial intelligence"},"search_in":{"title":"Search In","enum":["title","title_content"],"type":"string","description":"Which article fields the query matches against. `title` (default) searches headlines only; `title_content` searches headline and body text.","default":"title"},"sources":{"title":"Sources","type":"array","items":{"type":"string"},"description":"Restrict results to these news sources, as domains or subdomains (e.g. `bbc.co.uk`, `edition.cnn.com`). Accepts a repeated parameter (`?sources=a.com&sources=b.com`) or one comma-separated string (`?sources=a.com,b.com`). NOTE: supplying `sources` overrides the default trusted-publisher filter, so you get exactly the sources you asked for."},"source_groups":{"title":"Source Groups","type":"string","description":"One Top-N source expression with optional language, topic, and country facets, for example: 'Top 10 English Finance US'."},"lang":{"title":"Lang","type":"array","items":{"type":"string"},"description":"Restrict results to these languages, as two-letter ISO 639-1 codes (e.g. `en`, `de`). Repeated or comma-separated. Rejected with `499` if a code is not in the supported set. Without an API token, requests are restricted to `en` and any other value is reset to `en` with a notice in `keyless_notices`.","example":["en"]},"countries":{"title":"Countries","type":"array","items":{"type":"string"},"description":"Restrict results to these publication countries, as two-letter ISO 3166-1 alpha-2 codes (e.g. `US`, `GB`). Case-insensitive on input; normalised to uppercase. Repeated or comma-separated."},"from_":{"title":"From ","anyOf":[{"type":"string","format":"date-time"},{"type":"string"}],"description":"Inclusive lower bound on publication date. Accepts an ISO 8601 datetime (`2026-07-01T00:00:00`) or natural language (`7 days ago`, `yesterday`, `now`). Defaults to 7 days ago at 00:00 UTC. All dates are treated as UTC. Without an API token, lookback is limited to the last 24 hours; older values are reset with a notice in `keyless_notices`.","example":"1 day ago"},"to_":{"title":"To ","anyOf":[{"type":"string","format":"date-time"},{"type":"string"}],"description":"Inclusive upper bound on publication date. Same formats as `from_`. Defaults to the current UTC time. Must not be earlier than `from_` (rejected with `499`).","example":"now"},"sort_by":{"title":"Sort By","enum":["relevancy","date","-date","cluster_size"],"type":"string","description":"Controls both the Elasticsearch article sample order and the order of returned events. `relevancy` (default): best text match first in the sample; events ordered by `event_score` descending. `cluster_size`: same sample as relevancy; events ordered by `article_count` descending. `date`: newest articles first in the sample; events ordered by the latest `published_date` in each cluster (newest event first). `-date`: oldest articles first in the sample; events ordered by the latest `published_date` in each cluster (oldest event first). Note: for date sorts the leading minus on `-date` follows the V3 convention (ascending), which is the opposite of the usual `-field` meaning.","default":"relevancy"},"theme":{"title":"Theme","type":"array","items":{"type":"string"},"description":"Restrict results to these theme labels. Repeated or comma-separated. Allowed values: Automotive, Business, Crime, Economics, Entertainment, Finance, Financial Crime, General, Health, Lifestyle, Politics, Science, Sports, Tech, Travel, Weather."},"sentiment":{"title":"Sentiment","type":"array","items":{"type":"string"},"description":"Restrict results to these sentiment labels. Allowed values: `Negative`, `Neutral`, `Positive` — case-insensitive on input (`negative` works) and normalised to capitalised form in the response. Repeated or comma-separated."},"grouping":{"title":"Grouping","enum":["low","medium","high"],"type":"string","description":"How aggressively similar articles are grouped into one event, via the cosine-similarity threshold used by clustering. `low` (default) = threshold 0.7, loosest grouping, fewer and broader events. `medium` = 0.8. `high` = 0.9, strictest, more and tighter events (more articles end up as single-article events).","default":"low"},"event_size":{"title":"Event Size","maximum":100.0,"minimum":1.0,"type":"integer","description":"Maximum number of events to return, from 1 to 100. Default 8. This caps EVENTS, not the number of articles fetched and grouped, so lowering it does not make the request cheaper. There is no pagination cursor — one page of events only. Without an API token, requests are capped at 8 regardless of this value, with a notice in `keyless_notices`.","default":8}},"additionalProperties":false,"description":"Search parameters for `/v0/search_news` (shared by GET and POST)."},"SearchNewsResponse":{"title":"SearchNewsResponse","required":["status","events_count","events"],"type":"object","properties":{"status":{"title":"Status","type":"string","description":"Always `\"ok\"` on a 2xx response. Failures use the error body instead.","example":"ok"},"events_count":{"title":"Events Count","type":"integer","description":"Total number of event clusters found in the fetched article sample — counted before the `event_size` cap is applied. There is no pagination; this is the full cluster count for the sample, not the number of articles returned."},"events":{"title":"Events","type":"array","items":{"$ref":"#/components/schemas/EventResponse"},"description":"The events, ordered according to `sort_by`: `relevancy` by `event_score` descending; `cluster_size` by `article_count` descending; `date` by latest cluster publication date (newest first); `-date` by latest cluster publication date (oldest first). Ties break on the secondary sort key documented for each mode."},"keyless_notices":{"title":"Keyless Notices","type":"array","items":{"type":"string"},"description":"Human-readable notices explaining which request parameters were narrowed because the call was made without an API key (e.g. `lang` reset to `en`, `from_` reset to the last 24 hours). Omitted entirely for API-key callers, and omitted when nothing was changed."}},"description":"Events matching the search, not a flat list of articles."}}},"tags":[{"name":"Search","description":"Search news and get back grouped story events."},{"name":"Health","description":"Health check endpoint. No authentication required."}]}