> ## Documentation Index
> Fetch the complete documentation index at: https://docs.teamduo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Run a read-only query

> Runs one read-only query against an exposed datasource. The body shape depends on the datasource type: `sql` for PostgreSQL or BigQuery, `collection`+`filter` (optionally `pipeline`) for MongoDB. Every query is checked against this datasource's exposure rules before it runs; anything outside it is refused by name — see [Security model](/reference/security).

Budgeted per team rather than per caller: every MCP query and every API-key query share one rate limit, because an IP or key bucket can't tell a busy team from a runaway agent.



## OpenAPI

````yaml /openapi.json post /teams/{teamId}/datasources/{id}/query
openapi: 3.1.0
info:
  title: TeamDuo REST API
  version: 1.0.0
  description: >-
    The subset of TeamDuo's REST API that an API key may call. Most of TeamDuo's
    backend is session-only by design — connecting a database, changing
    exposure, and managing your team all require a signed-in browser session.
    These five endpoints are the ones a key can reach, matching the read-only
    surface the MCP connector uses. See [Overview](/api-reference/introduction)
    before using this reference.
servers:
  - url: https://api.teamduo.ai
    description: Production
security:
  - bearerAuth: []
paths:
  /teams/{teamId}/datasources/{id}/query:
    post:
      tags:
        - Datasources
      summary: Run a read-only query
      description: >-
        Runs one read-only query against an exposed datasource. The body shape
        depends on the datasource type: `sql` for PostgreSQL or BigQuery,
        `collection`+`filter` (optionally `pipeline`) for MongoDB. Every query
        is checked against this datasource's exposure rules before it runs;
        anything outside it is refused by name — see [Security
        model](/reference/security).


        Budgeted per team rather than per caller: every MCP query and every
        API-key query share one rate limit, because an IP or key bucket can't
        tell a busy team from a runaway agent.
      operationId: runQuery
      parameters:
        - name: teamId
          in: path
          required: true
          schema:
            type: string
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Datasource id from `GET /teams/{teamId}/datasources`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/SqlQuery'
                - $ref: '#/components/schemas/FindQuery'
                - $ref: '#/components/schemas/AggregateQuery'
            examples:
              sql:
                summary: PostgreSQL / BigQuery
                value:
                  sql: SELECT status, count(*) FROM public.orders GROUP BY 1
                  limit: 100
              find:
                summary: MongoDB find
                value:
                  collection: orders
                  filter:
                    status: paid
                  sort:
                    createdAt: -1
                  limit: 50
              aggregate:
                summary: MongoDB aggregate
                value:
                  collection: orders
                  pipeline:
                    - $match:
                        status: paid
                    - $group:
                        _id: $customerId
                        total:
                          $sum: $amount
      responses:
        '200':
          description: The query ran.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QueryResult'
        '400':
          description: The request body doesn't match any of the three query shapes.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >-
            Either the key lacks `data:read`, or the query itself was refused —
            an unexposed table, a disallowed MongoDB stage, or a query over the
            configured cost/scan limit. The message names what was blocked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Table "public.salaries" is not exposed.
                code: QUERY_DENIED
        '404':
          description: No such datasource, or you can't reach it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: This team has run too many queries in the last minute.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds to wait.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateLimitError'
      security:
        - bearerAuth:
            - data:read
components:
  schemas:
    SqlQuery:
      type: object
      description: PostgreSQL or BigQuery.
      properties:
        sql:
          type: string
          minLength: 1
          maxLength: 50000
          description: A single SELECT statement.
        limit:
          type: integer
          minimum: 1
          maximum: 10000
          description: >-
            Clamped server-side to the team's row cap regardless of what is
            asked for.
      required:
        - sql
      additionalProperties: false
    FindQuery:
      type: object
      description: MongoDB.
      properties:
        collection:
          type: string
          minLength: 1
          maxLength: 120
        filter:
          type: object
          default: {}
        projection:
          type: object
        sort:
          type: object
          additionalProperties:
            type: number
        limit:
          type: integer
          minimum: 1
          maximum: 10000
      required:
        - collection
      additionalProperties: false
    AggregateQuery:
      type: object
      description: >-
        MongoDB. Stages that write ($out, $merge) or run server-side JavaScript
        ($where, $function) are refused.
      properties:
        collection:
          type: string
          minLength: 1
          maxLength: 120
        pipeline:
          type: array
          maxItems: 50
          items:
            type: object
        limit:
          type: integer
          minimum: 1
          maximum: 10000
      required:
        - collection
        - pipeline
      additionalProperties: false
    QueryResult:
      type: object
      properties:
        columns:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              dataType:
                type: string
                description: >-
                  The driver's own type name, carried through rather than
                  normalized.
            required:
              - name
              - dataType
        rows:
          type: array
          items:
            type: array
            items: {}
          description: Each row is an array positioned to match `columns`.
        rowCount:
          type: integer
        truncated:
          type: boolean
          description: >-
            True when more rows matched than were returned. Narrow the query
            rather than raising the limit — the per-team cap applies regardless.
        durationMs:
          type: integer
      required:
        - columns
        - rows
        - rowCount
        - truncated
        - durationMs
    Error:
      type: object
      properties:
        error:
          type: string
          description: Human-readable explanation. Always safe to show a user or an agent.
        code:
          type: string
          description: Stable machine-readable code, when the error has one.
      required:
        - error
    RateLimitError:
      allOf:
        - $ref: '#/components/schemas/Error'
        - type: object
          properties:
            code:
              type: string
              const: RATE_LIMITED
            retryAfter:
              type: integer
              description: Seconds to wait before retrying.
  responses:
    Unauthorized:
      description: Missing or invalid bearer token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        An API key (`sk_live_...`) created under Settings → API keys, or a
        short-lived JWT access token from a browser session. A key only reaches
        the scope it was granted; see [API keys and
        scopes](/reference/api-keys).

````