{
  "openapi": "3.1.0",
  "info": {
    "title": "News Search API",
    "version": "1.0.0",
    "license": {
      "name": "Proprietary",
      "url": "https://newssearchapi.com/legal/terms"
    },
    "summary": "Search and retrieve news articles from publishers worldwide.",
    "description": "The News Search API public REST API (v1).\n\n## Conventions\n- All responses are JSON (`application/json; charset=utf-8`). `HEAD` is supported on every\n  endpoint (headers only); `OPTIONS` on any path answers a CORS preflight with `204`.\n- Every response carries an `X-Request-Id` header (a 26-character ULID). Error bodies repeat it as `requestId`.\n- Success envelope: `{\"status\":\"ok\", ...}`. Error envelope:\n  `{\"status\":\"error\",\"code\":\"<ErrorCode>\",\"message\":\"<human readable>\",\"requestId\":\"<ULID>\"}`.\n- CORS: `Access-Control-Allow-Origin: *`; preflight allows methods `GET, HEAD, OPTIONS` and headers\n  `X-Api-Key, Authorization`. All rate-limit / credit / request-id headers are exposed to browsers.\n- Unknown paths return `404 routeNotFound`; methods other than GET/HEAD/OPTIONS return `405 methodNotAllowed`\n  (with an `Allow: GET, HEAD, OPTIONS` header).\n\n## Query parameter validation (all endpoints)\n- Parameters not listed for an endpoint are rejected with `400 parameterInvalid` naming the parameter\n  (`apiKey` is accepted everywhere). Unknown parameters are checked first, in alphabetical order.\n- A parameter may appear only once; pass multiple values as one comma-separated list.\n- Values are trimmed; an empty value (`?country=`) is treated as if the parameter were absent.\n- Comma-separated lists: empty items (`en,,fr`) are invalid; duplicates are removed.\n- Integers must be plain base-10 digits (no sign, decimals or exponent).\n- Validation errors are reported one at a time (the first failing check).\n\n## Authentication, rate limits and credits\nAuthenticated endpoints accept keys through either the `X-Api-Key` header or the `apiKey` query parameter.\nUse the header for server-side requests to keep keys out of URLs and logs; the query parameter is convenient\nfor browser tests but may be retained by browser history, proxies, or analytics. `Authorization: Bearer` is also\nsupported. Keys look like `gna_live_...`.\nAPI-key metadata is cached for up to 60 seconds. Sharedstack plan entitlements refresh every 5 minutes; while refresh is unavailable, the last successful entitlement snapshot is used and refreshed in the background. New accounts fail closed with 503 when Stack has no cached entitlement; an authoritative missing/inactive plan returns 403.\n\nEvery plan returns article metadata plus an excerpt of about 200 characters in `content`; no plan returns full article text. All plans allow `page_size` up to 100. Article date ranges may cover the available archive; the API does not impose a plan-based history limit. The gateway applies one uniform rate limit; the API has no per-plan rate limit. Credit holds and balances are sharedstack-owned.\n\nRequests require a valid API key and active plan; article searches reserve credits with Sharedstack before querying. The gateway applies its uniform rate limit independently of plan entitlements.\nOne credit equals USD $0.01. Each successful search costs a 1-credit base (including zero-result searches), covers the first 10 returned articles, then costs 0.1 credit per additional article. The first 30 days of search history are included; beyond that, searches cost 0.1 credit per started day. A successful lookup-by-ID costs 1 credit. A successful `/v1/counts` request costs one search base charge, whatever its range or number of queries. Errors (including 4xx/5xx) and non-article endpoints cost nothing. Search requests reserve the maximum possible page charge plus the range surcharge, then settle the actual charge using the request ID for idempotency; unused holds are released. Credit charges are reported to five decimal places. Balances are never cached by News. Article responses expose `X-Credits-Charged` and `X-Credits-Remaining` when Stack returns a balance; an insufficient-credit response may report `unknown` remaining because Stack's AppAuth error does not include balance.\n",
    "contact": {
      "name": "News Search API",
      "url": "https://newssearchapi.com/contact"
    },
    "termsOfService": "https://newssearchapi.com/legal/terms"
  },
  "servers": [
    {
      "url": "https://apis.newssearchapi.com",
      "description": "Production"
    },
    {
      "url": "https://apis.dev.newssearchapi.com",
      "description": "Development"
    }
  ],
  "tags": [
    {
      "name": "Articles",
      "description": "Search and retrieve articles (API key required)."
    },
    {
      "name": "Intelligence",
      "description": "News Intelligence aggregates — article volume trends and share of voice (API key required)."
    },
    {
      "name": "Public",
      "description": "Key-less endpoints used by the website. Not logged and not charged credits."
    }
  ],
  "security": [
    {
      "ApiKeyHeader": []
    },
    {
      "BearerAuth": []
    },
    {
      "ApiKeyQuery": []
    }
  ],
  "paths": {
    "/v1/articles": {
      "get": {
        "tags": [
          "Articles"
        ],
        "operationId": "searchArticles",
        "summary": "Search articles",
        "description": "Returns articles matching all supplied filters. `q` supports quoted phrases, AND/OR/NOT and parentheses\n(at most 10 boolean operators); search uses the isx archive keyword index. `search_in` selects title, description,\nand/or content. Content can be searched on every plan; responses return only an excerpt of it.\nWhen dates are omitted, the search defaults to the last 7 days; this default window is separate from the 30 days of history included in pricing. Ranges may extend across the available archive, with the documented surcharge applying beyond 30 days. A query the archive rejects as too broad returns `400`; narrow the range or add filters.\nPage one includes `totalResults`; later pages omit it. `dedupe=true` keeps the highest-trust article per story.\nRelevance ranks title hits above description and content, then sorts by recency.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/Q"
          },
          {
            "name": "search_in",
            "in": "query",
            "description": "Comma-separated fields to search: title, description, content. Content search is available on every plan.",
            "schema": {
              "type": "string"
            },
            "example": "title,description,content"
          },
          {
            "name": "domains",
            "in": "query",
            "description": "Include up to 10 publisher domains; matching includes subdomains.",
            "schema": {
              "type": "string"
            },
            "example": "example.com,news.example.org"
          },
          {
            "name": "exclude_domains",
            "in": "query",
            "description": "Exclude up to 10 publisher domains; matching includes subdomains.",
            "schema": {
              "type": "string"
            },
            "example": "spam.example"
          },
          {
            "name": "region",
            "in": "query",
            "description": "Case-insensitive article region filter (maximum 100 characters).",
            "schema": {
              "type": "string",
              "maxLength": 100
            },
            "example": "Europe"
          },
          {
            "name": "dedupe",
            "in": "query",
            "description": "Keep one article per story, selecting the highest trust score.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque nextPageToken from the previous response; continue after exactly the articles returned.",
            "schema": {
              "type": "string",
              "maxLength": 64
            }
          },
          {
            "$ref": "#/components/parameters/Country"
          },
          {
            "$ref": "#/components/parameters/Language"
          },
          {
            "name": "category",
            "in": "query",
            "description": "Comma-separated categories (max 10, each 1–50 characters). Matches articles having any of them (case-insensitive, exact match).",
            "schema": {
              "type": "string"
            },
            "example": "politics,world"
          },
          {
            "name": "author",
            "in": "query",
            "description": "Case-insensitive substring match on the article author (max 100 characters).",
            "schema": {
              "type": "string",
              "maxLength": 100
            },
            "example": "smith"
          },
          {
            "name": "from",
            "in": "query",
            "description": "Oldest `publishedAt` to include (inclusive), UTC. Either a date `YYYY-MM-DD` (00:00:00 UTC that day) or a\ndatetime: RFC 3339 with `Z` or an offset (URL-encode `+` as `%2B`), optional fractional seconds, or\n`YYYY-MM-DDTHH:MM[:SS]` without offset (interpreted as UTC). Must be earlier than `to`.\n",
            "schema": {
              "type": "string"
            },
            "examples": {
              "date": {
                "value": "2026-10-01"
              },
              "datetime": {
                "value": "2026-10-01T08:30:00Z"
              }
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Newest `publishedAt` to include (inclusive), UTC. Same formats as `from`; a date-only value includes the\nwhole day (up to 23:59:59.999999 UTC).\n",
            "schema": {
              "type": "string"
            },
            "example": "2026-10-31"
          },
          {
            "name": "indexed_from",
            "in": "query",
            "description": "ISO-8601 date or datetime (inclusive). Filters on `created_at`, the time this article version was last indexed. Use with `sort=indexed_desc` and the last seen `indexedAt` for incremental sync; repeat with an overlap window if your pipeline needs retries.",
            "schema": {
              "type": "string"
            },
            "example": "2026-10-31T09:00:00Z"
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Relevance (title hit > description hit > content hit, then recency), publication time, or indexed time.",
            "schema": {
              "type": "string",
              "enum": [
                "relevance",
                "published_desc",
                "published_asc",
                "indexed_desc"
              ],
              "default": "published_desc"
            }
          },
          {
            "name": "mode",
            "in": "query",
            "description": "Ranking mode. `lexical` (default) is keyword search over the whole archive. `semantic` ranks by meaning\n(embedding similarity); `hybrid` fuses lexical and semantic ranks (reciprocal rank fusion). Both require `q`,\napply every other filter, sort by relevance only (`sort` must be omitted or `relevance`), do not support\n`dedupe`, return at most the first 200 results (offset plus `page_size` ≤ 200), and omit `totalResults`.\nSemantic coverage is limited to the most recently indexed article versions; the newest articles may\nnot be embedded yet (hybrid still finds them lexically). If the semantic index has not been built or\nits provider is unavailable the request fails with `503 serviceUnavailable`; it never falls back to\nlexical.\n",
            "schema": {
              "type": "string",
              "enum": [
                "lexical",
                "semantic",
                "hybrid"
              ],
              "default": "lexical"
            },
            "example": "hybrid"
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "page_size",
            "in": "query",
            "description": "Results per page; maximum 100 for every key. `page × page_size` must be ≤ 10,000.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 10
            },
            "example": 10
          }
        ],
        "responses": {
          "200": {
            "description": "A page of articles.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Credits-Charged": {
                "$ref": "#/components/headers/X-Credits-Charged"
              },
              "X-Credits-Remaining": {
                "$ref": "#/components/headers/X-Credits-Remaining"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control-NoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ArticleList"
                },
                "examples": {
                  "page": {
                    "$ref": "#/components/examples/ArticleListOk"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/AuthenticatedBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientCredits"
          },
          "403": {
            "$ref": "#/components/responses/AuthenticatedForbiddenPlan"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/AuthenticatedTooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/AuthenticatedInternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthenticatedServiceUnavailable"
          }
        }
      }
    },
    "/v1/articles/{id}": {
      "get": {
        "tags": [
          "Articles"
        ],
        "operationId": "getArticle",
        "summary": "Get one article",
        "description": "Returns a single article by the stored `id` returned from `/v1/articles`. IDs are signed 64-bit decimal\nstrings (migrated from the URL fingerprint), so they may be negative.\nAccepts no query parameters other than `apiKey`. Any article still available in the data store can be retrieved.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^-?[0-9]{1,20}$"
            },
            "example": "-4210765838120375291"
          }
        ],
        "responses": {
          "200": {
            "description": "The article.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Credits-Charged": {
                "$ref": "#/components/headers/X-Credits-Charged"
              },
              "X-Credits-Remaining": {
                "$ref": "#/components/headers/X-Credits-Remaining"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control-NoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "article"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "ok"
                    },
                    "article": {
                      "$ref": "#/components/schemas/Article"
                    }
                  }
                },
                "examples": {
                  "excerpt": {
                    "$ref": "#/components/examples/ArticleOkTruncated"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/AuthenticatedBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientCredits"
          },
          "403": {
            "$ref": "#/components/responses/AuthenticatedForbiddenPlan"
          },
          "404": {
            "description": "No article with this id (`notFound`).",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Credits-Charged": {
                "$ref": "#/components/headers/X-Credits-Charged"
              },
              "X-Credits-Remaining": {
                "$ref": "#/components/headers/X-Credits-Remaining"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control-NoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "code": "notFound",
                  "message": "No article found with id '123'.",
                  "requestId": "01JBDX4Q8Z6M3N2P1R0S9T8V7W"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/AuthenticatedTooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/AuthenticatedInternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthenticatedServiceUnavailable"
          }
        }
      }
    },
    "/v1/counts": {
      "get": {
        "tags": [
          "Intelligence"
        ],
        "operationId": "countArticles",
        "summary": "Count articles over time",
        "description": "News Intelligence: daily or weekly article volume for 1–5 queries over the same filters, with each query's\ntop 5 sources, countries and languages, and its share of voice (`total / sum of totals × 100`, one decimal;\nall 0 when nothing matched). Use it for trend charts and to compare topics, brands or people.\nRepeat `q` once per query (the only parameter that may be repeated); each `q` uses the `/v1/articles`\nsearch syntax and the series are returned in request order. `from`/`to` are inclusive UTC days and default to the\nlast 30 days; a range may span at most 366 days. Week buckets are ISO weeks starting Monday.\nA successful request costs one search base charge, regardless of the range or number of queries; errors cost nothing.\nA query the archive rejects as too broad returns `400`; narrow the range or add filters.\n",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Search query in `/v1/articles` syntax (max 500 characters). Repeat 1–5 times for one series per query; values must be distinct.",
            "style": "form",
            "explode": true,
            "schema": {
              "type": "array",
              "minItems": 1,
              "maxItems": 5,
              "items": {
                "type": "string",
                "maxLength": 500
              }
            },
            "example": [
              "electric vehicles"
            ]
          },
          {
            "name": "from",
            "in": "query",
            "description": "First UTC day to count (inclusive), `YYYY-MM-DD`. Defaults to 29 days before `to`.",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-10-01"
          },
          {
            "name": "to",
            "in": "query",
            "description": "Last UTC day to count (inclusive), `YYYY-MM-DD`. Defaults to today (UTC).",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-10-31"
          },
          {
            "name": "interval",
            "in": "query",
            "description": "Bucket size; `week` buckets are ISO weeks dated by their Monday.",
            "schema": {
              "type": "string",
              "enum": [
                "day",
                "week"
              ],
              "default": "day"
            }
          },
          {
            "$ref": "#/components/parameters/Country"
          },
          {
            "name": "category",
            "in": "query",
            "description": "Comma-separated categories (max 10, each 1–50 characters). Matches articles having any of them (case-insensitive, exact match).",
            "schema": {
              "type": "string"
            },
            "example": "business,technology"
          },
          {
            "$ref": "#/components/parameters/Language"
          },
          {
            "name": "source",
            "in": "query",
            "description": "Comma-separated publisher domains (max 10); matching includes subdomains.",
            "schema": {
              "type": "string"
            },
            "example": "example.com,news.example.org"
          },
          {
            "name": "minTrust",
            "in": "query",
            "description": "Count only articles with a trust score of at least this value.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1
            },
            "example": 0.6
          },
          {
            "name": "dedupe",
            "in": "query",
            "description": "Count each story once.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One series per query plus share of voice.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Credits-Charged": {
                "$ref": "#/components/headers/X-Credits-Charged"
              },
              "X-Credits-Remaining": {
                "$ref": "#/components/headers/X-Credits-Remaining"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control-NoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Counts"
                },
                "examples": {
                  "compare": {
                    "$ref": "#/components/examples/CountsOk"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid parameter, or a query the archive rejects as too broad; no credits are charged.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control-NoStore"
              },
              "X-Credits-Charged": {
                "$ref": "#/components/headers/X-Credits-Charged"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "code": "parameterInvalid",
                  "message": "Parameter 'q' must be given 1 to 5 times, once per query (got 6).",
                  "requestId": "01JBDX4Q8Z6M3N2P1R0S9T8V7W"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientCredits"
          },
          "403": {
            "$ref": "#/components/responses/AuthenticatedForbiddenPlan"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/AuthenticatedTooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/AuthenticatedInternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthenticatedServiceUnavailable"
          }
        }
      }
    },
    "/v1/stats": {
      "get": {
        "tags": [
          "Public"
        ],
        "operationId": "getStats",
        "summary": "Coverage statistics",
        "description": "Live coverage numbers. No API key, not logged, coverage cached server-side for 10 minutes and ingestion/performance metrics for 60 seconds (if a refresh fails the\nprevious value keeps being served). Accepts no query parameters other than `apiKey`.\n",
        "security": [],
        "responses": {
          "200": {
            "description": "Coverage statistics.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stats"
                },
                "example": {
                  "status": "ok",
                  "sources": 412,
                  "countries": 57,
                  "languages": 31,
                  "updatedAt": "2026-11-01T06:00:00Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/v1/headlines/sample": {
      "get": {
        "tags": [
          "Public"
        ],
        "operationId": "sampleHeadlines",
        "summary": "Sample of latest headlines",
        "description": "Up to 100 most recently published articles with a non-empty title from the last 15 minutes (and not\nin the future), in descending publication order. If fewer than five are available, the response is\nsupplemented with the newest older articles needed to reach five, limited to articles published\nwithin the last 2 hours. `feedStatus: stale` indicates supplementation; if no articles are available\nwithin the 2-hour maximum age, the response is empty with `feedStatus: empty`. No all-time fallback.\nNo API key, not logged,\ncached server-side for 1 minute. Rate-limited to\n30 requests per minute per client IP (token bucket, burst 30); over the limit returns `429 rateLimited`\nwith `Retry-After`. Accepts no query parameters other than `apiKey`.\n",
        "security": [],
        "responses": {
          "200": {
            "description": "Latest headlines.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/Cache-Control"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "feedStatus",
                    "articles"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "ok"
                    },
                    "feedStatus": {
                      "type": "string",
                      "enum": [
                        "current",
                        "stale",
                        "empty"
                      ],
                      "description": "Current means no older articles were needed",
                      "stale means articles up to 2 hours old supplemented the 15-minute window": null,
                      "and empty means no articles met the 2-hour maximum age.": null
                    },
                    "articles": {
                      "type": "array",
                      "maxItems": 100,
                      "items": {
                        "$ref": "#/components/schemas/Headline"
                      }
                    }
                  }
                },
                "example": {
                  "status": "ok",
                  "feedStatus": "current",
                  "articles": [
                    {
                      "id": "-4210765838120375291",
                      "title": "Central bank holds rates steady amid cooling inflation",
                      "url": "https://www.dailyledger.example/business/rates-hold",
                      "publishedAt": "2026-11-01T05:42:10Z",
                      "indexedAt": "2026-11-01T05:42:12Z",
                      "language": "en",
                      "country": "gb",
                      "source": {
                        "name": "The Daily Ledger",
                        "domain": "dailyledger.example"
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "description": "More than 30 requests per minute from this IP (`rateLimited`); no credits are charged.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "code": "rateLimited",
                  "message": "Rate limit exceeded: this public endpoint allows 30 requests per minute per IP. Get a free API key for more.",
                  "requestId": "01JBDX4Q8Z6M3N2P1R0S9T8V7W"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/v1/publisher-icons/{domain}": {
      "get": {
        "tags": [
          "Public"
        ],
        "operationId": "getPublisherIcon",
        "summary": "Get a publisher icon",
        "description": "Fetches an enabled publisher's declared site icon using that publisher's website as the\nReferer and Origin, then caches the image for up to one year. Declared page icons are\npreferred (sizes near 48px first); Google's favicon service is the fallback, and `/favicon.ico`\nis tried last. Enabled source domains and a fixed allowlist of featured publishers are accepted.\n",
        "security": [],
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "description": "The publisher website hostname, without a scheme or path.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9.-]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Publisher icon bytes.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "example": "public, max-age=31536000, immutable"
              },
              "ETag": {
                "schema": {
                  "type": "string"
                }
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "image/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "No enabled publisher matches the requested domain.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Publisher icon request rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/health": {
      "get": {
        "tags": [
          "Public"
        ],
        "operationId": "health",
        "summary": "Health check",
        "description": "Liveness. With `deep=1` also checks the accounts DB, the sites DB and the isx archive (5 s timeout).",
        "security": [],
        "parameters": [
          {
            "name": "deep",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "0",
                "true",
                "false"
              ]
            },
            "description": "Run dependency checks (case-insensitive)."
          }
        ],
        "responses": {
          "200": {
            "description": "Healthy. `checks` is present only with `deep=1`.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                },
                "examples": {
                  "shallow": {
                    "value": {
                      "status": "ok"
                    }
                  },
                  "deep": {
                    "value": {
                      "status": "ok",
                      "checks": {
                        "newsDb": "ok",
                        "sitesDb": "ok",
                        "isx": "ok"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "503": {
            "description": "A dependency check failed (deep only).",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                },
                "example": {
                  "status": "error",
                  "code": "serviceUnavailable",
                  "message": "One or more dependencies are unavailable.",
                  "checks": {
                    "newsDb": "ok",
                    "sitesDb": "ok",
                    "isx": "error"
                  },
                  "requestId": "01JBDX4Q8Z6M3N2P1R0S9T8V7W"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "Recommended for server-side calls because keys stay out of request URLs and logs."
      },
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Also accepts API keys in the Authorization header using bearer authentication."
      },
      "ApiKeyQuery": {
        "type": "apiKey",
        "in": "query",
        "name": "apiKey",
        "description": "Convenient for browser tests; query values may be retained by browser history, proxies, or analytics."
      }
    },
    "parameters": {
      "Q": {
        "name": "q",
        "in": "query",
        "description": "Search syntax (max 500 characters): quoted phrases, AND/OR/NOT and parentheses; max 10 boolean operators.",
        "schema": {
          "type": "string",
          "maxLength": 500
        },
        "example": "\"climate summit\" OR elections"
      },
      "Country": {
        "name": "country",
        "in": "query",
        "description": "Comma-separated ISO 3166-1 alpha-2 codes (max 10, case-insensitive). Matches any of them.",
        "schema": {
          "type": "string"
        },
        "example": "us,gb"
      },
      "Language": {
        "name": "language",
        "in": "query",
        "description": "Comma-separated ISO 639-1 codes (max 10, case-insensitive). Matches any of them.",
        "schema": {
          "type": "string"
        },
        "example": "en,fr"
      },
      "Page": {
        "name": "page",
        "in": "query",
        "description": "1-based page number.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 10000,
          "default": 1
        }
      }
    },
    "headers": {
      "X-Request-Id": {
        "description": "Unique request id (ULID). Quote it when contacting support.",
        "schema": {
          "type": "string",
          "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$"
        },
        "example": "01JBDX4Q8Z6M3N2P1R0S9T8V7W"
      },
      "X-RateLimit-Limit": {
        "description": "Uniform gateway requests-per-minute limit shared by all plans.",
        "schema": {
          "type": "integer"
        },
        "example": 15
      },
      "X-RateLimit-Remaining": {
        "description": "Whole requests remaining in the gateway rate-limit window.",
        "schema": {
          "type": "integer"
        },
        "example": 29
      },
      "X-RateLimit-Reset": {
        "description": "Unix time (seconds, rounded up) when the gateway rate limit resets.",
        "schema": {
          "type": "integer"
        },
        "example": 1793512900
      },
      "X-Credits-Charged": {
        "description": "Credits debited for this response with five decimal places, including the base search charge and any history surcharge (zero for errors and non-article endpoints).",
        "schema": {
          "type": "number",
          "minimum": 0,
          "multipleOf": 0.00001
        },
        "example": 1.2
      },
      "X-Credits-Remaining": {
        "description": "Spendable credit balance from sharedstack as a decimal string; `unknown` when an insufficient-credit response does not return a balance.",
        "schema": {
          "type": "string",
          "pattern": "^(unknown|\\d+(\\.\\d{1,5})?)$"
        },
        "example": "198.00000"
      },
      "Retry-After": {
        "description": "Seconds to wait before retrying (minimum 1).",
        "schema": {
          "type": "integer",
          "minimum": 1
        },
        "example": 1
      },
      "Cache-Control": {
        "description": "`public, max-age=<seconds until the server-side cache refreshes>`.",
        "schema": {
          "type": "string"
        },
        "example": "public, max-age=412"
      },
      "Cache-Control-NoStore": {
        "description": "Authenticated responses are never stored by HTTP caches.",
        "schema": {
          "type": "string",
          "const": "no-store"
        },
        "example": "no-store"
      }
    },
    "schemas": {
      "ErrorCode": {
        "type": "string",
        "enum": [
          "parameterInvalid",
          "apiKeyMissing",
          "apiKeyInvalid",
          "apiKeyRevoked",
          "notFound",
          "routeNotFound",
          "methodNotAllowed",
          "rateLimited",
          "insufficientCredits",
          "activePlanRequired",
          "internalError",
          "serviceUnavailable"
        ]
      },
      "Error": {
        "type": "object",
        "required": [
          "status",
          "code",
          "message",
          "requestId"
        ],
        "properties": {
          "status": {
            "type": "string",
            "const": "error"
          },
          "code": {
            "$ref": "#/components/schemas/ErrorCode"
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation; names the offending parameter for 400/403."
          },
          "requestId": {
            "type": "string",
            "description": "Same value as the X-Request-Id header."
          }
        }
      },
      "ArticleSource": {
        "type": "object",
        "required": [
          "name",
          "domain"
        ],
        "description": "Publisher display name and website hostname; internal site IDs are never exposed.",
        "properties": {
          "name": {
            "type": "string",
            "example": "The Daily Ledger"
          },
          "domain": {
            "type": "string",
            "example": "dailyledger.example"
          }
        }
      },
      "Article": {
        "type": "object",
        "required": [
          "id",
          "title",
          "description",
          "content",
          "contentTruncated",
          "url",
          "imageUrl",
          "author",
          "publishedAt",
          "language",
          "country",
          "region",
          "categories",
          "source",
          "indexedAt",
          "storyId",
          "trustScore",
          "corroboratedBy"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^-?[0-9]{1,20}$",
            "description": "Stable stored article id."
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "content": {
            "type": "string",
            "description": "Plain-text excerpt of the article body (every plan). Markdown links become link text and images are omitted; stored HTML is converted to text. Paragraphs are separated by blank lines. The excerpt is about 200 characters, ending at a sentence or paragraph boundary when possible, otherwise at a word boundary, followed by an ellipsis. Full article text is not returned."
          },
          "contentTruncated": {
            "type": "boolean",
            "description": "True when `content` is an excerpt ending with an ellipsis, which is the case on every plan."
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "imageUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "author": {
            "type": [
              "string",
              "null"
            ]
          },
          "publishedAt": {
            "type": "string",
            "format": "date-time",
            "description": "RFC 3339, UTC, second precision."
          },
          "language": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 639-1 code (lowercase first two letters of the article locale)."
          },
          "country": {
            "type": [
              "string",
              "null"
            ],
            "description": "Lowercase country code."
          },
          "region": {
            "type": [
              "string",
              "null"
            ]
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "May be empty."
          },
          "source": {
            "$ref": "#/components/schemas/ArticleSource"
          },
          "indexedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Time this article version was last indexed; use with `indexed_from` for incremental sync."
          },
          "storyId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Dedupe story identifier."
          },
          "trustScore": {
            "type": [
              "number",
              "null"
            ],
            "description": "Article trust score."
          },
          "corroboratedBy": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of corroborating publishers."
          }
        }
      },
      "ArticleList": {
        "type": "object",
        "required": [
          "status",
          "page",
          "pageSize",
          "articles"
        ],
        "properties": {
          "status": {
            "type": "string",
            "const": "ok"
          },
          "totalResults": {
            "type": "integer",
            "description": "Total matches; present on page one only."
          },
          "nextPageToken": {
            "type": [
              "string",
              "null"
            ],
            "description": "Use as the cursor query parameter to continue after returned articles."
          },
          "semanticLag": {
            "type": "integer",
            "minimum": 0,
            "description": "mode=semantic|hybrid only: newest article versions not yet in the semantic index (they can still match lexically in hybrid)."
          },
          "semanticState": {
            "type": "string",
            "enum": [
              "ok",
              "indexing",
              "stalled",
              "not_built"
            ],
            "description": "mode=semantic|hybrid only: semantic index progress. stalled means indexing is far behind or not progressing."
          },
          "page": {
            "type": "integer"
          },
          "pageSize": {
            "type": "integer"
          },
          "articles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Article"
            }
          }
        }
      },
      "Counts": {
        "type": "object",
        "required": [
          "status",
          "from",
          "to",
          "interval",
          "series",
          "shareOfVoice"
        ],
        "properties": {
          "status": {
            "type": "string",
            "const": "ok"
          },
          "from": {
            "type": "string",
            "format": "date",
            "description": "First counted UTC day (inclusive)."
          },
          "to": {
            "type": "string",
            "format": "date",
            "description": "Last counted UTC day (inclusive)."
          },
          "interval": {
            "type": "string",
            "enum": [
              "day",
              "week"
            ]
          },
          "series": {
            "type": "array",
            "description": "One entry per `q`, in request order.",
            "items": {
              "type": "object",
              "required": [
                "query",
                "total",
                "buckets",
                "topSources",
                "topCountries",
                "topLanguages"
              ],
              "properties": {
                "query": {
                  "type": "string",
                  "description": "The `q` value as sent."
                },
                "total": {
                  "type": "integer",
                  "minimum": 0,
                  "description": "Matching articles in the whole range."
                },
                "buckets": {
                  "type": "array",
                  "description": "Article count per interval, oldest first.",
                  "items": {
                    "type": "object",
                    "required": [
                      "date",
                      "count"
                    ],
                    "properties": {
                      "date": {
                        "type": "string",
                        "format": "date",
                        "description": "First UTC day of the bucket (Monday for weeks)."
                      },
                      "count": {
                        "type": "integer",
                        "minimum": 0
                      }
                    }
                  }
                },
                "topSources": {
                  "type": "array",
                  "description": "Up to 5 publisher domains with the most matches.",
                  "items": {
                    "type": "object",
                    "required": [
                      "source",
                      "count"
                    ],
                    "properties": {
                      "source": {
                        "type": "string"
                      },
                      "count": {
                        "type": "integer",
                        "minimum": 0
                      }
                    }
                  }
                },
                "topCountries": {
                  "type": "array",
                  "description": "Up to 5 lowercase country codes with the most matches.",
                  "items": {
                    "type": "object",
                    "required": [
                      "country",
                      "count"
                    ],
                    "properties": {
                      "country": {
                        "type": "string"
                      },
                      "count": {
                        "type": "integer",
                        "minimum": 0
                      }
                    }
                  }
                },
                "topLanguages": {
                  "type": "array",
                  "description": "Up to 5 ISO 639-1 language codes with the most matches.",
                  "items": {
                    "type": "object",
                    "required": [
                      "language",
                      "count"
                    ],
                    "properties": {
                      "language": {
                        "type": "string"
                      },
                      "count": {
                        "type": "integer",
                        "minimum": 0
                      }
                    }
                  }
                }
              }
            }
          },
          "shareOfVoice": {
            "type": "array",
            "description": "Each query's percentage of the summed totals (one decimal; all 0 when the sum is 0).",
            "items": {
              "type": "object",
              "required": [
                "query",
                "percent"
              ],
              "properties": {
                "query": {
                  "type": "string"
                },
                "percent": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 100
                }
              }
            }
          }
        }
      },
      "Stats": {
        "type": "object",
        "required": [
          "status",
          "sources",
          "countries",
          "languages",
          "updatedAt"
        ],
        "properties": {
          "status": {
            "type": "string",
            "const": "ok"
          },
          "sources": {
            "type": "integer",
            "description": "Enabled sources."
          },
          "countries": {
            "type": "integer",
            "description": "Distinct countries among enabled sources."
          },
          "languages": {
            "type": "integer",
            "description": "Distinct article languages published in the last 30 days."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When coverage figures were computed."
          }
        }
      },
      "Headline": {
        "type": "object",
        "required": [
          "id",
          "title",
          "url",
          "publishedAt",
          "indexedAt",
          "language",
          "country",
          "source"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^-?[0-9]{1,20}$"
          },
          "title": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "publishedAt": {
            "type": "string",
            "format": "date-time"
          },
          "indexedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Time this article version was last indexed; use with `indexed_from` for incremental sync; null when unavailable."
          },
          "language": {
            "type": [
              "string",
              "null"
            ]
          },
          "country": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "type": "object",
            "required": [
              "name",
              "domain"
            ],
            "properties": {
              "name": {
                "type": "string"
              },
              "domain": {
                "type": "string",
                "description": "Publisher website hostname."
              }
            }
          }
        }
      },
      "Health": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "error"
            ]
          },
          "code": {
            "type": "string",
            "const": "serviceUnavailable",
            "description": "Present only on 503."
          },
          "message": {
            "type": "string",
            "description": "Present only on 503."
          },
          "checks": {
            "type": "object",
            "description": "Present only with `deep=1`.",
            "additionalProperties": {
              "type": "string",
              "enum": [
                "ok",
                "error"
              ]
            }
          },
          "requestId": {
            "type": "string",
            "description": "Present only on 503."
          }
        }
      }
    },
    "responses": {
      "AuthenticatedBadRequest": {
        "description": "Invalid parameter after authentication; no credits are charged.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "Cache-Control": {
            "$ref": "#/components/headers/Cache-Control-NoStore"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          },
          "X-Credits-Charged": {
            "$ref": "#/components/headers/X-Credits-Charged"
          },
          "X-Credits-Remaining": {
            "$ref": "#/components/headers/X-Credits-Remaining"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "AuthenticatedForbiddenPlan": {
        "description": "No active sharedstack plan or subscription; no credits are charged. Revoked-key responses may omit these headers.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "Cache-Control": {
            "$ref": "#/components/headers/Cache-Control-NoStore"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          },
          "X-Credits-Charged": {
            "$ref": "#/components/headers/X-Credits-Charged"
          },
          "X-Credits-Remaining": {
            "$ref": "#/components/headers/X-Credits-Remaining"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "InsufficientCredits": {
        "description": "Sharedstack could not reserve the maximum charge for this article request (`insufficientCredits`). `X-Credits-Remaining` is `unknown` if Stack does not return a balance.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "Cache-Control": {
            "$ref": "#/components/headers/Cache-Control-NoStore"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          },
          "X-Credits-Charged": {
            "$ref": "#/components/headers/X-Credits-Charged"
          },
          "X-Credits-Remaining": {
            "$ref": "#/components/headers/X-Credits-Remaining"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "status": "error",
              "code": "insufficientCredits",
              "message": "Insufficient credits: this request requires up to 2.00 credits. Add credits or request a smaller page_size.",
              "requestId": "01JBDX4Q8Z6M3N2P1R0S9T8V7W"
            }
          }
        }
      },
      "AuthenticatedTooManyRequests": {
        "description": "The uniform gateway rate limit was exceeded; no credits are charged.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "Cache-Control": {
            "$ref": "#/components/headers/Cache-Control-NoStore"
          },
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          },
          "X-Credits-Charged": {
            "$ref": "#/components/headers/X-Credits-Charged"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "AuthenticatedInternalError": {
        "description": "Unexpected server error after authentication; no credits are charged.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "Cache-Control": {
            "$ref": "#/components/headers/Cache-Control-NoStore"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          },
          "X-Credits-Charged": {
            "$ref": "#/components/headers/X-Credits-Charged"
          },
          "X-Credits-Remaining": {
            "$ref": "#/components/headers/X-Credits-Remaining"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "AuthenticatedServiceUnavailable": {
        "description": "Dependency unavailable after authentication; no credits are charged.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "Cache-Control": {
            "$ref": "#/components/headers/Cache-Control-NoStore"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          },
          "X-Credits-Charged": {
            "$ref": "#/components/headers/X-Credits-Charged"
          },
          "X-Credits-Remaining": {
            "$ref": "#/components/headers/X-Credits-Remaining"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BadRequest": {
        "description": "Invalid parameter (`parameterInvalid`) — unknown/repeated parameter, bad format or out-of-range value.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "unknown": {
                "value": {
                  "status": "error",
                  "code": "parameterInvalid",
                  "message": "Unknown parameter 'limit'. Allowed parameters: q, search_in, country, language, category, domains, exclude_domains, region, author, from, to, indexed_from, sort, dedupe, mode, page, page_size, cursor.",
                  "requestId": "01JBDX4Q8Z6M3N2P1R0S9T8V7W"
                }
              },
              "format": {
                "value": {
                  "status": "error",
                  "code": "parameterInvalid",
                  "message": "Parameter 'from' must be an ISO-8601 date (YYYY-MM-DD) or datetime (e.g. 2026-01-31T13:45:00Z); URL-encode '+' in offsets as %2B (got 'yesterday').",
                  "requestId": "01JBDX4Q8Z6M3N2P1R0S9T8V7W"
                }
              },
              "range": {
                "value": {
                  "status": "error",
                  "code": "parameterInvalid",
                  "message": "Parameter 'page_size' must be an integer between 1 and 100 (got '500').",
                  "requestId": "01JBDX4Q8Z6M3N2P1R0S9T8V7W"
                }
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing (`apiKeyMissing`) or invalid (`apiKeyInvalid`) API key.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "missing": {
                "value": {
                  "status": "error",
                  "code": "apiKeyMissing",
                  "message": "No API key provided. Send it in the X-Api-Key header, as 'Authorization: Bearer <key>', or as the apiKey query parameter.",
                  "requestId": "01JBDX4Q8Z6M3N2P1R0S9T8V7W"
                }
              },
              "invalid": {
                "value": {
                  "status": "error",
                  "code": "apiKeyInvalid",
                  "message": "The API key is invalid. Check it in your console at https://newssearchapi.com/console/keys.",
                  "requestId": "01JBDX4Q8Z6M3N2P1R0S9T8V7W"
                }
              }
            }
          }
        }
      },
      "ForbiddenRevoked": {
        "description": "The key was revoked (`apiKeyRevoked`).",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "Cache-Control": {
            "$ref": "#/components/headers/Cache-Control-NoStore"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "status": "error",
              "code": "apiKeyRevoked",
              "message": "This API key has been revoked. Create a new key in your console.",
              "requestId": "01JBDX4Q8Z6M3N2P1R0S9T8V7W"
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "Only GET, HEAD and OPTIONS are supported (`methodNotAllowed`).",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "Allow": {
            "schema": {
              "type": "string",
              "const": "GET, HEAD, OPTIONS"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "status": "error",
              "code": "methodNotAllowed",
              "message": "Method not allowed. This endpoint supports GET, HEAD and OPTIONS.",
              "requestId": "01JBDX4Q8Z6M3N2P1R0S9T8V7W"
            }
          }
        }
      },
      "InternalError": {
        "description": "Unexpected server error (`internalError`). Not billable.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "status": "error",
              "code": "internalError",
              "message": "An internal error occurred. Please retry; contact support with the requestId if it persists.",
              "requestId": "01JBDX4Q8Z6M3N2P1R0S9T8V7W"
            }
          }
        }
      },
      "ServiceUnavailable": {
        "description": "A backing service is unavailable or timed out (`serviceUnavailable`). Not billable.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "status": "error",
              "code": "serviceUnavailable",
              "message": "The service is temporarily unavailable. Please retry shortly.",
              "requestId": "01JBDX4Q8Z6M3N2P1R0S9T8V7W"
            }
          }
        }
      }
    },
    "examples": {
      "CountsOk": {
        "summary": "Two queries, weekly buckets",
        "value": {
          "status": "ok",
          "from": "2026-10-05",
          "to": "2026-10-18",
          "interval": "week",
          "series": [
            {
              "query": "electric vehicles",
              "total": 1840,
              "buckets": [
                {
                  "date": "2026-10-05",
                  "count": 910
                },
                {
                  "date": "2026-10-12",
                  "count": 930
                }
              ],
              "topSources": [
                {
                  "source": "dailyledger.example",
                  "count": 120
                },
                {
                  "source": "wire.example",
                  "count": 96
                }
              ],
              "topCountries": [
                {
                  "country": "us",
                  "count": 702
                },
                {
                  "country": "gb",
                  "count": 311
                }
              ],
              "topLanguages": [
                {
                  "language": "en",
                  "count": 1404
                },
                {
                  "language": "de",
                  "count": 188
                }
              ]
            },
            {
              "query": "hydrogen",
              "total": 460,
              "buckets": [
                {
                  "date": "2026-10-05",
                  "count": 220
                },
                {
                  "date": "2026-10-12",
                  "count": 240
                }
              ],
              "topSources": [
                {
                  "source": "wire.example",
                  "count": 41
                }
              ],
              "topCountries": [
                {
                  "country": "de",
                  "count": 150
                }
              ],
              "topLanguages": [
                {
                  "language": "en",
                  "count": 300
                },
                {
                  "language": "de",
                  "count": 120
                }
              ]
            }
          ],
          "shareOfVoice": [
            {
              "query": "electric vehicles",
              "percent": 80
            },
            {
              "query": "hydrogen",
              "percent": 20
            }
          ]
        }
      },
      "ArticleListOk": {
        "summary": "One page (excerpts)",
        "value": {
          "status": "ok",
          "totalResults": 1342,
          "page": 1,
          "pageSize": 10,
          "articles": [
            {
              "id": "-4210765838120375291",
              "title": "Central bank holds rates steady amid cooling inflation",
              "description": "Policymakers voted 7–2 to keep the benchmark rate unchanged.",
              "content": "Policymakers voted 7–2 on Thursday to keep the benchmark rate unchanged, citing a steady decline in core inflation over the past quarter. The decision was widely expected by markets. …",
              "contentTruncated": true,
              "url": "https://www.dailyledger.example/business/rates-hold",
              "imageUrl": "https://cdn.dailyledger.example/img/rates.jpg",
              "author": "Jane Smith",
              "publishedAt": "2026-10-31T09:15:00Z",
              "language": "en",
              "country": "gb",
              "region": "Europe",
              "categories": [
                "business",
                "economy"
              ],
              "source": {
                "name": "The Daily Ledger",
                "domain": "dailyledger.example"
              },
              "indexedAt": "2026-10-31T09:15:02Z",
              "storyId": "story-2026-10-31-001",
              "trustScore": 0.94,
              "corroboratedBy": 8
            }
          ]
        }
      },
      "ArticleOkTruncated": {
        "summary": "Article (excerpt of about 200 characters)",
        "value": {
          "status": "ok",
          "article": {
            "id": "-4210765838120375291",
            "title": "Central bank holds rates steady amid cooling inflation",
            "description": "Policymakers voted 7–2 to keep the benchmark rate unchanged.",
            "content": "Policymakers voted 7–2 on Thursday to keep the benchmark rate unchanged, citing a steady decline in core inflation over the past quarter. The decision was widely expected by markets. …",
            "contentTruncated": true,
            "url": "https://www.dailyledger.example/business/rates-hold",
            "imageUrl": "https://cdn.dailyledger.example/img/rates.jpg",
            "author": "Jane Smith",
            "publishedAt": "2026-10-31T09:15:00Z",
            "language": "en",
            "country": "gb",
            "region": "Europe",
            "categories": [
              "business",
              "economy"
            ],
            "source": {
              "name": "The Daily Ledger",
              "domain": "dailyledger.example"
            },
            "indexedAt": "2026-10-31T09:15:02Z",
            "storyId": "story-2026-10-31-001",
            "trustScore": 0.94,
            "corroboratedBy": 8
          }
        }
      }
    }
  }
}