{
  "openapi": "3.1.0",
  "info": {
    "title": "Grounder API",
    "version": "1",
    "summary": "Licensed sources for AI agents: one call across the listed providers, results deduplicated, each marked with the provider that found it and whether it is licensed.",
    "description": "Grounder is a web search API and MCP server for AI agents. This document covers the HTTP API: `POST /v1/search`, `POST /v1/search/{request_id}/cited`, `GET /v1/features`, `GET /v1/providers` and `GET /v1/account`. The MCP server is at https://api.grounder.net/mcp and the Tavily and Brave drop-in endpoints are under https://api.grounder.net/compat; both are described in the docs, not here. Docs: https://grounder.net/docs/api. Every docs page is also Markdown: https://grounder.net/llms.txt.",
    "termsOfService": "https://grounder.net/terms",
    "contact": {
      "name": "Grounder support",
      "email": "support@grounder.net",
      "url": "https://grounder.net/docs/api"
    }
  },
  "externalDocs": {
    "description": "API reference",
    "url": "https://grounder.net/docs/api"
  },
  "servers": [
    {
      "url": "https://api.grounder.net",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Search",
      "description": "Run a search."
    },
    {
      "name": "Providers",
      "description": "The providers a search can reach and the filters each supports."
    },
    {
      "name": "Account",
      "description": "Your own account, read with a search key."
    },
    {
      "name": "Workspaces",
      "description": "Named configurations of your account, each with its own routing settings and its own stats, all under the one search key."
    }
  ],
  "paths": {
    "/v1/search": {
      "post": {
        "operationId": "search",
        "tags": [
          "Search"
        ],
        "summary": "Search",
        "description": "Takes a query and returns one list of results, deduplicated across providers, each marked with the provider that found it (`provenance.found_by`) and whether it is licensed (`provenance.licensed`). Unless you pin providers with `requested_provider` or `providers`, Grounder chooses which providers answer. Send a search key (made under Search keys on your account) or an OAuth access token with the `search` scope in `Authorization: Bearer`.",
        "security": [
          {
            "searchKey": []
          },
          {
            "oauth": [
              "search"
            ]
          }
        ],
        "parameters": [
          {
            "name": "X-Grounder-Workspace",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The slug of the workspace to search under: the same as `workspace` in the body, which it may stand in for (sending both, they must agree). None means the default workspace."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SearchRequest"
              },
              "example": {
                "query": "what is the half-life of caffeine",
                "top_k": 5
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The results.",
            "headers": {
              "X-Cache": {
                "description": "Whether the answer came from the cache.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "HIT",
                    "MISS"
                  ]
                }
              },
              "X-RateLimit-Limit": {
                "description": "Requests allowed a minute on this key.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests left in the current minute.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "When the current minute ends, in Unix seconds.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request or a setting in it is not valid (`details` names the failing fields), or a filter the chosen provider cannot apply. `error.code` is one of `invalid_request`, `unsupported_filter`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The search key or access token is missing, invalid, revoked or expired, or the token lacks the `search` scope. `error.code` is `unauthorized`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "description": "A Bearer challenge naming the protected-resource metadata and the `search` scope, with `error=\"invalid_token\"` when a token was sent and refused (RFC 6750, 3.1).",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "A daily, monthly or per-minute limit is reached, or a provider rate-limited the search. `details` says which and when it resets. `error.code` is one of `daily_limit_reached`, `cap_reached`, `rate_limited`, `provider_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds until the limit lifts; sent with `daily_limit_reached`, `cap_reached` and `capacity_reached`.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "A failure on Grounder's side. `error.code` is `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "No provider could serve the search, free searches have reached today's limit across Grounder, a provider failed or refused it, or Grounder could not finish it. `error.code` is one of `no_provider_available`, `capacity_reached`, `provider_error`, `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds until the limit lifts; sent with `daily_limit_reached`, `cap_reached` and `capacity_reached`.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          }
        }
      }
    },
    "/v1/search/{request_id}/cited": {
      "post": {
        "operationId": "reportCitedSources",
        "tags": [
          "Search"
        ],
        "summary": "Report the sources an answer cited",
        "description": "Names which of a search's results your answer cited, by the `request_id` the search returned and the `rank` of each result the answer used (`[]` when it used none). Since 2 November 2026 Grounder does not record the report: the call is accepted and answered as described here and nothing is stored, so there is no need to make it. `urls` names the same results by address. Anything the search did not send is ignored and listed in `ignored`. A repeat gets the same answer, so the call is safe to retry. Only the account that made the search can report on it; any other `request_id` is a 404. It is not a search and counts toward no limit of one.",
        "security": [
          {
            "searchKey": []
          },
          {
            "oauth": [
              "search"
            ]
          }
        ],
        "parameters": [
          {
            "name": "request_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The `request_id` of the search, from its response."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SourcesReport"
              },
              "example": {
                "ranks": [
                  1,
                  3
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What was recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SourcesReportResult"
                }
              }
            }
          },
          "400": {
            "description": "The body is not a list of ranks. `error.code` is `invalid_request`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The search key or access token is missing, invalid, revoked or expired, or the token lacks the `search` scope. `error.code` is `unauthorized`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "description": "A Bearer challenge naming the protected-resource metadata and the `search` scope, with `error=\"invalid_token\"` when a token was sent and refused (RFC 6750, 3.1).",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "No search of this account has that `request_id`. `error.code` is `invalid_request`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 600 reports a minute. `error.code` is `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds until the limit lifts; sent with `daily_limit_reached`, `cap_reached` and `capacity_reached`.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          }
        }
      }
    },
    "/v1/features": {
      "get": {
        "operationId": "listFeatures",
        "tags": [
          "Providers"
        ],
        "summary": "List features",
        "description": "Every feature a search can ask for in `features`, and the listed providers' modes and the specialized APIs that deliver each. Needs no authorization.",
        "security": [],
        "responses": {
          "200": {
            "description": "The features.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "features"
                  ],
                  "properties": {
                    "features": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Feature"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/providers": {
      "get": {
        "operationId": "listProviders",
        "tags": [
          "Providers"
        ],
        "summary": "List providers",
        "description": "The providers a search can reach, and for each the filters it supports and the settings it takes. Needs no authorization.",
        "security": [],
        "responses": {
          "200": {
            "description": "The providers.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "providers"
                  ],
                  "properties": {
                    "providers": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Provider"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/account": {
      "get": {
        "operationId": "getAccount",
        "tags": [
          "Account"
        ],
        "summary": "Get your account",
        "description": "Who the key's account is, its free searches and credit, and its saved routing settings with what each can be set to: the answer of the MCP `user_info` tool. Send a search key. An OAuth access token is not accepted on this address, only on `POST /v1/search`, `POST /v1/search/{request_id}/cited` and the drop-in endpoints. A key from before accounts gets `user_info` alone, and any account leaves out a part that cannot be read.",
        "security": [
          {
            "searchKey": []
          }
        ],
        "parameters": [
          {
            "name": "workspace",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The slug of the workspace whose `routing_settings` are read; the default workspace when left out."
          }
        ],
        "responses": {
          "200": {
            "description": "The account.",
            "headers": {
              "Cache-Control": {
                "description": "Always `no-store`: the answer is the account's own.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "401": {
            "description": "The search key is missing, invalid or revoked, or its account is not active. An OAuth access token is refused here. `error.code` is `unauthorized`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More requests in a minute than the key allows. `details` carries `limit` and `reset_at`. `error.code` is `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/account/workspaces": {
      "get": {
        "operationId": "listWorkspaces",
        "tags": [
          "Workspaces"
        ],
        "summary": "List your workspaces",
        "description": "The account's workspaces, the default one first. A workspace is a named configuration with its own routing settings and its own stats; the account, its keys, billing and allowance are shared. Name one in a search with `workspace`.",
        "security": [
          {
            "searchKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The workspaces.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "workspaces",
                    "limit"
                  ],
                  "properties": {
                    "workspaces": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Workspace"
                      }
                    },
                    "limit": {
                      "type": "integer",
                      "description": "The most workspaces an account holds."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The key's account has no workspaces (a key from before accounts). `error.code` is `invalid_request`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The search key is missing, invalid or revoked. `error.code` is `unauthorized`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More requests in a minute than the key allows. `error.code` is `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createWorkspace",
        "tags": [
          "Workspaces"
        ],
        "summary": "Create a workspace",
        "description": "Makes a workspace: a `name`, an optional `slug` (made from the name when left out), and routing settings that start as the defaults, or as another workspace's (`copy_from`), then `routing_settings` over them. An account holds 20 at most, the default one included.",
        "security": [
          {
            "searchKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WorkspaceCreate"
              },
              "example": {
                "name": "Research",
                "slug": "research",
                "routing_settings": {
                  "providers": {
                    "exa": {
                      "use": "always"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The workspace and its settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkspaceWithSettings"
                }
              }
            }
          },
          "400": {
            "description": "The name, slug or settings are not valid, or the account holds the most workspaces. `error.code` is `invalid_request`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The search key is missing, invalid or revoked. `error.code` is `unauthorized`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The account already has a workspace with that slug. `error.code` is `invalid_request`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More requests in a minute than the key allows. `error.code` is `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/account/workspaces/{slug}": {
      "get": {
        "operationId": "getWorkspace",
        "tags": [
          "Workspaces"
        ],
        "summary": "Get a workspace",
        "description": "One workspace with its routing settings and what each can be set to.",
        "security": [
          {
            "searchKey": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The workspace's slug; `default` is the default workspace."
          }
        ],
        "responses": {
          "200": {
            "description": "The workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/WorkspaceWithSettings"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "choices": {
                          "$ref": "#/components/schemas/RoutingChoices"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The slug is not valid, or the account has no such workspace. `error.code` is `invalid_request`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The search key is missing, invalid or revoked. `error.code` is `unauthorized`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More requests in a minute than the key allows. `error.code` is `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "renameWorkspace",
        "tags": [
          "Workspaces"
        ],
        "summary": "Rename a workspace",
        "description": "Changes the display name. The slug stays, so a request that names it keeps working.",
        "security": [
          {
            "searchKey": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The workspace's slug; `default` is the default workspace."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 60
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "workspace"
                  ],
                  "properties": {
                    "workspace": {
                      "$ref": "#/components/schemas/Workspace"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The name is not valid, or the account has no such workspace. `error.code` is `invalid_request`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The search key is missing, invalid or revoked. `error.code` is `unauthorized`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More requests in a minute than the key allows. `error.code` is `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteWorkspace",
        "tags": [
          "Workspaces"
        ],
        "summary": "Delete a workspace",
        "description": "Deletes a workspace and its saved routing settings. A search that names it is refused afterwards; searches already made keep their record. The default workspace cannot be deleted.",
        "security": [
          {
            "searchKey": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The workspace's slug; `default` is the default workspace."
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "400": {
            "description": "The account has no such workspace, or it is the default workspace. `error.code` is `invalid_request`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The search key is missing, invalid or revoked. `error.code` is `unauthorized`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More requests in a minute than the key allows. `error.code` is `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/account/workspaces/{slug}/routing": {
      "put": {
        "operationId": "saveWorkspaceRouting",
        "tags": [
          "Workspaces"
        ],
        "summary": "Save a workspace's routing settings",
        "description": "Saves any part of the workspace's routing settings, in the shape a search's `routing_settings` takes; the rest keeps its saved value. Every search that names the workspace runs under them. A cap is `null` or a whole number in its range; `caps.month` counts the whole account and is set on the account page.",
        "security": [
          {
            "searchKey": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The workspace's slug; `default` is the default workspace."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RoutingSettings"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The workspace and its settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkspaceWithSettings"
                }
              }
            }
          },
          "400": {
            "description": "A setting is not valid, or the account has no such workspace. `error.code` is `invalid_request`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The search key is missing, invalid or revoked. `error.code` is `unauthorized`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More requests in a minute than the key allows. `error.code` is `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "resetWorkspaceRouting",
        "tags": [
          "Workspaces"
        ],
        "summary": "Reset a workspace's routing settings",
        "description": "Puts the workspace's routing settings back to the defaults.",
        "security": [
          {
            "searchKey": []
          }
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The workspace's slug; `default` is the default workspace."
          }
        ],
        "responses": {
          "200": {
            "description": "The workspace and its settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkspaceWithSettings"
                }
              }
            }
          },
          "400": {
            "description": "The account has no such workspace. `error.code` is `invalid_request`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The search key is missing, invalid or revoked. `error.code` is `unauthorized`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More requests in a minute than the key allows. `error.code` is `rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "searchKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "A search key, made under Search keys on your Grounder account (https://grounder.net/account/keys) and shown once: `Authorization: Bearer gr-…`. It searches as the account does, under its free searches and credit."
      },
      "oauth": {
        "type": "oauth2",
        "description": "OAuth 2.0 authorization code with PKCE (S256), as a public client. Read https://api.grounder.net/.well-known/oauth-protected-resource/v1/search and the authorization server metadata it names, register a client at https://api.grounder.net/oauth/register (`token_endpoint_auth_method` `none`), then authorize with `scope=search` and `resource=https://api.grounder.net/v1/search`. The account signs in and allows the client. Send the access token as `Authorization: Bearer <access_token>`. A token for the MCP server does not authorize search, and a refresh token is never sent as an access token. A client ends its sign-in by posting the refresh token and its `client_id` to https://api.grounder.net/oauth/revoke (RFC 7009; the metadata names it as `revocation_endpoint`).",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://api.grounder.net/oauth/authorize",
            "tokenUrl": "https://api.grounder.net/oauth/token",
            "refreshUrl": "https://api.grounder.net/oauth/token",
            "scopes": {
              "search": "Call POST /v1/search."
            }
          }
        }
      }
    },
    "schemas": {
      "Account": {
        "type": "object",
        "required": [
          "user_info"
        ],
        "description": "Your account, as the MCP `user_info` tool answers. `free_searches`, `credit`, `workspace`, `workspaces`, `routing_settings` and `routing_choices` are there for an account made by signing in.",
        "properties": {
          "user_info": {
            "description": "The account's profile, or null when it has none.",
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/AccountProfile"
              }
            ]
          },
          "free_searches": {
            "$ref": "#/components/schemas/FreeSearches"
          },
          "credit": {
            "$ref": "#/components/schemas/Credit"
          },
          "workspace": {
            "type": "string",
            "description": "The slug of the workspace `routing_settings` belong to: the one asked for with `?workspace=`, else `default`."
          },
          "workspaces": {
            "type": "array",
            "description": "The account's workspaces.",
            "items": {
              "$ref": "#/components/schemas/Workspace"
            }
          },
          "routing_settings": {
            "$ref": "#/components/schemas/AccountRoutingSettings"
          },
          "routing_choices": {
            "$ref": "#/components/schemas/RoutingChoices"
          }
        }
      },
      "Workspace": {
        "type": "object",
        "required": [
          "slug",
          "name",
          "default",
          "created_at",
          "updated_at",
          "mode"
        ],
        "description": "A named configuration of the account: its own routing settings and its own stats, searched with the account's one key.",
        "properties": {
          "slug": {
            "type": "string",
            "pattern": "^[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?$",
            "description": "Its short id, the value of `workspace` in a search. The default workspace's is `default`."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60
          },
          "default": {
            "type": "boolean",
            "description": "The workspace a search that names none uses; it cannot be deleted."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When its routing settings were last saved; null for the defaults."
          },
          "mode": {
            "type": "string",
            "enum": [
              "optimized",
              "preferred"
            ],
            "description": "`optimized` while every provider, mode and specialized API is on, else `preferred`."
          }
        }
      },
      "WorkspaceWithSettings": {
        "type": "object",
        "required": [
          "workspace",
          "settings"
        ],
        "properties": {
          "workspace": {
            "$ref": "#/components/schemas/Workspace"
          },
          "settings": {
            "$ref": "#/components/schemas/AccountRoutingSettings"
          }
        }
      },
      "WorkspaceCreate": {
        "type": "object",
        "required": [
          "name"
        ],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60
          },
          "slug": {
            "type": "string",
            "pattern": "^[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?$",
            "description": "Left out, it is made from the name."
          },
          "copy_from": {
            "type": "string",
            "description": "The slug of a workspace whose routing settings this one starts with."
          },
          "routing_settings": {
            "$ref": "#/components/schemas/RoutingSettings"
          }
        }
      },
      "AccountProfile": {
        "type": "object",
        "required": [
          "account_id",
          "email",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "account_id": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "routing_profile": {
            "type": "object",
            "properties": {
              "user_type": {
                "type": "string",
                "enum": [
                  "unknown",
                  "individual",
                  "researcher",
                  "developer",
                  "journalist",
                  "business",
                  "student"
                ]
              },
              "country": {
                "type": "string",
                "pattern": "^[A-Z]{2}$"
              },
              "language": {
                "type": "string",
                "pattern": "^[a-z]{2,3}(?:-[A-Z]{2})?$"
              }
            }
          },
          "signup_data": {
            "type": "object",
            "description": "What the account said at sign-up, if anything."
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": "string"
          }
        }
      },
      "FreeSearches": {
        "type": "object",
        "required": [
          "daily_limit",
          "used_today",
          "left_today",
          "resets_at"
        ],
        "description": "The account's free searches in the current UTC day.",
        "properties": {
          "daily_limit": {
            "type": "integer",
            "minimum": 0
          },
          "used_today": {
            "type": "integer",
            "minimum": 0
          },
          "left_today": {
            "type": "integer",
            "minimum": 0
          },
          "resets_at": {
            "type": "string",
            "format": "date-time",
            "description": "The next midnight UTC, when the count starts again."
          }
        }
      },
      "Credit": {
        "type": "object",
        "required": [
          "currency",
          "balance_cents",
          "purchased_cents",
          "price_per_1000_cents",
          "searches_left"
        ],
        "description": "The credit that pays for searches past the free ones.",
        "properties": {
          "currency": {
            "type": "string",
            "const": "usd"
          },
          "balance_cents": {
            "type": "integer",
            "minimum": 0
          },
          "purchased_cents": {
            "type": "integer",
            "minimum": 0
          },
          "price_per_1000_cents": {
            "type": "integer",
            "minimum": 1,
            "description": "The price of 1,000 searches past the free ones."
          },
          "searches_left": {
            "type": "integer",
            "minimum": 0,
            "description": "About how many searches past the free ones the balance pays for."
          }
        }
      },
      "AccountRoutingSettings": {
        "type": "object",
        "required": [
          "providers",
          "fallback",
          "specialized",
          "caps"
        ],
        "description": "The account's saved routing settings (the defaults when none are saved), in the shape a search's `routing_settings` takes: send any part of it to change that part for one search.",
        "properties": {
          "providers": {
            "type": "object",
            "description": "Settings by provider, keyed by `provider`.",
            "additionalProperties": {
              "type": "object",
              "required": [
                "use",
                "modes"
              ],
              "properties": {
                "use": {
                  "type": "string",
                  "enum": [
                    "off",
                    "auto",
                    "always"
                  ],
                  "description": "Never, when routing picks it, or on every search."
                },
                "modes": {
                  "type": "object",
                  "description": "Settings by search mode of the provider.",
                  "additionalProperties": {
                    "type": "string",
                    "enum": [
                      "off",
                      "auto",
                      "always"
                    ]
                  }
                }
              }
            }
          },
          "fallback": {
            "type": "boolean"
          },
          "specialized": {
            "type": "object",
            "description": "Settings by specialized API.",
            "additionalProperties": {
              "type": "object",
              "required": [
                "use"
              ],
              "properties": {
                "use": {
                  "type": "string",
                  "enum": [
                    "off",
                    "auto",
                    "always"
                  ]
                }
              }
            }
          },
          "caps": {
            "type": "object",
            "required": [
              "search",
              "month"
            ],
            "description": "Limits; null is no limit.",
            "properties": {
              "search": {
                "type": "object",
                "description": "On each search: a whole number in its range.",
                "properties": {
                  "searches": {
                    "oneOf": [
                      {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 10
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "sources": {
                    "oneOf": [
                      {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 50
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                }
              },
              "month": {
                "type": "object",
                "description": "On each UTC month of searches.",
                "properties": {
                  "searches": {
                    "oneOf": [
                      {
                        "type": "integer",
                        "minimum": 1
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                }
              }
            }
          }
        }
      },
      "RoutingChoices": {
        "type": "object",
        "description": "What each routing setting can be set to, the steps the account page offers. Read it to build a search's `routing_settings`; the steps are not listed in this document.",
        "properties": {
          "providers": {
            "type": "array",
            "description": "The listed providers and the modes of each.",
            "items": {
              "type": "object"
            }
          },
          "uses": {
            "type": "array",
            "description": "The values `use` can take.",
            "items": {
              "type": "string"
            }
          },
          "specialized": {
            "type": "array",
            "description": "The specialized APIs.",
            "items": {
              "type": "object"
            }
          },
          "cap_steps": {
            "type": "object",
            "description": "The numbers the account page lists for each search cap, beside its Custom entry."
          },
          "cap_range": {
            "type": "object",
            "description": "The whole numbers each search cap takes: `{ searches: { min, max }, sources: { min, max } }`."
          }
        }
      },
      "SearchRequest": {
        "type": "object",
        "required": [
          "query"
        ],
        "properties": {
          "query": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2000,
            "description": "The search query."
          },
          "top_k": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "default": 10,
            "description": "Results wanted after merging."
          },
          "filters": {
            "$ref": "#/components/schemas/SearchFilters"
          },
          "include_text": {
            "type": "boolean",
            "default": true,
            "description": "Include passage text with each result. Turn it off to keep a response small."
          },
          "text_max_chars": {
            "type": "integer",
            "minimum": 200,
            "maximum": 100000,
            "description": "The most characters of each result's `text` to return. Left out, `text` is the whole text, as it always was. Set, `text` is the passages of that result that best match the query and fit, in the text's own order with ` … ` between passages that were not neighbours; a text that fits stays whole. A re-read result keeps its \"Live as of\" or \"Read at\" note, and a page you ask for with `urls` keeps its opening instead of passages. It narrows `text` and changes nothing else: which providers are called, what a search costs and the rest of the response are the same. With `include_text` false there is no text to narrow."
          },
          "requested_provider": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40,
            "description": "Pin one provider for this query: a `provider` from GET /v1/providers."
          },
          "providers": {
            "type": "array",
            "minItems": 1,
            "maxItems": 32,
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 40
            },
            "description": "Fan out to exactly these providers and merge. Each is a `provider` from GET /v1/providers, called once however often it is named; a name that is not one is refused with 400 before anything is called. A free search may name only providers whose call costs no more than a search does; credit pays for any."
          },
          "realtime": {
            "type": "boolean",
            "description": "For callers that need one quick answer (voice, autocomplete): the search makes one call, for its first needed feature."
          },
          "features": {
            "type": "array",
            "minItems": 1,
            "maxItems": 10,
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 40
            },
            "description": "The features the answer needs, by `id` from GET /v1/features. Named, they are the search's needed features, each answered by a call to the provider that delivers it, and Grounder does not second-guess them; absent, Grounder finds them from the query. An unknown id is a 400 `invalid_request`."
          },
          "urls": {
            "type": "array",
            "minItems": 1,
            "maxItems": 10,
            "items": {
              "type": "string",
              "format": "uri",
              "maxLength": 2000,
              "pattern": "^https?://"
            },
            "description": "Pages to read, as http(s) addresses. A page reader that gets past sites blocking a plain fetch returns each page's text, with the licensed walk applied to the reader that fetched it. Leave it out and the http(s) addresses in the prompt or the query are read the same way."
          },
          "routing_settings": {
            "$ref": "#/components/schemas/RoutingSettings"
          },
          "workspace": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "description": "The slug of the workspace to search under: a named configuration of your account with its own routing settings and its own stats (`GET /v1/account/workspaces`). Left out, the default workspace. A workspace the account does not have is a 400, never the default in its place. The `X-Grounder-Workspace` header says the same."
          },
          "purpose": {
            "type": "string",
            "description": "Why the user wants this search, in a few words (\"choosing running shoes for flat feet\"). Optional, and accepted so a client that sends it keeps working: since 2 November 2026 Grounder does not keep it and does not ask for it, so passing one changes nothing."
          },
          "context": {
            "$ref": "#/components/schemas/SearchContext"
          }
        }
      },
      "SearchFilters": {
        "type": "object",
        "description": "A filter the chosen provider cannot apply is refused with 400 `unsupported_filter`, not dropped. See `capabilities` in GET /v1/providers.",
        "properties": {
          "domains": {
            "type": "object",
            "description": "Include or exclude these domains.",
            "required": [
              "values"
            ],
            "properties": {
              "values": {
                "type": "array",
                "minItems": 1,
                "maxItems": 200,
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 253
                }
              },
              "mode": {
                "type": "string",
                "enum": [
                  "include",
                  "exclude"
                ],
                "default": "include"
              }
            }
          },
          "publisher_ids": {
            "type": "object",
            "description": "Licensed-publisher providers only: publishers to include or exclude.",
            "required": [
              "values"
            ],
            "properties": {
              "values": {
                "type": "array",
                "minItems": 1,
                "maxItems": 200,
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 253
                }
              },
              "mode": {
                "type": "string",
                "enum": [
                  "include",
                  "exclude"
                ],
                "default": "include"
              }
            }
          },
          "article_types": {
            "type": "object",
            "description": "Licensed-publisher providers only: article types to include or exclude.",
            "required": [
              "values"
            ],
            "properties": {
              "values": {
                "type": "array",
                "minItems": 1,
                "maxItems": 200,
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 253
                }
              },
              "mode": {
                "type": "string",
                "enum": [
                  "include",
                  "exclude"
                ],
                "default": "include"
              }
            }
          },
          "temporal_sensitivity": {
            "type": "object",
            "description": "Licensed-publisher providers only: temporal sensitivities to include or exclude.",
            "required": [
              "values"
            ],
            "properties": {
              "values": {
                "type": "array",
                "minItems": 1,
                "maxItems": 200,
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 253
                }
              },
              "mode": {
                "type": "string",
                "enum": [
                  "include",
                  "exclude"
                ],
                "default": "include"
              }
            }
          },
          "recency": {
            "description": "Limit results by date: the last N days (`rolling`) or a date `range`.",
            "oneOf": [
              {
                "type": "object",
                "required": [
                  "mode",
                  "days"
                ],
                "properties": {
                  "mode": {
                    "const": "rolling"
                  },
                  "days": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 3650
                  }
                }
              },
              {
                "type": "object",
                "required": [
                  "mode"
                ],
                "properties": {
                  "mode": {
                    "const": "range"
                  },
                  "from": {
                    "type": "string",
                    "format": "date"
                  },
                  "to": {
                    "type": "string",
                    "format": "date"
                  }
                }
              }
            ]
          },
          "language": {
            "type": "string",
            "minLength": 2,
            "maxLength": 8
          }
        }
      },
      "RoutingSettings": {
        "type": "object",
        "description": "Your account's routing settings for this search only, over the saved ones; nothing is saved. A part you send replaces the saved value for this search and the rest keeps it. Needs a Grounder account. An unknown key, or a value outside what its setting takes (a cap outside its range, say), is a 400 `invalid_request`. A search may tighten the account's caps with `caps.search`; `caps.month` is refused. Every result is licensed, whatever the settings say.",
        "properties": {
          "providers": {
            "type": "object",
            "description": "Settings by provider, keyed by `provider`.",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "use": {
                  "type": "string",
                  "enum": [
                    "off",
                    "auto",
                    "always"
                  ],
                  "description": "Never, when routing picks it, or on every search."
                },
                "modes": {
                  "type": "object",
                  "description": "Settings by search mode of the provider.",
                  "additionalProperties": {
                    "type": "string",
                    "enum": [
                      "off",
                      "auto",
                      "always"
                    ]
                  }
                }
              }
            }
          },
          "fallback": {
            "type": "boolean"
          },
          "specialized": {
            "type": "object",
            "description": "Settings by specialized API.",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "use": {
                  "type": "string",
                  "enum": [
                    "off",
                    "auto",
                    "always"
                  ]
                }
              }
            }
          },
          "caps": {
            "type": "object",
            "description": "Only `search` may be set on a search.",
            "properties": {
              "search": {
                "type": "object",
                "properties": {
                  "searches": {
                    "oneOf": [
                      {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 10
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "sources": {
                    "oneOf": [
                      {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 50
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                }
              }
            }
          }
        }
      },
      "SourcesReport": {
        "type": "object",
        "required": [
          "ranks"
        ],
        "properties": {
          "ranks": {
            "type": "array",
            "maxItems": 100,
            "items": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            },
            "description": "The `rank` of each result the answer cited; empty when it cited none."
          },
          "urls": {
            "type": "array",
            "maxItems": 100,
            "items": {
              "type": "string",
              "format": "uri",
              "maxLength": 2000
            },
            "description": "The same results by address, instead of or beside their ranks."
          }
        }
      },
      "SourcesReportResult": {
        "type": "object",
        "required": [
          "request_id",
          "sent",
          "cited",
          "ignored"
        ],
        "properties": {
          "request_id": {
            "type": "string"
          },
          "sent": {
            "type": "integer",
            "minimum": 0,
            "description": "How many results the search sent."
          },
          "cited": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "The ranks named that the search sent, ascending."
          },
          "ignored": {
            "type": "array",
            "items": {
              "oneOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            },
            "description": "The ranks and addresses named that the search did not send."
          }
        }
      },
      "SearchContext": {
        "type": "object",
        "description": "What the calling agent knows about the user and the conversation.",
        "properties": {
          "conversation_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "description": "A unique identifier for the chat, reused for the whole chat."
          },
          "user_message_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "description": "One stable ID for this actual user prompt; reuse it across searches and retries, use a new ID for the next user prompt."
          },
          "previous_request_id": {
            "type": "string",
            "format": "uuid"
          },
          "root_query_id": {
            "type": "string",
            "format": "uuid",
            "description": "An explicitly linked existing query thread."
          },
          "prompt_content": {
            "type": "string",
            "minLength": 1,
            "maxLength": 20000,
            "description": "The user's words, as recorded for the request in place of the query."
          },
          "user_location": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "Where the user is (\"Austin, Texas\"), only when the user said so. Never a guess."
          },
          "user_time_zone": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "description": "The user's time zone as an IANA name (\"America/Chicago\"), only when the user said so. Never a guess."
          },
          "agent": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100
              },
              "model": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100
              },
              "version": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100
              }
            }
          },
          "metadata": {
            "type": "object",
            "description": "Any JSON object, at most 16 KiB and six container levels deep."
          }
        }
      },
      "SearchResponse": {
        "type": "object",
        "required": [
          "query",
          "results",
          "routing",
          "usage",
          "request_id"
        ],
        "properties": {
          "query": {
            "type": "string"
          },
          "results": {
            "type": "array",
            "description": "In final order, best first. Every result is licensed.",
            "items": {
              "$ref": "#/components/schemas/SearchResult"
            }
          },
          "features": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The features the search used: the needed features its calls were made for, web and specialized, in the order they were found (ids from GET /v1/features)."
          },
          "routing": {
            "type": "object",
            "required": [
              "policy",
              "calls"
            ],
            "properties": {
              "policy": {
                "type": "string",
                "description": "How the calls were chosen: `ranked` for a search that names no provider (one call per needed feature, to the provider allowed on the most pages for it), `fanout` for one that names providers."
              },
              "calls": {
                "type": "array",
                "description": "What each provider did, in provider order.",
                "items": {
                  "$ref": "#/components/schemas/ProviderCall"
                }
              },
              "live_calls": {
                "type": "array",
                "description": "The specialized APIs asked for a value only a data feed holds now (odds, a game, a flight), when the search needed one.",
                "items": {
                  "$ref": "#/components/schemas/ProviderCall"
                }
              }
            }
          },
          "usage": {
            "type": "object",
            "required": [
              "latency_ms",
              "cached",
              "licensed_result_count"
            ],
            "properties": {
              "latency_ms": {
                "type": "integer",
                "minimum": 0
              },
              "cached": {
                "type": "boolean"
              },
              "licensed_result_count": {
                "type": "integer",
                "minimum": 0
              }
            }
          },
          "request_id": {
            "type": "string"
          },
          "workspace": {
            "type": "string",
            "description": "The workspace the search ran under; `default` when it named none. Sent for a search by a Grounder account."
          },
          "root_query_id": {
            "type": "string",
            "format": "uuid",
            "description": "The initial query root of this chat."
          },
          "response_count": {
            "type": "integer",
            "minimum": 0
          },
          "historical_count_known": {
            "type": "boolean"
          },
          "counting_started_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SearchResult": {
        "type": "object",
        "required": [
          "id",
          "url",
          "domain",
          "canonical_url",
          "title",
          "text",
          "score",
          "rank",
          "provenance"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable per-provider identity; for web results the URL."
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "domain": {
            "type": "string",
            "description": "The hostname without a leading `www.`."
          },
          "canonical_url": {
            "type": "string",
            "description": "The canonical form of the URL, the key results are deduplicated on."
          },
          "title": {
            "type": "string"
          },
          "text": {
            "type": "string",
            "description": "The result's text: whole, or its best-matching passages when `text_max_chars` was set. Empty when `include_text` was false."
          },
          "summary": {
            "type": "string"
          },
          "section_heading": {
            "type": "string"
          },
          "published_date": {
            "type": "string",
            "description": "ISO 8601 date, YYYY-MM-DD."
          },
          "score": {
            "type": "number",
            "description": "The provider's own relevance, as it reported it. Not comparable across providers; `rank` is the merged order."
          },
          "rank": {
            "type": "integer",
            "minimum": 1,
            "description": "The final position after merging, starting at 1."
          },
          "article_type": {
            "type": "string"
          },
          "temporal_sensitivity": {
            "type": "string"
          },
          "language": {
            "type": "string"
          },
          "provenance": {
            "$ref": "#/components/schemas/Provenance"
          }
        }
      },
      "Provenance": {
        "type": "object",
        "required": [
          "provider",
          "found_by",
          "licensed",
          "promoted"
        ],
        "properties": {
          "provider": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40,
            "description": "The provider whose record is served for this result."
          },
          "found_by": {
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 40
            },
            "description": "Every provider that returned this URL."
          },
          "licensed": {
            "type": "boolean",
            "description": "True for a result no site's rules or robots.txt block for the provider that found it."
          },
          "publisher_id": {
            "type": "string",
            "description": "Set when `licensed`, identifying the owning publisher where one licenses it."
          },
          "publisher_name": {
            "type": "string"
          },
          "promoted": {
            "type": "string",
            "enum": [
              "publisher_precedence",
              "none"
            ],
            "description": "`publisher_precedence` means another provider found the URL first but a licensed publisher owns it, so the publisher's record was served in its place."
          },
          "features": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The needed features of the call whose record is served; absent when that call was made for none."
          },
          "licensed_basis": {
            "type": "string",
            "enum": [
              "publisher-license",
              "robots-allowed",
              "data-feed"
            ],
            "description": "Why it may be served: `publisher-license` (a licensed publisher's own content), `robots-allowed` (the site's rules and robots.txt allow the provider that found it) or `data-feed` (a specialized API's own data)."
          },
          "publisher": {
            "type": "object",
            "required": [
              "name",
              "domain"
            ],
            "description": "Whose content it is: `domain` is the site's registrable domain, `name` the licensing publisher's name where one licenses it, else the domain.",
            "properties": {
              "name": {
                "type": "string"
              },
              "domain": {
                "type": "string"
              }
            }
          }
        }
      },
      "ProviderCall": {
        "type": "object",
        "required": [
          "provider",
          "status",
          "latency_ms",
          "result_count"
        ],
        "properties": {
          "provider": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40
          },
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "error",
              "timeout",
              "skipped",
              "rate_limited"
            ]
          },
          "latency_ms": {
            "type": "integer",
            "minimum": 0
          },
          "result_count": {
            "type": "integer",
            "minimum": 0
          },
          "mode": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40,
            "description": "The provider's search mode this call ran in; absent for its default search."
          },
          "results_requested": {
            "type": "integer",
            "minimum": 1,
            "description": "Results the provider was asked for; absent when it was skipped before any request."
          },
          "unique_result_count": {
            "type": "integer",
            "minimum": 0,
            "description": "Valid canonical URLs, counted once within this provider's response."
          },
          "fresh_result_count": {
            "type": "integer",
            "minimum": 0,
            "description": "Unique dated URLs inside the requested or classified freshness window."
          },
          "cost_units": {
            "type": "number",
            "minimum": 0,
            "description": "The provider-reported cost, when known, in `cost_unit`."
          },
          "cost_unit": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40,
            "description": "The provider's native unit; costs in different units are not comparable."
          },
          "error_code": {
            "type": "string"
          },
          "features": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The needed features this call was made for; absent for a call made for none (a provider set to Always, or one the search named)."
          }
        }
      },
      "Feature": {
        "type": "object",
        "required": [
          "id",
          "description",
          "inputs",
          "returns",
          "offerings",
          "specialized"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Lowercase and hyphenated, stable once published; what a search names in `features`."
          },
          "description": {
            "type": "string",
            "description": "What a caller gets, in plain words."
          },
          "inputs": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "query",
                "days",
                "domains",
                "language",
                "urls",
                "location"
              ]
            },
            "description": "The request inputs it honours."
          },
          "returns": {
            "type": "string",
            "description": "The shape of its results."
          },
          "offerings": {
            "type": "array",
            "description": "The listed providers' modes that deliver it.",
            "items": {
              "type": "object",
              "required": [
                "provider",
                "mode"
              ],
              "properties": {
                "provider": {
                  "type": "string"
                },
                "mode": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "specialized": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The specialized APIs that deliver it."
          }
        }
      },
      "Provider": {
        "type": "object",
        "properties": {
          "provider": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40
          },
          "display_name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "retrieval",
              "model_tool",
              "none"
            ]
          },
          "capabilities": {
            "type": "object",
            "description": "Which filters the provider can apply, and whether it returns full text or finds similar pages.",
            "properties": {
              "domain_filter": {
                "type": "boolean"
              },
              "date_window": {
                "type": "boolean"
              },
              "publisher_filter": {
                "type": "boolean"
              },
              "article_type_filter": {
                "type": "boolean"
              },
              "temporal_sensitivity_filter": {
                "type": "boolean"
              },
              "language_filter": {
                "type": "boolean"
              },
              "full_text": {
                "type": "boolean"
              },
              "similar": {
                "type": "boolean"
              }
            }
          },
          "settings_schema": {
            "type": "object",
            "description": "The settings the provider takes, by name.",
            "additionalProperties": {
              "type": "object",
              "required": [
                "type"
              ],
              "properties": {
                "type": {
                  "type": "string",
                  "enum": [
                    "integer",
                    "number",
                    "string",
                    "boolean"
                  ]
                },
                "default": {},
                "enum": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "min": {
                  "type": "number"
                },
                "max": {
                  "type": "number"
                }
              }
            }
          },
          "licensed": {
            "type": "boolean",
            "description": "True only for a provider that serves content Grounder holds a licence to."
          },
          "active": {
            "type": "boolean"
          },
          "configured": {
            "type": "boolean",
            "description": "Whether this deployment holds working credentials for the provider."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "request_id": {
            "type": "string",
            "description": "On a search that failed after it started: quote it when you write to support."
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "unauthorized",
                  "invalid_request",
                  "unsupported_filter",
                  "no_provider_available",
                  "provider_error",
                  "rate_limited",
                  "daily_limit_reached",
                  "capacity_reached",
                  "cap_reached",
                  "billing_unavailable",
                  "already_subscribed",
                  "internal_error"
                ],
                "description": "The machine-readable name; branch on it, not on the status."
              },
              "message": {
                "type": "string"
              },
              "details": {
                "description": "Present when there is more to say: the failing fields, the limit and what was used, or when it resets."
              }
            }
          }
        }
      }
    }
  }
}
