Universal news search
One query, planned into angles and localized into up to 12 Google News country editions in parallel, merged into a single deduplicated article list with honest per-leg provenance. JSON or SSE.
GET /v1/search/news
Send one query and get back a planned, localized, multi-country news search in a single call. An LLM plans your query into search angles and localizes each angle into the language of every requested country edition (one combined call, fail-open), the lane fans out up to 12 parallel legs against Google News (50 supported country editions), and the articles merge into one deduplicated list. Every leg reports truthful provenance: query_source tells you whether its keyword was translated, confirmed original, or a fallback_original after a localization failure. Returns one JSON envelope by default, or a live SSE stream when you ask for one.
curl 'https://www.socialcrawl.dev/v1/search/news?query=samsung+galaxy+launch&countries=KR,US,JP' \
-H 'x-api-key: sc_...'Credit cost: metered, 2-14 credits per call: a 2-credit base covers planning and localization, then 1 credit per leg that returned at least one article. The upfront hold is 2 + min(5 x countries, max_legs, 12) and the unused remainder is refunded automatically, so the credits_used you see in the response is what you actually paid. See Billing and Endpoint pricing.
Auth: x-api-key header, same as every other endpoint.
Method: GET, all inputs as query params.
Request parameters
| Param | Required | Type | Default | Description |
|---|---|---|---|---|
query | yes | string (1 to 500 chars) | none | News topic or question. Boolean AND/OR/NOT and quoted phrases are supported; Google advanced operators (site:, intitle:, before:) are rejected with a 400. |
countries | no | CSV of ISO alpha-2 codes | US | 1 to 12 of the 50 supported country editions (e.g. KR,US,JP). Each country searches its Google News edition in that edition's default language; duplicates collapse. |
time_range | no | day | week | month | year | day | Recency window. Ignored when from/to is present (explicit dates win). |
from | no | YYYY-MM-DD or Unix | none | Lower publish-date bound. Pins every leg to the primary news source (see below). |
to | no | YYYY-MM-DD or Unix | none | Upper publish-date bound. Pins every leg to the primary news source. |
publisher | no | bare domain | none | Publisher filter, e.g. bbc.com (no scheme, no path). Pins every leg to the primary news source. |
depth | no | integer 10-100, steps of 10 | 10 | Articles requested per leg. |
max_legs | no | integer 1-12 | 12 | Hard cap on billable legs. The upfront hold shrinks with it, so it is your cost-control knob. |
publisher, from, and to pin every leg to the primary Google News source: the fallback source cannot express those filters, and silently dropping a filter would return unfiltered data. Pinned requests trade away the fallback's empty-result rescue for filter correctness.
How a request becomes legs
- Plan. One combined LLM call turns your query into up to 5 weighted search angles and localizes each angle into the default language of every requested country. The call is fail-open: on timeout or error the lane degrades to a single identity angle (your query, clipped to a keyword) and every leg is stamped
fallback_original, never silently. - Expand. Angles cross with countries into legs, capped at
min(5 x countries, max_legs, 12). Identical (country, language, keyword) legs collapse before dispatch, so two angles that localize to the same keyword never bill twice. - Fan out. Every leg runs in parallel as an in-process
google_news/searchcall (primary source with automatic fallback and empty-result rescue, unless the request is pinned). - Assemble. Articles dedupe across legs on
canonical_url ?? url; the first leg's copy wins the body and every later leg that returned the same article is appended to that item'slegs_returnedprovenance list.
The query_source vocabulary on every leg is part of the public contract:
translated: the localizer produced a different keyword for that language.original: the localizer confirmed the planned keyword is already correct for that language. This value is never stamped on a failure path.fallback_original: localization was unavailable for that language (call failed, timed out, or the language was missing from the result), so the leg searched the planned keyword as-is. A total localizer outage is therefore always distinguishable from "keyword already correct".
What you get back (JSON)
By default (Accept: application/json) you get one envelope. data carries:
| Field | Type | Description |
|---|---|---|
query | string | Your query as parsed. |
status | string | succeeded (every leg returned), partial (some legs failed), or failed (every leg failed). |
plan | object | The resolved plan: topic, source (llm or identity), degraded, subqueries[], notes[]. |
legs | array | One truthful outcome row per dispatched leg (below). |
items | array | Deduplicated articles across all legs, each with legs_returned provenance. |
total_articles | number | items.length. |
countries_searched | array | The resolved country codes, first-seen order. |
credits_used | number | The net charge for this call (base + productive legs). |
methodology_version | string | search-news/1.0. |
Each legs[] row: leg_id (angleId|COUNTRY), country, language, angle_id, angle_label, effective_query (the keyword actually searched), query_source, status (succeeded / failed / skipped), article_count, quarantined_count (rows dropped as schema-invalid or for having no usable http(s) URL), credits_used (1 only when the leg succeeded with at least one article, else 0), latency_ms, error.
Each items[] article: id (upstream article id, null when the source omits one), title, url (always http(s)), canonical_url (lane-derived: fragment dropped, tracking params stripped, host lowercased), source_name, domain, snippet, image_url, published_at (UTC ISO or null, never fabricated), rank, placement, plus the provenance stamps from the leg that found it (country_code, language_code, angle_id, effective_query, query_source) and legs_returned (every leg that returned it).
- Zero articles with succeeded legs is a 200 success with an empty
itemsand base-only billing, not an error. - All legs failed is a
502 ALL_LEGS_FAILEDwithok: false; the full hold is refunded automatically. The 502 body is the standard error envelope on its own: per-leg detail (legs,plan) is not returned on this path, so ask for the SSE stream when you need to see which legs failed and why.
Streaming (SSE)
Add Accept: text/event-stream and you get a live stream instead of one envelope. Chunk types in order:
{ "type": "meta", "query": "...", "plan": {...}, "sources_planned": ["KR", "US", ...] }
{ "type": "plan_refined", "plan": {...}, "legs": [{ "leg_id": "sq0|KR", "country": "KR", "language": "ko", "keyword": "...", "query_source": "translated", "angle_id": "sq0", "angle_label": "..." }] }
{ "type": "source_started", "source": "sq0|KR" }
{ "type": "items", "source": "sq0|KR", "items": [...], "duration_ms": 1234 }
{ "type": "source_failed", "source": "sq1|US", "error": { "code": "...", "message": "..." } }
{ "type": "done", "summary": { "credits_used": 4, "refunded": true, "coverage": 1, ... } }Order and guarantees:
metais first, carrying the deterministic identity plan before the LLM call resolves.plan_refinedis always emitted on this lane (unlike/v1/search/everywhere, where it is conditional on the LLM changing the plan). It carries the final plan plus the expandedlegs[]with per-legquery_source, so a consumer can render localization honesty (translated vs fallback chips) at plan time, before any leg settles. A degraded run still emits it, with every leg stampedfallback_original.- Legs are keyed as
sourcestrings in the formangleId|COUNTRY(e.g.sq0|KR; angle ids are positional with ansqprefix). These are per-request keys, not the fixed source ids of/v1/search/everywhere; the everywhere client reducer is not suitable for news streams. Skipped legs (aborted before dispatch) surface assource_failedwitherror.code: "aborted". itemsyield in completion order, so fast editions land first. The deduplicated cross-leg view is only available in sync mode or by deduplicating client-side oncanonical_url ?? url.doneis always last with the run summary:credits_used(net, equals theX-Credits-Usedheader),refunded,coverage,sources_called/sources_succeeded/sources_failed,total_items,partial_failure.
Billing on a stream is charge-for-delivered: if you disconnect mid-stream you pay the base plus the productive legs whose items chunks you received; the rest of the hold is refunded. On an internal error the whole hold is refunded and the terminal done reports credits_used: 0, refunded: true.
Streaming responses are never cached and never touch the sync cache. They are also not replayable with an Idempotency-Key: replay applies to sync JSON responses only, and an SSE-accept request that reuses a spent key will receive the stored JSON body back instead of a stream.
Billing
Metered, 2-14 credits: 2 base plus 1 per productive leg, held upfront at 2 + min(5 x countries, max_legs, 12) and settled down to actual after the run.
| Scenario | Charge |
|---|---|
| 3 countries, all legs return articles | 2 + productive legs (up to 12 with max_legs=12) |
| Zero articles anywhere, legs succeeded | 2 (base only), 200 response |
| Every leg failed | 0 (full automatic refund), 502 ALL_LEGS_FAILED |
| Cache hit (sync, within 5 minutes) | 0, X-Cache: HIT |
| Idempotency-Key replay (sync) | 0, X-Idempotent-Replay: true |
X-Credits-Used on the response (and done.summary.credits_used on a stream) is the source of truth for what you paid. Use max_legs to cap the worst case: max_legs=3 caps the hold at 5 credits regardless of how many countries you request.
Latency and failure modes
The lane plans before it fans out, so the combined plan+localize call is on the critical path. Its budget scales with the number of distinct languages (8s base + 0.7s per extra language, capped at 18s); on timeout the run degrades to the identity plan with every leg stamped fallback_original and a note in plan.notes, it does not fail. Legs then run fully in parallel, each bounded by the remaining route budget. Typical end-to-end: a few seconds for one country, 10 to 30s for a wide multi-language fan-out.
- One leg times out or errors: its row reports
status: "failed"with an error class; sibling legs continue; the leg bills 0. - The localizer drops a language (operator-laden, overlong, or script-mismatched keyword): that leg searches the planned keyword stamped
fallback_original. - Client disconnect (SSE): work stops, undelivered legs are refunded, the charge is settled exactly once.
Supported countries: the 50 editions in the lane's catalog (up to date list in the error message of an unknown countries code). Notable exclusions: Russia and Lebanon are not servable by the primary news source and are not in the catalog.
Related
GET /v1/search/everywhere: the universal social search sibling.GET /v1/google_news/search: the single-edition news search each leg rides on.- Credits and Endpoint pricing.
