{
  "openapi": "3.1.0",
  "info": {
    "title": "Bidlo Market Data API",
    "version": "1.0.0",
    "summary": "Heavy civil lettings, bid items, contractors and facilities, read by key.",
    "description": "Bidlo holds public heavy civil bid history — jobs from advertisement through award, the bid items under them, the contractors that bid and the plants and yards that supply them. This is the REST half; the MCP server at /api/mcp is the half an agent should reach for first. Both are read-only and both authenticate with one key. Written for agents at https://www.bidlo.ai/developers.",
    "termsOfService": "https://www.bidlo.ai/terms",
    "contact": {
      "name": "Bidlo",
      "url": "https://www.bidlo.ai/developers",
      "email": "support@bidlo.ai"
    }
  },
  "externalDocs": {
    "description": "Developers",
    "url": "https://www.bidlo.ai/developers"
  },
  "servers": [
    {
      "url": "https://lite.bidlo.ai",
      "description": "Bidlo"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Documents",
      "description": "Read a page of documents out of one collection."
    },
    {
      "name": "MCP",
      "description": "The Model Context Protocol server."
    },
    {
      "name": "Discovery",
      "description": "Collections and their fields. Not live yet."
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "A team API key: `sk_live_` followed by 48 hex characters. A team owner or admin mints one in the app under Settings → Features → API. Keys are hashed at rest and shown once."
      }
    },
    "schemas": {
      "Filter": {
        "type": "object",
        "required": [
          "field_id",
          "operator"
        ],
        "properties": {
          "field_id": {
            "type": "string",
            "description": "The id of the field to filter on. Read it off the MCP server’s get_collection_fields tool; there is no REST field listing yet."
          },
          "operator": {
            "type": "string",
            "enum": [
              "is_null",
              "is_not_null",
              "eq",
              "ne",
              "contains",
              "not_contains",
              "starts_with",
              "ends_with",
              "gt",
              "lt",
              "gte",
              "lte",
              "between",
              "relative_range",
              "within_distance",
              "outside_distance"
            ]
          },
          "value": {
            "description": "Shape follows the field type: a string or number for text and number fields, {start, end} for a date between, {lat, lng, radiusInMeters} for within_distance and outside_distance, an array of document ids for tag, tags and relation fields. Omitted for is_null and is_not_null."
          },
          "conjunction": {
            "type": "string",
            "enum": [
              "and",
              "or"
            ],
            "default": "and"
          }
        },
        "additionalProperties": false
      },
      "Sort": {
        "type": "object",
        "required": [
          "field_id",
          "direction"
        ],
        "properties": {
          "field_id": {
            "type": "string"
          },
          "direction": {
            "type": "string",
            "enum": [
              "asc",
              "desc"
            ]
          }
        },
        "additionalProperties": false
      },
      "Grouping": {
        "type": "object",
        "required": [
          "field_id",
          "field_type"
        ],
        "properties": {
          "field_id": {
            "type": "string"
          },
          "field_type": {
            "type": "string"
          },
          "visible_groups": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "additionalProperties": false
      },
      "Document": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "collection_id": {
            "type": "string"
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": "string"
          },
          "fields": {
            "type": "object",
            "description": "Keyed by field id. Dates come back as ISO 8601 strings, locations as {id, name, address, lat, lng}, states as {id, name, abbreviation}.",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "value": {},
                "field_name": {
                  "type": "string"
                },
                "field_type": {
                  "type": "string"
                },
                "field_id": {
                  "type": "string"
                },
                "field_value_id": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "DocumentList": {
        "type": "object",
        "required": [
          "data",
          "meta"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "collection_id": {
                  "type": "string"
                },
                "created_at": {
                  "type": "string"
                },
                "updated_at": {
                  "type": "string"
                },
                "fields": {
                  "type": "object",
                  "description": "Keyed by field id. Dates come back as ISO 8601 strings, locations as {id, name, address, lat, lng}, states as {id, name, abbreviation}.",
                  "additionalProperties": {
                    "type": "object",
                    "properties": {
                      "value": {},
                      "field_name": {
                        "type": "string"
                      },
                      "field_type": {
                        "type": "string"
                      },
                      "field_id": {
                        "type": "string"
                      },
                      "field_value_id": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "meta": {
            "type": "object",
            "properties": {
              "page": {
                "type": "integer"
              },
              "collection_id": {
                "type": "string"
              },
              "groupPagination": {
                "type": "object"
              }
            }
          }
        }
      }
    }
  },
  "paths": {
    "/api/v1/{collection_id}": {
      "get": {
        "tags": [
          "Documents"
        ],
        "operationId": "listDocuments",
        "summary": "Read a page of documents",
        "description": "The same read as POST, with every structured argument passed as a JSON string in the query. Prefer POST for anything with a filter in it.",
        "parameters": [
          {
            "name": "collection_id",
            "in": "path",
            "required": true,
            "description": "projects, contractors or facilities. `sources` is a deprecated alias of facilities. Anything else answers 404.",
            "schema": {
              "type": "string",
              "enum": [
                "projects",
                "contractors",
                "facilities",
                "sources"
              ]
            }
          },
          {
            "name": "team_id",
            "in": "query",
            "required": false,
            "description": "The team to read. An API key carries its own team and this is ignored; it is required only on a session-cookie request.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "JSON array of field ids.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sorts",
            "in": "query",
            "description": "JSON array of Sort objects.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filters",
            "in": "query",
            "description": "JSON array of Filter objects.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "grouping",
            "in": "query",
            "description": "JSON Grouping object.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 2000,
              "default": 100
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "query",
            "in": "query",
            "description": "Free-text search across the collection.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of documents.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentList"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request. A limit outside 1–2000, a filter value of the wrong shape, or two filters on one field.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "details": {}
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "details": {}
                  }
                }
              }
            }
          },
          "403": {
            "description": "The team’s subscription has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "details": {}
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown collection.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "details": {}
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Documents"
        ],
        "operationId": "queryDocuments",
        "summary": "Query a collection",
        "description": "Filters, sorts and grouping as JSON. One filter per field: a second filter on the same field is a 400, so combine them into one.",
        "parameters": [
          {
            "name": "collection_id",
            "in": "path",
            "required": true,
            "description": "projects, contractors or facilities. `sources` is a deprecated alias of facilities. Anything else answers 404.",
            "schema": {
              "type": "string",
              "enum": [
                "projects",
                "contractors",
                "facilities",
                "sources"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "team_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The team to read. An API key carries its own team and this is ignored; it is required only on a session-cookie request."
                  },
                  "fields": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "sorts": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/Sort"
                    }
                  },
                  "filters": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/Filter"
                    }
                  },
                  "grouping": {
                    "$ref": "#/components/schemas/Grouping"
                  },
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 2000,
                    "default": 100
                  },
                  "page": {
                    "type": "integer",
                    "minimum": 1,
                    "default": 1
                  },
                  "query": {
                    "type": "string"
                  }
                },
                "additionalProperties": false
              },
              "examples": {
                "nearAustin": {
                  "summary": "Asphalt plants within 50 km of Austin",
                  "value": {
                    "filters": [
                      {
                        "field_id": "FACILITY_LOCATION_FIELD_ID",
                        "operator": "within_distance",
                        "value": {
                          "lat": 30.2672,
                          "lng": -97.7431,
                          "radiusInMeters": 50000
                        }
                      }
                    ],
                    "limit": 100
                  }
                },
                "lettingWindow": {
                  "summary": "Jobs letting this quarter",
                  "value": {
                    "filters": [
                      {
                        "field_id": "PROJECT_BID_DATE_FIELD_ID",
                        "operator": "between",
                        "value": {
                          "start": "2026-10-01",
                          "end": "2026-12-31"
                        }
                      }
                    ],
                    "sorts": [
                      {
                        "field_id": "PROJECT_BID_DATE_FIELD_ID",
                        "direction": "asc"
                      }
                    ],
                    "limit": 100
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A page of documents.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentList"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request. A limit outside 1–2000, a filter value of the wrong shape, or two filters on one field.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "details": {}
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "details": {}
                  }
                }
              }
            }
          },
          "403": {
            "description": "The team’s subscription has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "details": {}
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown collection.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "details": {}
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/mcp": {
      "post": {
        "tags": [
          "MCP"
        ],
        "operationId": "mcp",
        "summary": "Model Context Protocol server",
        "description": "A streamable-HTTP MCP server, stateless, answering JSON-RPC 2.0 over POST. Five read tools: get_team_collections, get_collection_fields, resolve_documents_by_name, query_database_data, query_mentioned_document. There is no OAuth handshake — discovery probes answer 404 on purpose so a client falls through to the bearer key.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "properties": {
                  "jsonrpc": {
                    "type": "string",
                    "const": "2.0"
                  },
                  "id": {
                    "type": [
                      "string",
                      "integer"
                    ]
                  },
                  "method": {
                    "type": "string"
                  },
                  "params": {
                    "type": "object"
                  }
                }
              },
              "examples": {
                "listTools": {
                  "summary": "List the tools",
                  "value": {
                    "jsonrpc": "2.0",
                    "id": 1,
                    "method": "tools/list"
                  }
                },
                "callTool": {
                  "summary": "Query a collection",
                  "value": {
                    "jsonrpc": "2.0",
                    "id": 2,
                    "method": "tools/call",
                    "params": {
                      "name": "query_database_data",
                      "arguments": {
                        "collection_name": "Project",
                        "filters": [
                          {
                            "field_name": "County",
                            "operator": "contains",
                            "value": [
                              "COUNTY_DOCUMENT_ID"
                            ]
                          }
                        ],
                        "limit": 25
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A JSON-RPC response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jsonrpc": {
                      "type": "string"
                    },
                    "id": {
                      "type": [
                        "string",
                        "integer"
                      ]
                    },
                    "result": {
                      "type": "object"
                    },
                    "error": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key. The body is JSON-RPC error -32001.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "details": {}
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/collections": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "operationId": "listCollections",
        "summary": "List collections",
        "x-status": "coming",
        "description": "Not live yet. Use the MCP server’s get_team_collections tool today.",
        "responses": {
          "200": {
            "description": "The collections this key can read.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "details": {}
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/collections/{collection_id}/fields": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "operationId": "listCollectionFields",
        "summary": "List a collection’s fields",
        "x-status": "coming",
        "description": "Not live yet. Use the MCP server’s get_collection_fields tool today — it returns the field ids a REST filter needs, plus the operators each field takes.",
        "parameters": [
          {
            "name": "collection_id",
            "in": "path",
            "required": true,
            "description": "projects, contractors or facilities. `sources` is a deprecated alias of facilities. Anything else answers 404.",
            "schema": {
              "type": "string",
              "enum": [
                "projects",
                "contractors",
                "facilities",
                "sources"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The fields on that collection.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "field_id": {
                            "type": "string"
                          },
                          "field_name": {
                            "type": "string"
                          },
                          "field_type": {
                            "type": "string"
                          },
                          "operators": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "details": {}
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown collection.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "details": {}
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
