openapi: 3.0.3
info:
  title: SaturnSQL API
  description: |-
    HTTP API for the SaturnSQL database query platform. SaturnSQL connects to your PostgreSQL, MySQL, MSSQL, Amazon Redshift, ClickHouse, DynamoDB and BigQuery databases and lets people — and AI agents — explore their schema and run queries through one authenticated surface, without ever handling raw database credentials.

    ## Authentication

    Two bearer credentials are accepted on the `Authorization` header:

    - **API key** (recommended for scripts, integrations, and AI agents) — a long-lived, scoped, revocable key in the `sat_live_...` format, created in Settings → Profile → API keys.
    - **JWT session token** — obtained from `POST /auth/login`; expires after 24 hours and requires the TOTP step when MFA is enrolled. This is what the web app uses.

    ```
    Authorization: Bearer sat_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    ```

    ### API key scopes

    A key carries one or more scopes. Everything not in the list below fails closed — a key can never manage other keys, users, billing, or connections.

    | Scope | Grants |
    |-------|--------|
    | `read` | `GET /connections`, `GET /schema/{connectionId}`, `GET /schema/context`, `GET /queries`, `GET /queries/{id}`, `GET /queries/history` |
    | `execute` | `POST /queries/execute` (run SQL), `POST /queries/{id}/run` (run a saved query from values) |
    | `write` | `POST /queries`, `PUT /queries` (save / update saved queries) |

    Give an agent the narrowest scope it needs: `read` alone to explore schema and saved queries, add `execute` only when it must run SQL, add `write` only when it should persist queries. Name the key after the agent and set an expiry.

    ## Building queries and insights with an AI agent

    The intended agent loop is **discover → understand → query → (optionally) save**. Every call is scoped to the company the key belongs to, and only connections the key's owner can see are reachable.

    ### 1. Discover connections

    List the databases available to the key and pick a `connectionId`:

    ```
    curl -s https://saturnsql.com/api/connections \
      -H 'Authorization: Bearer sat_live_...'
    ```

    Each connection reports its `id`, `name`, database `type` and whether it is read-only.

    ### 2. Understand the schema

    Pull the tables, columns and types so the agent writes SQL against the real structure — never guess table or column names:

    ```
    # Full schema for a connection
    curl -s 'https://saturnsql.com/api/schema/CONNECTION_ID' \
      -H 'Authorization: Bearer sat_live_...'

    # Relevance-ranked subset for a goal (best for large databases)
    curl -s 'https://saturnsql.com/api/schema/context?connection_id=CONNECTION_ID&intent=monthly%20revenue%20by%20plan' \
      -H 'Authorization: Bearer sat_live_...'
    ```

    `GET /schema/context` accepts `connection_id` (required), `intent` (a natural-language description of what you're after), `tables` (comma-separated allowlist) and `current_sql`, and returns only the slice of schema relevant to that intent. Feed it straight into your SQL-generation prompt to keep context small and accurate.

    ### 3. Run read-only queries

    Execute generated SQL and get rows back (`execute` scope required):

    ```
    curl -s https://saturnsql.com/api/queries/execute \
      -H 'Authorization: Bearer sat_live_...' \
      -H 'Content-Type: application/json' \
      -d '{"connectionId":"CONNECTION_ID","sql":"SELECT plan, count(*) AS n FROM companies GROUP BY plan ORDER BY n DESC","limit":100}'
    ```

    `limit` defaults to 1000 and caps at 10000. The response returns the result rows plus column metadata.

    **Read-only enforcement.** Execution is guarded so an agent cannot mutate data: only `SELECT`, `WITH` (CTEs) and `EXPLAIN` are accepted, one statement at a time, and write / DDL / admin verbs (`INSERT`, `UPDATE`, `DELETE`, `DROP`, `CREATE`, `ALTER`, `TRUNCATE`, `GRANT`, `EXEC`, `SELECT … INTO`, …) are rejected. Connections flagged read-only are additionally enforced at the driver level. Design agent prompts around exploration and reporting, not mutation.

    ### Insight-query patterns

    Once the schema is known, these read-only patterns surface useful insights on almost any database — adapt table and column names from step 2:

    - **Shape & volume** — `SELECT count(*) FROM <table>`; row counts per table to find the big / active ones.
    - **Distributions** — `SELECT <dim>, count(*) n FROM <table> GROUP BY <dim> ORDER BY n DESC LIMIT 20` for top categories, statuses, plans.
    - **Trends over time** — `SELECT date_trunc('month', <ts>) m, count(*) FROM <table> GROUP BY 1 ORDER BY 1` for growth and seasonality.
    - **Data-quality audit** — null / blank counts per column, duplicate keys, orphaned foreign keys.
    - **Cohorts & funnels** — first-seen vs last-seen per entity, step-to-step conversion with `WITH` CTEs.

    Run `EXPLAIN` first on unfamiliar or large tables, and keep a `LIMIT` on ad-hoc `SELECT`s.

    ### 4. Save the good ones (optional, `write` scope)

    Persist a query the user will want again, optionally filed into a folder with `folderPath` (`null` = root):

    ```
    curl -s https://saturnsql.com/api/queries \
      -H 'Authorization: Bearer sat_live_...' \
      -H 'Content-Type: application/json' \
      -d '{"name":"Signups by month","sql":"SELECT ...","connectionId":"CONNECTION_ID","folderPath":"Growth"}'
    ```

    Browse queries the team already trusts with `GET /queries`, and recent runs with `GET /queries/history`, to reuse rather than reinvent. (Sharing a query company-wide is a plan-gated action available in the web app, not over an API key.)

    ### 5. A saved query is also a report page

    Every saved query is readable at `https://saturnsql.com/app/report/{id}` — the result as a table, with a chart above it when the shape allows one. With no chart pinned (the only case an API key can produce), the shape decides:

    - 1 date/timestamp column + 1–3 numeric columns → line / area
    - no date column + exactly 2 numeric columns → scatter
    - no date column + 1 text column (2–12 values) + 1 numeric → bar
    - anything else, including a 4th numeric column → table only

    So keep the numeric columns to three and the date columns to one; emit the rest as text. Rows plot in result order, so a time series needs `ORDER BY <time> ASC`.

    Pinning a chart (`PUT /queries/{id}/chart`) is session-token, owner-only. Save the query, hand over the link, and leave that to the web app.

    ## Limits & errors

    - **Query quota** — each company has a monthly execution quota; exceeding it (or hitting the rate limit) returns `429` with `usage` / `limit` details.
    - **Errors** — non-2xx responses return `{ "error": "..." }`: `400` validation, `401` bad or missing credential, `403` insufficient scope, `404` unknown connection or query.

    Guide: [Using SaturnSQL with AI coding agents](https://saturnsql.com/help/ai-coding-agents).
  version: 1.0.0
  contact:
    name: SaturnSQL Support
    email: support@saturnsql.com
servers:
  - url: https://saturnsql.com/api
    description: Production server
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: 24h session token from POST /auth/login
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: sat_live_...
      description: 'Long-lived scoped API key (sat_live_ prefix), created in Settings. Scopes: read, execute.'
  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
          description: Human-readable error message
        details:
          type: object
          additionalProperties: true
          description: Optional field-level validation details
      required:
        - error
      example:
        error: Insufficient scope
    AuthSession:
      type: object
      description: 'A successful authentication: session token plus the signed-in user and their companies.'
      properties:
        token:
          type: string
          description: 'JWT session token (24h). Send as `Authorization: Bearer <token>`.'
        refreshToken:
          type: string
          description: Long-lived refresh token (30d) for POST /auth/refresh.
        selectedCompanyId:
          type: string
        mustChangePassword:
          type: boolean
        user:
          type: object
          properties:
            id:
              type: string
            email:
              type: string
            name:
              type: string
        companies:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
              plan:
                type: string
      required:
        - token
        - refreshToken
    Connection:
      type: object
      description: A database connection. Credentials (password) are never returned.
      properties:
        id:
          type: string
        name:
          type: string
        type:
          type: string
          enum:
            - postgres
            - mysql
            - mssql
            - redshift
            - clickhouse
            - dynamodb
            - bigquery
        host:
          type: string
        port:
          type: integer
        database:
          type: string
        username:
          type: string
        sslEnabled:
          type: boolean
        readOnly:
          type: boolean
          description: When true, writes are blocked at the driver level.
        status:
          type: string
        isShared:
          type: boolean
          description: Visible to the whole company.
    Column:
      type: object
      properties:
        name:
          type: string
        type:
          type: string
        nullable:
          type: boolean
        default:
          type: string
          nullable: true
        isPrimaryKey:
          type: boolean
        isForeignKey:
          type: boolean
      required:
        - name
        - type
        - nullable
    Table:
      type: object
      properties:
        name:
          type: string
        columns:
          type: array
          items:
            $ref: '#/components/schemas/Column'
      required:
        - name
        - columns
    SavedQuery:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        sql:
          type: string
        connectionId:
          type: string
          nullable: true
        folderPath:
          type: string
          nullable: true
          description: A single flat folder label (not a nested path); "A/B" is one folder named "A/B", not "A" containing "B". null = root.
        isShared:
          type: boolean
        description:
          type: string
          nullable: true
          description: Subtitle on the report page. Returned by GET /queries; nothing can set it yet, so always null.
        chart:
          allOf:
            - $ref: '#/components/schemas/ChartSpec'
          nullable: true
          description: Pinned chart, or null to let the result shape decide. Returned by GET /queries for editors only (absent from GET /queries/{id}); set with PUT /queries/{id}/chart (session token only).
        userId:
          type: string
        variables:
          type: array
          description: Declared `{{name}}` variables, in first-appearance order in the SQL. Empty when the query has none.
          items:
            $ref: '#/components/schemas/QueryVariable'
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    ReportMeta:
      type: object
      description: 'What the report page needs to render a saved query as a form. Deliberately excludes the SQL: the report page is the one place a query is not meant to be read as SQL, so this is strictly less than GET /queries/{id}.'
      properties:
        id:
          type: string
        title:
          type: string
        description:
          type: string
          nullable: true
        connection:
          type: object
          nullable: true
          description: Where the numbers come from. Null when the query has no connection, in which case it cannot be run.
          properties:
            name:
              type: string
            database:
              type: string
        variables:
          type: array
          description: The filters a reader fills in. Supply values to POST /queries/{id}/run.
          items:
            $ref: '#/components/schemas/QueryVariable'
        chart:
          allOf:
            - $ref: '#/components/schemas/ChartSpec'
          nullable: true
          description: Null means decide from the result shape, which is the common case.
    ChartSpec:
      type: object
      description: |-
        How a report should be drawn. Stored verbatim and interpreted at render time, since the columns a spec refers to only exist after a run: a spec naming a column the SELECT list no longer returns falls back to the shape-derived guess rather than failing.

        With no spec stored, the result shape decides — see the "saved query is also a report page" section of the API guide for the rules an agent should write SQL against.
      required:
        - type
      properties:
        type:
          type: string
          enum:
            - line
            - area
            - bar
            - scatter
            - share
            - number
            - none
          description: '`none` suppresses the chart for good, which is why it is distinct from a null `chart`. `share` is never guessed and must be set explicitly: its shape is identical to `bar` and nothing in the data says which is meant.'
        x:
          type: string
          description: Column for the x axis or the category. Absent for `number`.
        series:
          type: array
          items:
            type: string
          maxItems: 3
          description: 'Measure columns, in order. Capped at 3: the validated palette separates three series by lightness and a fourth has no colour.'
        group:
          type: string
          description: Low-cardinality column splitting a scatter into groups.
    QueryVariable:
      type: object
      description: One `{{name}}` variable declared by a saved query. Values are supplied to POST /queries/{id}/run and substituted server side.
      required:
        - name
        - type
      properties:
        name:
          type: string
          description: Matches `[A-Za-z_][A-Za-z0-9_]*`
        type:
          type: string
          enum:
            - date
            - text
            - number
            - boolean
            - dropdown
        required:
          type: boolean
          description: An empty value for a required variable is a 422, not an empty substitution.
        default:
          type: object
          nullable: true
          description: Used when the value is absent. A `relative` default is only valid on a `date` variable and resolves against the caller time zone (or the schedule time zone).
          properties:
            kind:
              type: string
              enum:
                - fixed
                - relative
            value:
              type: string
              description: Literal for `fixed`; one of today, yesterday, 7_days_ago, 30_days_ago, 90_days_ago, start_of_this_month, start_of_last_month, end_of_last_month, start_of_this_year for `relative`.
        options:
          type: array
          items:
            type: string
          description: Dropdown only, non-empty. A value outside this list is a 422.
    QueryHistoryEntry:
      type: object
      properties:
        id:
          type: string
        sqlContent:
          type: string
        connectionId:
          type: string
          nullable: true
        rowCount:
          type: integer
        executionTimeMs:
          type: integer
        errorMessage:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
    QueryResult:
      type: object
      properties:
        success:
          type: boolean
        result:
          type: object
          properties:
            rows:
              type: array
              items:
                type: object
                additionalProperties: true
            fields:
              type: array
              description: Column descriptors. `name` and `dataTypeID` are always present; the driver may include additional fields (e.g. dataTypeSize, format, tableID) which are safe to ignore.
              items:
                type: object
                properties:
                  name:
                    type: string
                  dataTypeID:
                    type: integer
                additionalProperties: true
            rowCount:
              type: integer
            command:
              type: string
            duration:
              type: integer
              description: Execution time in ms
            truncated:
              type: boolean
              description: True when rows/cells were capped to fit the memory budget.
    ApiKey:
      type: object
      description: Public shape of an API key (the secret is only returned once, at creation).
      properties:
        id:
          type: string
        name:
          type: string
        prefix:
          type: string
          description: First chars of the key, for display (e.g. sat_live_ab12cd34).
        scopes:
          type: array
          items:
            type: string
            enum:
              - read
              - execute
              - write
        expiresAt:
          type: string
          format: date-time
          nullable: true
        lastUsedAt:
          type: string
          format: date-time
          nullable: true
        revokedAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
    Schedule:
      type: object
      properties:
        id:
          type: string
        savedQueryId:
          type: string
        frequency:
          type: string
          enum:
            - hourly
            - daily
            - weekly
            - monthly
        hour:
          type: integer
        minute:
          type: integer
        dayOfWeek:
          type: integer
          nullable: true
          description: 0-6 (Sun-Sat), for weekly
        dayOfMonth:
          type: integer
          nullable: true
          description: 1-31, for monthly
        includeHeaders:
          type: boolean
        isActive:
          type: boolean
        nextRun:
          type: string
          format: date-time
          nullable: true
        destinationType:
          type: string
          enum:
            - google_sheets
            - slack
        spreadsheetId:
          type: string
          nullable: true
        sheetName:
          type: string
          nullable: true
security:
  - BearerAuth: []
  - ApiKeyAuth: []
paths:
  /api-keys/{id}:
    delete:
      tags:
        - API Keys
      summary: Revoke an API key
      description: Revokes a key by id. Takes effect immediately; the key can no longer authenticate. Requires a session token.
      operationId: delete_api_keys
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: cuid
      responses:
        '200':
          description: Revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
              example:
                success: true
        '401':
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Key not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api-keys:
    get:
      tags:
        - API Keys
      summary: List API keys
      description: Lists the current company's active (non-revoked) API keys. Only the display prefix is shown — the secret is never returned after creation. Requires a session token.
      operationId: list_api_keys
      parameters: []
      responses:
        '200':
          description: Keys
          content:
            application/json:
              schema:
                type: object
                properties:
                  keys:
                    type: array
                    items:
                      $ref: '#/components/schemas/ApiKey'
              example:
                keys:
                  - id: cmr9key1
                    name: Claude Code
                    prefix: sat_live_ab12cd34
                    scopes:
                      - read
                      - execute
                    expiresAt: '2027-01-01T00:00:00.000Z'
                    lastUsedAt: '2026-07-26T09:12:00.000Z'
                    revokedAt: null
                    createdAt: '2026-07-20T08:00:00.000Z'
        '401':
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      tags:
        - API Keys
      summary: Create an API key
      description: Creates a scoped `sat_live_` key. The plaintext `key` is returned ONCE in the response — store it immediately, it cannot be retrieved again. Requires a session token.
      operationId: create_api_keys
      parameters:
        - name: name
          in: query
          required: true
          description: Display name for the key
          schema:
            type: string
        - name: expiry
          in: query
          required: true
          description: One of 24h, 30d, 1y, never
          schema:
            type: string
        - name: scopes
          in: query
          required: true
          description: Subset of ["read", "execute", "write"], defaults to ["read"]
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - expiry
              properties:
                name:
                  type: string
                  description: Display name, e.g. the agent it is for
                expiry:
                  type: string
                  enum:
                    - 24h
                    - 30d
                    - 1y
                    - never
                scopes:
                  type: array
                  items:
                    type: string
                    enum:
                      - read
                      - execute
                      - write
                  description: Defaults to ["read"]
            example:
              name: Claude Code
              expiry: 1y
              scopes:
                - read
                - execute
      responses:
        '201':
          description: Created (secret shown once)
          content:
            application/json:
              schema:
                type: object
                properties:
                  key:
                    type: string
                    description: The plaintext secret — shown only here.
                  apiKey:
                    $ref: '#/components/schemas/ApiKey'
              example:
                key: sat_live_ab12cd34ef56gh78ij90kl12mn34op56
                apiKey:
                  id: cmr9key1
                  name: Claude Code
                  prefix: sat_live_ab12cd34
                  scopes:
                    - read
                    - execute
                  expiresAt: '2027-07-26T00:00:00.000Z'
                  lastUsedAt: null
                  revokedAt: null
                  createdAt: '2026-07-26T10:00:00.000Z'
        '400':
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /auth/login:
    post:
      tags:
        - Authentication
      summary: Log in (get a session token)
      description: 'Exchanges email + password for a 24h JWT session token and a refresh token. If MFA is enrolled, returns `{ mfaRequired: true, mfaToken }` instead — complete the login with POST /auth/mfa/login. For scripts and AI agents prefer a `sat_live_` API key over this flow.'
      operationId: create_auth_login
      security: []
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - password
              properties:
                email:
                  type: string
                password:
                  type: string
            example:
              email: you@company.com
              password: your-password
      responses:
        '200':
          description: Authenticated (or MFA challenge)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthSession'
              example:
                token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
                refreshToken: rt_9f2c1a...
                selectedCompanyId: cmr9k2company
                mustChangePassword: false
                user:
                  id: cmr9k2user
                  email: you@company.com
                  name: Ada Lovelace
                companies:
                  - id: cmr9k2company
                    name: Acme Inc
                    plan: pro
        '400': &ref_0
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401': &ref_1
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /auth/mfa/login:
    post:
      tags:
        - Authentication
      summary: Complete MFA login
      description: 'Completes a login challenged by MFA: submit the short-lived `mfaToken` from POST /auth/login plus the 6-digit TOTP `code` (or a backup code).'
      operationId: create_auth_mfa_login
      security: []
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - mfaToken
                - code
              properties:
                mfaToken:
                  type: string
                code:
                  type: string
            example:
              mfaToken: mfa_5d3b...
              code: '123456'
      responses:
        '200':
          description: Authenticated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthSession'
              example:
                token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
                refreshToken: rt_9f2c1a...
                selectedCompanyId: cmr9k2company
                mustChangePassword: false
                user:
                  id: cmr9k2user
                  email: you@company.com
                  name: Ada Lovelace
                companies:
                  - id: cmr9k2company
                    name: Acme Inc
                    plan: pro
        '400': *ref_0
        '401': *ref_1
  /auth/refresh:
    post:
      tags:
        - Authentication
      summary: Refresh a session token
      description: Exchanges a valid refresh token for a fresh 24h session token. Optionally switch the active company with `selectedCompanyId`.
      operationId: create_auth_refresh
      security: []
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - refreshToken
              properties:
                refreshToken:
                  type: string
                selectedCompanyId:
                  type: string
            example:
              refreshToken: rt_9f2c1a...
      responses:
        '200':
          description: New session token
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string
                  refreshToken:
                    type: string
                  user:
                    type: object
                    additionalProperties: true
              example:
                token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
                refreshToken: rt_9f2c1a...
                user:
                  id: cmr9k2user
                  email: you@company.com
                  name: Ada Lovelace
        '400': *ref_0
        '401': *ref_1
  /connections:
    get:
      tags:
        - Connections
      summary: List connections
      description: Lists the database connections available to the caller (own + company-shared). Use an `id` as the `connectionId` for schema and query calls. Available to API keys with the `read` scope.
      operationId: list_connections
      parameters: []
      responses:
        '200':
          description: Connections
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Connection'
              example:
                - id: cmrt9ccrc0001
                  name: Prod Postgres
                  type: postgres
                  host: db.internal
                  port: 5432
                  database: app
                  username: readonly
                  sslEnabled: true
                  readOnly: true
                  status: connected
                  isShared: true
        '401':
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /queries/{id}/chart:
    put:
      tags:
        - Reports
      summary: Set a report’s chart, or clear it
      description: Stores a ChartSpec on a saved query, or clears it with `null` to go back to deciding from the result shape. The spec is stored verbatim and not validated against columns, because the columns only exist after a run; a spec that stops fitting falls back to the guess at render time rather than erroring. Owner only, and its own route rather than a field on PUT /queries so that choosing a chart cannot collide with that endpoint’s optimistic-concurrency baseline. Session token only; API keys cannot reach this.
      operationId: update_queries_chart
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: cuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - chart
              properties:
                chart:
                  allOf:
                    - $ref: '#/components/schemas/ChartSpec'
                  nullable: true
                  description: The spec to store, or null to go back to deciding from the shape
            example:
              chart:
                type: bar
                x: region
                series:
                  - revenue
      responses:
        '200':
          description: The stored configuration
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  chart:
                    allOf:
                      - $ref: '#/components/schemas/ChartSpec'
                    nullable: true
              example:
                success: true
                chart:
                  type: bar
                  x: region
                  series:
                    - revenue
        '400':
          description: Chart must be an object or null
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Editor access required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Query not found, or the caller does not own it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /queries/{id}/report:
    get:
      tags:
        - Reports
      summary: Get a report’s metadata
      description: 'Everything needed to present a saved query as a form: title, description, which connection and database the numbers come from, the variables to fill in, and the chart configuration. Never returns the SQL, so this is the endpoint to use when a caller should be able to run a query without reading it. Scoped to the caller’s company and to their own or shared queries; anything else is a 404 rather than a 403, so the endpoint cannot be used to discover query ids. Available to API keys with the `read` scope.'
      operationId: get_queries_report
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: cuid
      responses:
        '200':
          description: Report metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportMeta'
              example:
                id: clx1report
                title: Acquired Vehicles Log
                description: Every vehicle acquired in the window.
                connection:
                  name: Sales DB
                  database: consumer_hub_prod
                variables:
                  - name: start_date
                    type: date
                    required: true
                    default:
                      kind: relative
                      value: 30_days_ago
                chart:
                  type: area
                  x: day
                  series:
                    - signups
        '401':
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Query not found, or not shared with the caller
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /queries/{id}:
    get:
      tags:
        - Queries
      summary: Get a saved query
      description: Returns a single saved query by id. Available to API keys with the `read` scope.
      operationId: get_queries
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: cuid
      responses:
        '200':
          description: Saved query
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SavedQuery'
              example:
                id: cmr9q1
                name: Signups by month
                sql: SELECT date_trunc('month', created_at) m, count(*) FROM users GROUP BY 1 ORDER BY 1
                connectionId: cmrt9ccrc0001
                folderPath: Growth
                isShared: true
                userId: cmr9k2user
                createdAt: '2026-07-01T08:00:00.000Z'
                updatedAt: '2026-07-01T08:00:00.000Z'
        '401':
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Query not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /queries/{id}/run:
    post:
      tags:
        - Queries
      summary: Run a saved query
      description: 'Runs a saved query, substituting its declared `{{variables}}` from the values supplied. The caller sends values, never SQL: the SQL comes from the stored query and every value is type-checked and escaped server side. A value that is missing or invalid returns `422` with one entry per bad variable and executes nothing. An absent name falls back to that variable''s default; a name present with an empty string stays empty. Same read-only guard, rate limit and monthly quota as POST /queries/execute. Available to API keys with the `execute` scope.'
      operationId: create_queries_run
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: cuid
        - name: values
          in: query
          required: true
          description: Variable values by name. An absent name falls back to that variable's default; a name present with an empty string stays empty.
          schema:
            type: object
        - name: timeZone
          in: query
          required: true
          description: 'IANA time zone used to resolve relative date defaults (default: UTC)'
          schema:
            type: string
        - name: limit
          in: query
          required: true
          description: 'Maximum rows to return (default: 1000, max: 10000)'
          schema:
            type: number
        - name: refresh
          in: query
          required: true
          description: Skip the result cache and read the database now. Omit to allow a cached result.
          schema:
            type: boolean
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                values:
                  type: object
                  additionalProperties:
                    type: string
                  description: Variable values by name
                timeZone:
                  type: string
                  description: IANA zone used to resolve relative date defaults (default UTC)
                limit:
                  type: integer
                  minimum: 1
                  maximum: 10000
                  description: Max rows (default 1000)
            example:
              values:
                start_date: '2026-06-01'
                end_date: ''
              timeZone: Europe/Helsinki
              limit: 100
      responses:
        '200':
          description: Result rows
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QueryResult'
              example:
                success: true
                result:
                  rows:
                    - id: 41
                      acquired_at: '2026-06-04'
                  fields:
                    - name: id
                      dataTypeID: 20
                    - name: acquired_at
                      dataTypeID: 1082
                  rowCount: 1
                  command: SELECT
                  duration: 24
                  truncated: false
        '400':
          description: Invalid request, no connection on the query, or variables on a DynamoDB connection
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Insufficient scope, paused connection, or non-read-only SQL
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Query not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: One or more variable values are missing or invalid; nothing was executed
          content:
            application/json:
              schema:
                type: object
                properties:
                  errors:
                    type: array
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                        code:
                          type: string
                          enum:
                            - required
                            - invalid_date
                            - invalid_number
                            - invalid_boolean
                            - not_an_option
                            - unknown_variable
                            - illegal_character
                        message:
                          type: string
              example:
                errors:
                  - name: start_date
                    code: invalid_date
                    message: Variable "start_date" must be a date in YYYY-MM-DD format.
        '429':
          description: Query quota or rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /queries/execute:
    post:
      tags:
        - Queries
      summary: Execute SQL
      description: 'Runs SQL against a connection and returns rows plus column metadata. Read-only guarded: only SELECT, WITH (CTE) and EXPLAIN are accepted, one statement at a time; write/DDL/admin verbs (INSERT, UPDATE, DELETE, DROP, …) are rejected with `400`, the same status as any invalid or multi-statement SQL. `limit` defaults to 1000, caps at 10000. Available to API keys with the `execute` scope. Exceeding the monthly query quota or the rate limit returns `429`.'
      operationId: create_queries_execute
      parameters:
        - name: connectionId
          in: query
          required: true
          description: Database connection ID
          schema:
            type: string
        - name: sql
          in: query
          required: true
          description: Read-only SQL query to execute (SELECT / WITH / EXPLAIN). May contain {{variable}} placeholders, in which case `variables` must define every one of them.
          schema:
            type: string
        - name: limit
          in: query
          required: true
          description: 'Maximum rows to return (default: 1000, max: 10000)'
          schema:
            type: number
        - name: variables
          in: query
          required: true
          description: Definitions for the {{placeholders}} in `sql`. Values are type-checked and escaped against these server side.
          schema:
            type: object
        - name: values
          in: query
          required: true
          description: Variable values by name. An absent name falls back to that variable's default; a name present with an empty string stays empty.
          schema:
            type: object
        - name: timeZone
          in: query
          required: true
          description: 'IANA time zone used to resolve relative date defaults (default: UTC)'
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - connectionId
                - sql
              properties:
                connectionId:
                  type: string
                sql:
                  type: string
                  description: 'Read-only SQL: SELECT / WITH / EXPLAIN'
                limit:
                  type: integer
                  minimum: 1
                  maximum: 10000
                  description: Max rows (default 1000)
            example:
              connectionId: cmrt9ccrc0001
              sql: SELECT plan, count(*) AS n FROM companies GROUP BY plan ORDER BY n DESC
              limit: 100
      responses:
        '200':
          description: Result rows
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QueryResult'
              example:
                success: true
                result:
                  rows:
                    - plan: pro
                      'n': 42
                    - plan: starter
                      'n': 17
                  fields:
                    - name: plan
                      dataTypeID: 25
                    - name: 'n'
                      dataTypeID: 20
                  rowCount: 2
                  command: SELECT
                  duration: 38
                  truncated: false
        '400':
          description: Invalid SQL or non-read-only statement
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Insufficient scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Query quota or rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /queries/history:
    get:
      tags:
        - Queries
      summary: Query run history
      description: Returns the caller's recent query executions (SQL, connection, timing, status). Available to API keys with the `read` scope.
      operationId: list_queries_history
      parameters: []
      responses:
        '200':
          description: History
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/QueryHistoryEntry'
              example:
                - id: cmr9h1
                  sqlContent: SELECT count(*) FROM users
                  connectionId: cmrt9ccrc0001
                  rowCount: 1
                  executionTimeMs: 42
                  errorMessage: null
                  createdAt: '2026-07-26T09:12:00.000Z'
        '401':
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /queries:
    get:
      tags:
        - Queries
      summary: List saved queries
      description: Returns the saved queries visible to the caller (own + company-shared). Available to API keys with the `read` scope.
      operationId: list_queries
      parameters: []
      responses:
        '200':
          description: Saved queries
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SavedQuery'
              example:
                - id: cmr9q1
                  name: Signups by month
                  sql: SELECT date_trunc('month', created_at) m, count(*) FROM users GROUP BY 1 ORDER BY 1
                  connectionId: cmrt9ccrc0001
                  folderPath: Growth
                  isShared: true
                  userId: cmr9k2user
                  createdAt: '2026-07-01T08:00:00.000Z'
                  updatedAt: '2026-07-01T08:00:00.000Z'
        '401':
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      tags:
        - Queries
      summary: Save a query
      description: |-
        Saves a query for reuse, optionally filed into a folder with `folderPath` (null = root). Available to API keys with the `write` scope. Sharing a query company-wide is plan-gated and done in the web app.

        The fields below are the whole create surface: `description` and `chart` are ignored if sent. The saved query is immediately readable at `/app/report/{id}`, where the result shape decides the chart.
      operationId: create_queries
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - sql
              properties:
                name:
                  type: string
                sql:
                  type: string
                connectionId:
                  type: string
                  description: Optional; the connection this query targets
                folderPath:
                  type: string
                  nullable: true
                  description: A single flat folder label, not a nested path ("A/B" is one label); null = root
                variables:
                  type: array
                  items:
                    $ref: '#/components/schemas/QueryVariable'
                  description: Declarations for the `{{name}}` placeholders in `sql`. Omit or send [] when there are none.
            example:
              name: Signups by month
              sql: SELECT date_trunc('month', created_at) m, count(*) FROM users GROUP BY 1 ORDER BY 1
              connectionId: cmrt9ccrc0001
              folderPath: Growth
      responses:
        '201':
          description: Saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SavedQuery'
              example:
                id: cmr9qNEW
                name: Signups by month
                sql: SELECT date_trunc('month', created_at) m, count(*) FROM users GROUP BY 1 ORDER BY 1
                connectionId: cmrt9ccrc0001
                folderPath: Growth
                isShared: false
                userId: cmr9k2user
                createdAt: '2026-07-26T10:00:00.000Z'
                updatedAt: '2026-07-26T10:00:00.000Z'
        '400':
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Saved-query limit reached (plan)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    put:
      tags:
        - Queries
      summary: Update a query
      description: Updates a saved query. Only the fields you send change; `sql` and `folderPath` are optional. Available to API keys with the `write` scope.
      operationId: update_queries
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - queryId
                - name
              properties:
                queryId:
                  type: string
                name:
                  type: string
                sql:
                  type: string
                folderPath:
                  type: string
                  nullable: true
                  description: A single flat folder label, not a nested path ("A/B" is one label); null moves to root; omit to leave unchanged
                variables:
                  type: array
                  items:
                    $ref: '#/components/schemas/QueryVariable'
                  description: Replaces the stored declarations. Omit to leave them unchanged; send [] to clear them.
            example:
              queryId: cmr9q1
              name: Signups by month (v2)
              sql: SELECT date_trunc('week', created_at) w, count(*) FROM users GROUP BY 1 ORDER BY 1
              folderPath: Growth
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SavedQuery'
              example:
                id: cmr9q1
                name: Signups by month (v2)
                sql: SELECT date_trunc('week', created_at) w, count(*) FROM users GROUP BY 1 ORDER BY 1
                connectionId: cmrt9ccrc0001
                folderPath: Growth
                isShared: true
                userId: cmr9k2user
                createdAt: '2026-07-01T08:00:00.000Z'
                updatedAt: '2026-07-26T10:05:00.000Z'
        '400': &ref_2
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401': &ref_3
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403': &ref_4
          description: Insufficient scope or plan restriction
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404': &ref_5
          description: Resource not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /schedules/{id}:
    get:
      tags:
        - Schedules
      summary: Get a schedule
      description: Returns a single schedule by id. Requires a session token.
      operationId: get_schedules
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: cuid
      responses:
        '200':
          description: Schedule
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Schedule'
              example:
                id: cmr9s1
                savedQueryId: cmr9q1
                frequency: daily
                hour: 7
                minute: 0
                dayOfWeek: null
                dayOfMonth: null
                includeHeaders: true
                isActive: true
                nextRun: '2026-07-27T07:00:00.000Z'
                destinationType: google_sheets
                spreadsheetId: 1AbC...
                sheetName: Signups
        '401':
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Schedule not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    put:
      tags:
        - Schedules
      summary: Update a schedule
      description: Updates cadence, destination, or active state of a schedule. Send only the fields you want to change. Requires a session token.
      operationId: update_schedules
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: cuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                frequency:
                  type: string
                  enum:
                    - hourly
                    - daily
                    - weekly
                    - monthly
                hour:
                  type: integer
                minute:
                  type: integer
                isActive:
                  type: boolean
            example:
              frequency: weekly
              dayOfWeek: 1
              hour: 8
              minute: 30
              isActive: true
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Schedule'
              example:
                id: cmr9s1
                savedQueryId: cmr9q1
                frequency: weekly
                hour: 8
                minute: 30
                dayOfWeek: 1
                dayOfMonth: null
                includeHeaders: true
                isActive: true
                nextRun: '2026-08-03T08:30:00.000Z'
                destinationType: google_sheets
                spreadsheetId: 1AbC...
                sheetName: Signups
        '400': *ref_2
        '401': *ref_3
        '403': *ref_4
        '404': *ref_5
    delete:
      tags:
        - Schedules
      summary: Delete a schedule
      description: Deletes a schedule by id. Requires a session token.
      operationId: delete_schedules
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: cuid
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
              example:
                success: true
        '401':
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Schedule not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /schedules/{id}/run:
    post:
      tags:
        - Schedules
      summary: Run a schedule now
      description: Triggers an immediate run of a schedule (runs the saved query and exports to its destination), independent of its cadence. Takes no request body. Requires a session token.
      operationId: create_schedules_run
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: cuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              description: No body required — send an empty object or omit the body entirely.
      responses:
        '200':
          description: Ran
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  spreadsheetUrl:
                    type: string
                  writtenRange:
                    type: string
                  slackDelivered:
                    type: boolean
              example:
                success: true
                spreadsheetUrl: https://docs.google.com/spreadsheets/d/1AbC.../edit
                writtenRange: Signups!A1:B44
                slackDelivered: false
        '401':
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Schedule not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Export failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /schedules:
    get:
      tags:
        - Schedules
      summary: List schedules
      description: Lists scheduled exports of saved queries (to Google Sheets or Slack). Requires a session token.
      operationId: list_schedules
      parameters: []
      responses:
        '200':
          description: Schedules
          content:
            application/json:
              schema:
                type: object
                properties:
                  schedules:
                    type: array
                    items:
                      $ref: '#/components/schemas/Schedule'
              example:
                schedules:
                  - id: cmr9s1
                    savedQueryId: cmr9q1
                    frequency: daily
                    hour: 7
                    minute: 0
                    dayOfWeek: null
                    dayOfMonth: null
                    includeHeaders: true
                    isActive: true
                    nextRun: '2026-07-27T07:00:00.000Z'
                    destinationType: google_sheets
                    spreadsheetId: 1AbC...
                    sheetName: Signups
        '401':
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      tags:
        - Schedules
      summary: Create a schedule
      description: Schedules a saved query to run on a cadence and export its result to ONE destination — Google Sheets (spreadsheetId/spreadsheetName + sheetName + startCell) OR Slack (slackChannelId). Scheduling is plan-gated. Requires a session token.
      operationId: create_schedules
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - savedQueryId
                - frequency
                - hour
              properties:
                savedQueryId:
                  type: string
                frequency:
                  type: string
                  enum:
                    - hourly
                    - daily
                    - weekly
                    - monthly
                hour:
                  type: integer
                  minimum: 0
                  maximum: 23
                minute:
                  type: integer
                  minimum: 0
                  maximum: 59
                  description: Default 0
                dayOfWeek:
                  type: integer
                  minimum: 0
                  maximum: 6
                  description: For weekly (0=Sun)
                dayOfMonth:
                  type: integer
                  minimum: 1
                  maximum: 31
                  description: For monthly
                spreadsheetId:
                  type: string
                  description: Google Sheets destination (existing sheet)
                spreadsheetName:
                  type: string
                  description: Or create a new spreadsheet by name
                sheetName:
                  type: string
                startCell:
                  type: string
                  description: A1 notation, default A1
                includeHeaders:
                  type: boolean
                  description: Default true
                slackChannelId:
                  type: string
                  nullable: true
                  description: Slack destination (instead of Sheets)
                slackMessageTemplate:
                  type: string
                  nullable: true
            example:
              savedQueryId: cmr9q1
              frequency: daily
              hour: 7
              minute: 0
              spreadsheetId: 1AbC...
              sheetName: Signups
              startCell: A1
              includeHeaders: true
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Schedule'
              example:
                id: cmr9sNEW
                savedQueryId: cmr9q1
                frequency: daily
                hour: 7
                minute: 0
                dayOfWeek: null
                dayOfMonth: null
                includeHeaders: true
                isActive: true
                nextRun: '2026-07-27T07:00:00.000Z'
                destinationType: google_sheets
                spreadsheetId: 1AbC...
                sheetName: Signups
        '400':
          description: Validation failed / more than one destination
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Scheduling not available on plan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /schema/{connectionId}:
    get:
      tags:
        - Schema
      summary: Get full schema
      description: Returns every table and column for a connection, so an agent can write SQL against the real structure. For large databases prefer GET /schema/context. Available to API keys with the `read` scope.
      operationId: get_schema
      parameters:
        - name: connectionId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Tables
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Table'
              example:
                - name: users
                  columns:
                    - name: id
                      type: uuid
                      nullable: false
                      isPrimaryKey: true
                    - name: email
                      type: text
                      nullable: false
                    - name: created_at
                      type: timestamptz
                      nullable: false
        '401':
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Connection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /schema/context:
    get:
      tags:
        - Schema
      summary: Get relevant schema for an intent
      description: Returns only the slice of schema relevant to a stated goal — ideal for keeping an AI agent's context small on large databases. Feed the result straight into a SQL-generation prompt. Available to API keys with the `read` scope.
      operationId: list_schema_context
      parameters:
        - name: connection_id
          in: query
          required: true
          schema:
            type: string
          description: Connection to inspect
        - name: intent
          in: query
          required: false
          schema:
            type: string
          description: Natural-language description of what you want to query
        - name: tables
          in: query
          required: false
          schema:
            type: string
          description: Comma-separated allowlist of tables
        - name: current_sql
          in: query
          required: false
          schema:
            type: string
          description: SQL already being drafted, used to rank relevance
      responses:
        '200':
          description: Relevant schema
          content:
            application/json:
              schema:
                type: object
                properties:
                  connection_id:
                    type: string
                  schema:
                    type: array
                    items:
                      $ref: '#/components/schemas/Table'
                  context_id:
                    type: string
              example:
                connection_id: cmrt9ccrc0001
                schema:
                  - name: orders
                    columns:
                      - name: id
                        type: uuid
                        nullable: false
                        isPrimaryKey: true
                      - name: total_cents
                        type: integer
                        nullable: false
                      - name: created_at
                        type: timestamptz
                        nullable: false
                context_id: ctx_cmrt9ccrc0001_1753524000000
        '400':
          description: connection_id is required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Bad or missing credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Connection not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
tags:
  - name: Authentication
    description: Log in and refresh session tokens
  - name: API Keys
    description: Create and manage scoped sat_live_ keys for scripts and agents
  - name: Connections
    description: Discover the databases you can query
  - name: Schema
    description: Inspect tables and columns before writing SQL
  - name: Queries
    description: Save, list, execute and review SQL
  - name: Reports
    description: Present a saved query as a form its audience fills in, without ever showing them the SQL
  - name: Schedules
    description: Schedule saved queries to export to Google Sheets or Slack
