Search API

POST https://api.groundroute.ai/v1/search

The core endpoint. Requires Authorization: Bearer <api_key>. The request body is JSON. Unknown fields are ignored, not rejected.

Request

FieldTypeDefaultNotes
querystring (1–2048 chars)Required.
modeenumautoIntent hint; the router may refine it. One of auto, web, news, academic, answer, page.
max_resultsint (1–50)10Part of the cache key.
freshnessenum | nullnullExplicit freshness override: fresh, semi, or static. fresh caps cache TTL and bypasses the semantic tier.
domainsstring[][]Include-filter. Part of the cache key.
exclude_domainsstring[][]Exclude-filter.
langstring (ISO 639-1, ≤8 chars) | nullnullPart of the cache key.
countrystring (ISO 3166-1 alpha-2, ≤2 chars) | nullnullPart of the cache key.

tenant_id and is_byok are server-authoritative — any value sent in the body is dropped and replaced with the value resolved from your API key.

curl -s https://api.groundroute.ai/v1/search \
  -H "Authorization: Bearer $GROUNDROUTE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "transformer attention complexity",
    "mode": "academic",
    "max_results": 10,
    "freshness": "static",
    "domains": ["arxiv.org"],
    "lang": "en"
  }'

Response — 200

All four *_meta blocks are always present, even on a cache hit or a degraded response.

{
  "request_id": "8f1c2e7a-…",      // uuid
  "tenant_id": "…",
  "results": [
    {
      "url": "https://arxiv.org/abs/...",
      "title": "Attention Is All You Need",
      "snippet": "…",
      "content": null,             // full body only when mode/engine provides it
      "score": 0.82,               // normalized relevance, 0–1, or null
      "source_engine": "exa",      // serper|brave|exa|tavily|perplexity|firecrawl
      "published_at": null,        // ISO 8601 or null
      "rank": 0                    // 0-based position as served
    }
  ],
  "answer": null,                  // synthesized answer (answer engines), or null
  "citations": [                   // present when an answer cites sources
    { "url": "https://...", "title": "…", "index": 0 }
  ],
  "degraded": false,               // true = partial results (one engine failed)

  "routing_meta": {
    "strategy": "price_led",       // price_led | learned | fanout
    "query_class": "academic",     // academic | web | news | page | answer
    "chosen_engine": "exa",
    "reason": "price_led:exa $7.00/1k (first eligible for academic, source=static)",
    "candidates": [
      { "engine": "exa",   "cost_per_1k_usd": 7.0, "eligible": true,  "picked": true,  "note": "cheapest eligible" },
      { "engine": "brave", "cost_per_1k_usd": 5.0, "eligible": true,  "picked": false, "note": null }
    ],
    "cost_usd": 0.007,             // actual provider cost incurred this request
    "latency_ms": 412,
    "degraded": false,
    "failover_from": null,         // engine that failed before chosen_engine, or null
    "missing_engines": []
  },

  "cache_meta": {
    "cache_tier": "miss",          // miss | exact_private | exact_pooled | semantic
    "poolable": false,
    "poolability_reason": "cell_not_allowlisted",
    "screens_post_nfkc": true,
    "freshness_bucket": "none",    // hourly | daily | none
    "cache_key_private": null,     // sha256 hex (no raw query)
    "cache_key_pooled": null,      // set only when poolable
    "cached_at": null,             // ISO 8601 when a hit
    "ttl_seconds": null,
    "cost_avoided_usd": 0.0,       // provider $ saved by this hit
    "similarity": null             // semantic tier only
  },

  "quality_meta": {
    "quality_score": null,         // composite 0–1, or null when not scored
    "components": {},
    "judge_sampled": false,
    "engine_agreement_rbo": null
  },

  "usage_meta": {
    "request_id": "8f1c2e7a-…",
    "tenant_id": "…",
    "account_id": "…",
    "api_key_id": "…",
    "ts": "2026-06-14T07:48:14Z",
    "billable": true,
    "searches_counted": 1,
    "provider_key_source": "platform",  // platform (managed) | byok
    "cost_usd": 0.007
  }
}

Object shapes

SearchResult: url, title, snippet, content?, score? (0–1), source_engine, published_at?, rank (0-based).

Citation: url, title?, index (maps into the answer text).

Response headers

HeaderValue
X-Request-IdThe request UUID.
X-Cache-Tiermiss, exact_private, exact_pooled, or semantic.
X-Degradedtrue / false.
X-GR-TimingPer-stage timing, e.g. auth=2(hit);enforce=1;engine=410;write=0;total=413.

Modes and query classes

mode is your intent hint. The router classifies each query into one of five query classes that drive engine selection and cache TTLs:

Query classTriggered byDefault engine order
webmode: web / auto fallthroughserper → brave → exa
newsmode: news, freshness: fresh, or news keywordsserper → brave
academicmode: academic, or academic keywordsexa → brave → serper
answermode: answerperplexity → tavily → exa
pagemode: pagefirecrawl

In auto/web mode, lightweight keyword rules can promote a query to academic (e.g. "paper", "arxiv", "doi") or news (e.g. "latest", "today", "breaking"). Explicit modes always win over keyword heuristics. See Routing and policies.

Errors

Errors use the standard error envelope:

{ "error": { "type": "auth", "message": "…", "request_id": "…", "retryable": false } }
StatusWhen
401Missing / invalid / revoked key (auth).
402Billing block — insufficient credit, no-free-managed, free cap exceeded, or spend hard-stop (invalid_request).
403Account inactive (invalid_request).
422Invalid request body.
429Rate limit or concurrency cap (rate_limit).
500Internal error (unavailable, retryable).
200 + degraded: truePartial results — one engine failed but at least one returned. Not an error.

If all engines fail with no successful failover, the request returns an error envelope rather than partial results.

GET /v1/usage

GET /v1/usage returns a usage summary for the authenticated tenant. Detailed usage analytics are exposed through the console API under /v1/console.