SocialCrawl

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

ParamRequiredTypeDefaultDescription
queryyesstring (1 to 500 chars)noneNews topic or question. Boolean AND/OR/NOT and quoted phrases are supported; Google advanced operators (site:, intitle:, before:) are rejected with a 400.
countriesnoCSV of ISO alpha-2 codesUS1 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_rangenoday | week | month | yeardayRecency window. Ignored when from/to is present (explicit dates win).
fromnoYYYY-MM-DD or UnixnoneLower publish-date bound. Pins every leg to the primary news source (see below).
tonoYYYY-MM-DD or UnixnoneUpper publish-date bound. Pins every leg to the primary news source.
publishernobare domainnonePublisher filter, e.g. bbc.com (no scheme, no path). Pins every leg to the primary news source.
depthnointeger 10-100, steps of 1010Articles requested per leg.
max_legsnointeger 1-1212Hard 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

  1. 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.
  2. 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.
  3. Fan out. Every leg runs in parallel as an in-process google_news/search call (primary source with automatic fallback and empty-result rescue, unless the request is pinned).
  4. 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's legs_returned provenance 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:

FieldTypeDescription
querystringYour query as parsed.
statusstringsucceeded (every leg returned), partial (some legs failed), or failed (every leg failed).
planobjectThe resolved plan: topic, source (llm or identity), degraded, subqueries[], notes[].
legsarrayOne truthful outcome row per dispatched leg (below).
itemsarrayDeduplicated articles across all legs, each with legs_returned provenance.
total_articlesnumberitems.length.
countries_searchedarrayThe resolved country codes, first-seen order.
credits_usednumberThe net charge for this call (base + productive legs).
methodology_versionstringsearch-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 items and base-only billing, not an error.
  • All legs failed is a 502 ALL_LEGS_FAILED with ok: 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:

  • meta is first, carrying the deterministic identity plan before the LLM call resolves.
  • plan_refined is 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 expanded legs[] with per-leg query_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 stamped fallback_original.
  • Legs are keyed as source strings in the form angleId|COUNTRY (e.g. sq0|KR; angle ids are positional with an sq prefix). 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 as source_failed with error.code: "aborted".
  • items yield in completion order, so fast editions land first. The deduplicated cross-leg view is only available in sync mode or by deduplicating client-side on canonical_url ?? url.
  • done is always last with the run summary: credits_used (net, equals the X-Credits-Used header), 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.

ScenarioCharge
3 countries, all legs return articles2 + productive legs (up to 12 with max_legs=12)
Zero articles anywhere, legs succeeded2 (base only), 200 response
Every leg failed0 (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.