openapi: 3.0.3 info: title: OpenSERP API version: 1.0.0 description: > OpenSERP provides dedicated and multi-engine search endpoints for Google, Yandex, Baidu, Bing, and DuckDuckGo. All responses are wrapped in a v1 envelope with query echo, metadata, normalized results, and pagination. Invalid client input returns 400 with a machine-readable `reason` code. license: name: MIT url: https://opensource.org/licenses/MIT servers: - url: http://127.0.0.1:7000 description: Local default server security: [] tags: - name: Search description: Dedicated per-engine search endpoints - name: Mega description: Cross-engine aggregated search endpoints - name: Health description: Health and readiness endpoints - name: Stats description: Runtime statistics endpoints - name: Docs description: OpenAPI and Swagger UI endpoints paths: /{engine}/search: get: tags: [Search] operationId: searchWeb summary: Search web results from a specific engine description: > Engine path values are `google`, `yandex`, `baidu`, `bing`, and `duck` (`duck` maps to DuckDuckGo internally). Use `?format=markdown|text|ndjson` for alternative output formats. parameters: - $ref: "#/components/parameters/EnginePath" - $ref: "#/components/parameters/TextQuery" - $ref: "#/components/parameters/LangQuery" - $ref: "#/components/parameters/DateQuery" - $ref: "#/components/parameters/FileQuery" - $ref: "#/components/parameters/SiteQuery" - $ref: "#/components/parameters/LimitQuery" - $ref: "#/components/parameters/StartQuery" - $ref: "#/components/parameters/FilterQuery" - $ref: "#/components/parameters/AnswersQuery" - $ref: "#/components/parameters/FormatQuery" - $ref: "#/components/parameters/UseProxyHeader" responses: "200": description: Search results envelope headers: X-Request-ID: $ref: "#/components/headers/XRequestID" X-Cache: $ref: "#/components/headers/XCache" X-Fallback-Engine: $ref: "#/components/headers/XFallbackEngine" X-Proxy-Mode: $ref: "#/components/headers/XProxyMode" X-Proxy-Tag: $ref: "#/components/headers/XProxyTag" X-Proxy-Used: $ref: "#/components/headers/XProxyUsed" content: application/json: schema: $ref: "#/components/schemas/SearchEnvelope" examples: default: value: query: text: golang lang: EN engines_requested: [google] meta: request_id: 01HXYZ... requested_at: "2026-04-24T12:00:00Z" took_ms: 842 engines_failed: [] version: "1.0" results: - id: s_a1b2c3d4e5f6a1b2 rank: 1 type: organic title: The Go Programming Language url: https://go.dev/ display_url: go.dev snippet: Go is an open source programming language... domain: go.dev favicon: https://go.dev/favicon.ico is_ad: false position: absolute: 1 page: 1 on_page: 1 engine: google domain_info: tld: dev sld: go is_gov: false is_edu: false is_mil: false is_news: false is_forum: false is_marketplace: false is_social: false classification: content_type: webpage source_hint: "" pagination: page: 1 has_more: true next_start: 25 text/markdown: schema: type: string text/plain: schema: type: string application/x-ndjson: schema: type: string "400": $ref: "#/components/responses/BadRequestError" "503": $ref: "#/components/responses/ServiceUnavailableError" "404": $ref: "#/components/responses/NotFoundError" "500": $ref: "#/components/responses/InternalServerError" /{engine}/image: get: tags: [Search] operationId: searchImages summary: Search image results from a specific engine parameters: - $ref: "#/components/parameters/EnginePath" - $ref: "#/components/parameters/TextQuery" - $ref: "#/components/parameters/LangQuery" - $ref: "#/components/parameters/DateQuery" - $ref: "#/components/parameters/FileQuery" - $ref: "#/components/parameters/SiteQuery" - $ref: "#/components/parameters/LimitQuery" - $ref: "#/components/parameters/StartQuery" - $ref: "#/components/parameters/FilterQuery" - $ref: "#/components/parameters/AnswersQuery" - $ref: "#/components/parameters/FormatQuery" - $ref: "#/components/parameters/UseProxyHeader" responses: "200": description: Image search results envelope headers: X-Request-ID: $ref: "#/components/headers/XRequestID" X-Cache: $ref: "#/components/headers/XCache" X-Fallback-Engine: $ref: "#/components/headers/XFallbackEngine" X-Proxy-Mode: $ref: "#/components/headers/XProxyMode" X-Proxy-Tag: $ref: "#/components/headers/XProxyTag" X-Proxy-Used: $ref: "#/components/headers/XProxyUsed" content: application/json: schema: $ref: "#/components/schemas/ImageEnvelope" "400": $ref: "#/components/responses/BadRequestError" "503": $ref: "#/components/responses/ServiceUnavailableError" "404": $ref: "#/components/responses/NotFoundError" "500": $ref: "#/components/responses/InternalServerError" /mega/search: get: tags: [Mega] operationId: megaSearch summary: Search across multiple engines in parallel description: > Results are deduplicated by normalized URL. The `clusters` field groups results that appeared in multiple engines, scored by cross-engine agreement. Partial failures are surfaced in `meta.engines_failed` rather than causing a 503. Use `?format=markdown|text|ndjson` for alternative output formats. parameters: - $ref: "#/components/parameters/TextQuery" - $ref: "#/components/parameters/LangQuery" - $ref: "#/components/parameters/DateQuery" - $ref: "#/components/parameters/FileQuery" - $ref: "#/components/parameters/SiteQuery" - $ref: "#/components/parameters/LimitQuery" - $ref: "#/components/parameters/StartQuery" - $ref: "#/components/parameters/FilterQuery" - $ref: "#/components/parameters/AnswersQuery" - $ref: "#/components/parameters/EnginesQuery" - $ref: "#/components/parameters/FormatQuery" - $ref: "#/components/parameters/UseProxyHeader" responses: "200": description: Aggregated envelope with clusters headers: X-Request-ID: $ref: "#/components/headers/XRequestID" X-Cache: $ref: "#/components/headers/XCache" X-Proxy-Mode: $ref: "#/components/headers/XProxyMode" X-Proxy-Tag: $ref: "#/components/headers/XProxyTag" X-Proxy-Used: $ref: "#/components/headers/XProxyUsed" content: application/json: schema: $ref: "#/components/schemas/MegaSearchEnvelope" text/markdown: schema: type: string text/plain: schema: type: string application/x-ndjson: schema: type: string "400": $ref: "#/components/responses/BadRequestError" "500": $ref: "#/components/responses/InternalServerError" /mega/image: get: tags: [Mega] operationId: megaImageSearch summary: Image search across multiple engines in parallel parameters: - $ref: "#/components/parameters/TextQuery" - $ref: "#/components/parameters/LangQuery" - $ref: "#/components/parameters/DateQuery" - $ref: "#/components/parameters/FileQuery" - $ref: "#/components/parameters/SiteQuery" - $ref: "#/components/parameters/LimitQuery" - $ref: "#/components/parameters/StartQuery" - $ref: "#/components/parameters/FilterQuery" - $ref: "#/components/parameters/AnswersQuery" - $ref: "#/components/parameters/EnginesQuery" - $ref: "#/components/parameters/FormatQuery" - $ref: "#/components/parameters/UseProxyHeader" responses: "200": description: Aggregated image results envelope headers: X-Request-ID: $ref: "#/components/headers/XRequestID" X-Cache: $ref: "#/components/headers/XCache" X-Proxy-Mode: $ref: "#/components/headers/XProxyMode" X-Proxy-Tag: $ref: "#/components/headers/XProxyTag" X-Proxy-Used: $ref: "#/components/headers/XProxyUsed" content: application/json: schema: $ref: "#/components/schemas/ImageEnvelope" "400": $ref: "#/components/responses/BadRequestError" "500": $ref: "#/components/responses/InternalServerError" /mega/engines: get: tags: [Mega] operationId: listMegaEngines summary: List available engines and runtime state responses: "200": description: Engine list content: application/json: schema: $ref: "#/components/schemas/MegaEnginesResponse" /health: get: tags: [Health] operationId: healthCheck summary: Service health status responses: "200": description: Healthy or degraded service content: application/json: schema: $ref: "#/components/schemas/HealthStatus" "503": description: Unhealthy service content: application/json: schema: $ref: "#/components/schemas/HealthStatus" /ready: get: tags: [Health] operationId: readinessCheck summary: Service readiness status responses: "200": description: Instance is ready content: application/json: schema: $ref: "#/components/schemas/ReadinessStatus" "503": description: Instance is draining content: application/json: schema: $ref: "#/components/schemas/ReadinessStatus" /stats: get: tags: [Stats] operationId: getStats summary: Combined cache, proxy, and circuit-breaker stats responses: "200": description: Runtime statistics content: application/json: schema: $ref: "#/components/schemas/StatsResponse" /stats/cache: get: tags: [Stats] operationId: getCacheStats summary: Cache statistics only responses: "200": description: Cache status content: application/json: schema: $ref: "#/components/schemas/CacheStats" /stats/proxy: get: tags: [Stats] operationId: getProxyStats summary: Proxy pool and per-engine proxy policy statistics responses: "200": description: Proxy stats content: application/json: schema: $ref: "#/components/schemas/ProxyStats" /stats/cb: get: tags: [Stats] operationId: getCircuitBreakerStats summary: Circuit breaker state per engine responses: "200": description: Circuit breaker stats content: application/json: schema: $ref: "#/components/schemas/CircuitBreakerStatsResponse" /openapi.yaml: get: tags: [Docs] operationId: getOpenAPISpec summary: Get raw OpenAPI YAML responses: "200": description: OpenAPI YAML content: application/yaml: schema: type: string /docs: get: tags: [Docs] operationId: getSwaggerUI summary: Swagger UI for interactive API docs responses: "200": description: HTML page loading Swagger UI from CDN content: text/html: schema: type: string components: parameters: EnginePath: name: engine in: path required: true description: Search engine endpoint alias (`duck` is DuckDuckGo). schema: type: string enum: [google, yandex, baidu, bing, duck] TextQuery: name: text in: query required: false description: > Search query text. At least one of `text`, `site`, or `file` must be non-empty. schema: type: string example: golang LangQuery: name: lang in: query required: false description: Language code (engine-specific behavior). schema: type: string example: EN DateQuery: name: date in: query required: false description: Date interval in `YYYYMMDD..YYYYMMDD` format. schema: type: string pattern: "^[0-9]{8}\\.\\.[0-9]{8}$" example: 20250101..20250131 FileQuery: name: file in: query required: false description: File extension filter (for engines that support it). schema: type: string example: PDF SiteQuery: name: site in: query required: false description: Site/domain filter. schema: type: string example: github.com LimitQuery: name: limit in: query required: false description: Maximum results to return (1–100). schema: type: integer minimum: 1 maximum: 100 default: 25 example: 10 StartQuery: name: start in: query required: false description: Pagination offset (must be >= 0). schema: type: integer minimum: 0 default: 0 example: 20 FilterQuery: name: filter in: query required: false description: Duplicate filtering flag (primarily used by Google parser behavior). schema: type: boolean default: true AnswersQuery: name: answers in: query required: false description: Include answer box style results when supported. schema: type: boolean default: false EnginesQuery: name: engines in: query required: false description: > Comma-separated engine list for mega endpoints. If omitted, all available engines are used. schema: type: string example: google,bing,duckduckgo FormatQuery: name: format in: query required: false description: > Output format. `json` (default) returns the full envelope. `markdown` returns a Markdown document suitable for Slack/email. `text` returns a minimal plain-text block optimised for LLM context windows. `ndjson` returns one result object per line with no envelope. The `Accept` header is also checked (`text/markdown`, `text/plain`, `application/x-ndjson`). schema: type: string enum: [json, markdown, text, ndjson] default: json UseProxyHeader: name: X-Use-Proxy in: header required: false description: > Request-scoped proxy override. Use `direct` to disable proxy or a tag name to force a specific proxy pool. schema: type: string examples: direct: value: direct tag: value: us headers: XRequestID: description: > UUID v7 request identifier. Matches `meta.request_id` in the response body and appears in server logs for correlation. schema: type: string XCache: description: Cache status when cache is enabled (`HIT`, `MISS`, `BYPASS`). schema: type: string enum: [HIT, MISS, BYPASS] XFallbackEngine: description: Engine name used when dedicated endpoint fallback served the response. schema: type: string XProxyMode: description: Effective proxy mode for the request (`off` or `tag_pool`). schema: type: string enum: [off, tag_pool] XProxyTag: description: Effective proxy tag when `X-Proxy-Mode=tag_pool`. schema: type: string XProxyUsed: description: Effective proxy target used (`direct`, masked URL, `pooled`, `multiple`, `mixed`). schema: type: string responses: BadRequestError: description: Invalid request parameters content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: invalidLimit: value: error: bad_request code: 400 message: "INVALID_LIMIT: limit must be between 1 and 100" reason: INVALID_LIMIT emptyQuery: value: error: bad_request code: 400 message: "EMPTY_QUERY: query cannot be empty" reason: EMPTY_QUERY ServiceUnavailableError: description: Search failed and no result could be produced content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: primaryFailed: value: error: service_unavailable code: 503 message: all search engines failed NotFoundError: description: Endpoint not found content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" InternalServerError: description: Internal error while handling request content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" schemas: # ── v1 envelope ────────────────────────────────────────────────── QueryEcho: type: object required: [text, engines_requested] properties: text: type: string example: golang lang: type: string example: EN engines_requested: type: array items: type: string example: [google] ResponseMeta: type: object required: [request_id, requested_at, took_ms, engines_failed, version] properties: request_id: type: string example: 01HXYZ... requested_at: type: string format: date-time example: "2026-04-24T12:00:00Z" took_ms: type: integer example: 842 engines_failed: type: array items: type: string example: [] version: type: string example: "1.0" Pagination: type: object required: [page, has_more, next_start] properties: page: type: integer minimum: 1 example: 1 has_more: type: boolean example: true next_start: type: integer example: 25 # ── Result ─────────────────────────────────────────────────────── Position: type: object required: [absolute, page, on_page] properties: absolute: type: integer description: 1-based rank counting from the first result of the first page. example: 1 page: type: integer example: 1 on_page: type: integer example: 1 DomainInfo: type: object required: [tld, sld, is_gov, is_edu, is_mil, is_news, is_forum, is_marketplace, is_social] properties: tld: type: string example: org sld: type: string example: wikipedia is_gov: type: boolean is_edu: type: boolean is_mil: type: boolean is_news: type: boolean is_forum: type: boolean is_marketplace: type: boolean is_social: type: boolean Classification: type: object required: [content_type, source_hint] properties: content_type: type: string enum: [article, document, video, forum_thread, webpage] example: article source_hint: type: string example: encyclopedia ResultType: type: string description: > SERP block type. New values require a minor version bump (`meta.version: "1.1"`). In v1.0, engines emit `organic`, `ad`, or `answer_box`; other enum values are reserved for future parser upgrades. enum: - organic - ad - featured_snippet - knowledge_panel - people_also_ask - video - image - news - shopping - local - answer_box Result: type: object required: - id - rank - type - title - url - display_url - snippet - domain - favicon - is_ad - position - engine properties: id: type: string description: > Stable identifier: `s_` + hex(first 8 bytes of MD5(engine|normalized_url)). Normalized URL: lowercase scheme+host, trailing slash stripped, utm_*/fbclid/gclid tracking params removed. Same URL → same ID across requests. example: s_a1b2c3d4e5f6a1b2 rank: type: integer example: 1 type: $ref: "#/components/schemas/ResultType" title: type: string example: The Go Programming Language url: type: string example: https://go.dev/ display_url: type: string description: Breadcrumb form of the URL (e.g. `go.dev › doc › install`). example: go.dev snippet: type: string example: Go is an open source programming language... domain: type: string description: Registrable domain with leading `www.` stripped. example: go.dev favicon: type: string description: Constructed as `https://{domain}/favicon.ico`. Not probed. example: https://go.dev/favicon.ico is_ad: type: boolean example: false position: $ref: "#/components/schemas/Position" engine: type: string example: google domain_info: $ref: "#/components/schemas/DomainInfo" classification: $ref: "#/components/schemas/Classification" # ── Image result ───────────────────────────────────────────────── ImageData: type: object required: [url] properties: url: type: string example: https://example.com/gopher.png thumbnail: type: string example: https://example.com/gopher-thumb.png width: type: integer example: 1200 height: type: integer example: 800 ImageSource: type: object required: [page_url, domain] properties: page_url: type: string example: https://example.com/article-about-gophers domain: type: string example: example.com ImageResult: type: object required: [id, rank, type, title, image, source, engine] properties: id: type: string description: Stable identifier prefixed with `i_`. example: i_a1b2c3d4e5f6a1b2 rank: type: integer example: 1 type: type: string enum: [image] title: type: string image: $ref: "#/components/schemas/ImageData" source: $ref: "#/components/schemas/ImageSource" engine: type: string # ── Clusters (mega only) ────────────────────────────────────────── ClusterOccurrence: type: object required: [engine, rank, result_id] properties: engine: type: string example: google rank: type: integer example: 1 result_id: type: string example: s_a1b2c3d4e5f6a1b2 Cluster: type: object required: [id, canonical_url, domain, title, occurrences, engines_count, best_rank, score] properties: id: type: string description: > Stable identifier: `c_` + hex(first 8 bytes of MD5(normalized_url)). example: c_a1b2c3d4e5f6a1b2 canonical_url: type: string example: https://go.dev/ domain: type: string example: go.dev title: type: string example: The Go Programming Language occurrences: type: array items: $ref: "#/components/schemas/ClusterOccurrence" engines_count: type: integer description: Number of engines this URL appeared in. example: 3 best_rank: type: integer description: Lowest (best) rank seen across all engines. example: 1 score: type: number format: float description: > Cross-engine agreement score: sum(1/rank for each occurrence) / engines_queried, capped at 1.0. Higher is better. example: 0.92 # ── Envelopes ───────────────────────────────────────────────────── SearchEnvelope: type: object required: [query, meta, results, pagination] properties: query: $ref: "#/components/schemas/QueryEcho" meta: $ref: "#/components/schemas/ResponseMeta" results: type: array items: $ref: "#/components/schemas/Result" pagination: $ref: "#/components/schemas/Pagination" MegaSearchEnvelope: allOf: - $ref: "#/components/schemas/SearchEnvelope" - type: object properties: clusters: type: array nullable: true items: $ref: "#/components/schemas/Cluster" description: > Cross-engine clusters, sorted by score descending. Only present on /mega/search responses. Absent (not null) on single-engine endpoints. ImageEnvelope: type: object required: [query, meta, results, pagination] properties: query: $ref: "#/components/schemas/QueryEcho" meta: $ref: "#/components/schemas/ResponseMeta" results: type: array items: $ref: "#/components/schemas/ImageResult" pagination: $ref: "#/components/schemas/Pagination" # ── Error ───────────────────────────────────────────────────────── ErrorResponse: type: object required: [error, code] properties: error: type: string description: Stable machine-readable error class. enum: [bad_request, not_found, rate_limited, service_unavailable, server_error, client_error, error] example: bad_request code: type: integer example: 400 message: type: string example: "INVALID_LIMIT: limit must be between 1 and 100" reason: type: string description: > Stable client-actionable reason code. Present on 400 errors. Known values: INVALID_LIMIT, INVALID_START, INVALID_PARAM, EMPTY_QUERY, NO_ENGINES, UNKNOWN_FORMAT. example: INVALID_LIMIT # ── Health / Stats ──────────────────────────────────────────────── EngineHealth: type: object required: [name, initialized, status] properties: name: type: string example: google initialized: type: boolean example: true status: type: string enum: [ready, not_initialized, circuit_open] HealthStatus: type: object required: [status, uptime, engines, system] properties: status: type: string enum: [healthy, degraded, unhealthy] uptime: type: string example: 1h12m3s engines: type: array items: $ref: "#/components/schemas/EngineHealth" system: type: object additionalProperties: true ReadinessStatus: type: object required: [status] properties: status: type: string enum: [ready, draining] CacheStatsEnabled: type: object required: [status, entries, hits, misses, bypasses, evictions, ttl_seconds, max_size] properties: status: type: boolean enum: [true] entries: type: integer hits: type: integer misses: type: integer bypasses: type: integer evictions: type: integer ttl_seconds: type: integer max_size: type: integer CacheStatsDisabled: type: object required: [status] properties: status: type: boolean enum: [false] CacheStats: oneOf: - $ref: "#/components/schemas/CacheStatsEnabled" - $ref: "#/components/schemas/CacheStatsDisabled" ProxyTagSummary: type: object required: [configured, healthy] properties: configured: type: integer healthy: type: integer ProxyStatsEntry: type: object required: [proxy, tags, healthy, failures, disabled] properties: proxy: type: string tags: type: array items: type: string healthy: type: boolean failures: type: integer disabled: type: boolean ProxyEngineStats: type: object required: [selected_proxy] properties: tag: type: string selected_proxy: type: string ProxyStats: type: object required: [configured_count, healthy_count, unhealthy_count, tags, entries] properties: configured_count: type: integer healthy_count: type: integer unhealthy_count: type: integer tags: type: object additionalProperties: $ref: "#/components/schemas/ProxyTagSummary" entries: type: array items: $ref: "#/components/schemas/ProxyStatsEntry" engines: type: object additionalProperties: $ref: "#/components/schemas/ProxyEngineStats" CircuitBreakerStat: type: object required: [engine, state, failure_count, last_changed] properties: engine: type: string state: type: string enum: [closed, open, half-open] failure_count: type: integer last_changed: type: string format: date-time retry_in: type: integer description: Seconds until next half-open attempt (present when state is open). CircuitBreakerStatsResponse: type: object required: [circuit_breakers] properties: circuit_breakers: type: array items: $ref: "#/components/schemas/CircuitBreakerStat" StatsResponse: type: object required: [cache, proxy, circuit_breakers] properties: cache: $ref: "#/components/schemas/CacheStats" proxy: $ref: "#/components/schemas/ProxyStats" circuit_breakers: type: array items: $ref: "#/components/schemas/CircuitBreakerStat" MegaEngineInfo: type: object required: [name, initialized] properties: name: type: string initialized: type: boolean circuit_state: type: string enum: [closed, open, half-open] MegaEnginesResponse: type: object required: [engines, total] properties: engines: type: array items: $ref: "#/components/schemas/MegaEngineInfo" total: type: integer