Querying data

The MCP server and the REST API read the same data through one model: a collection, its fields, a filter, a sort and a page of results. This page is that model, and every place the two differ. Both need a Bidlo account, and anyone can get one by booking a call.

Open in ChatGPTOpen in Claudellms.txtllms-full.txtopenapi.json

Contents

How is the data organised?

A key reads one team, a team sees collections, a collection holds documents, and every document carries a value for each of that collection’s fields. One document is one job, one contractor, one plant. A collection is one kind of them.

WordWhat it is
TeamYour company’s account in Bidlo. One API key reads one team and nothing else.
CollectionOne kind of thing: Projects holds jobs, Bid Items holds unit prices as bid, Contractors holds companies, Facilities holds plants, pits and yards.
DocumentOne of them: one job, one company, one plant.
FieldOne thing a collection knows about each of its documents, such as a name, a date, a quantity, or a link to a document in another collection.
ValueWhat one document holds in one field.

The MCP server reads every collection your team can see. The REST API names projects, contractors and facilities in its path, https://lite.bidlo.ai/api/v1/{collection_id}, and nothing else. The two name a collection differently as well: over REST it is the alias in the path, over MCP it is the collection’s name on query_database_data and its id on get_collection_fields. Names match case-insensitively over MCP, trimmed, and singular or plural.

How do I find the fields on a collection?

Call the MCP tool get_collection_fields with the collection ids you care about: it returns every field with its id, its name, its type and the operators that field will take. List the collections first with get_team_collections, then pass their ids in.

KeyWhat it tells you
field_idThe id a REST filter or sort names the field by.
nameYour team’s name for the field, which is what an MCP filter names it by.
field_typeHow the value is stored: text, number, date, tag, relation, location, state.
filter_field_typeHow the field is filtered, which is what the operator tables below are keyed on.
primary_keyWhether this is the field the document’s name comes from.
reference_collection_idFor a relation, the collection its value points at. That is the collection to resolve a name in.
description, akaWhat the field means, and other names it goes by.

The same answer carries the operator table for every filter type, the value shape for every operator, and the formula syntax, so one call tells an agent everything it needs to write a filter. Three field types are filtered as something else: a formula is filtered as whatever it works out to, a relation limited to a single document is filtered as a tag, and a type with no table of its own is filtered as text.

A team can rename a built-in field, so the same field can be called two things in two accounts. The id never changes. That is why no field id is printed on this page: read your own off get_collection_fields, and treat every field name in an example here as an example. It is also the fastest way to get ids for a REST call, because REST has no route that lists fields today. The other way is to read the field_id off any value in a REST response, which every value carries.

Coming. Two REST routes will list collections and their fields: /api/v1/collections and /api/v1/collections/{collection_id}/fields. Both are marked as coming in /openapi.json. Neither is live today. Until they are, field ids come from get_collection_fields over MCP.

How do filters work?

A filter is a field, an operator and a value, and one field takes one filter; over REST the field is named by its id, and over MCP by its name. Send them as an array; the operators a field will take depend on its filter type.

A REST filter
"filters": [
  {
    "field_id": "<field_id of your Bid Date field>",
    "operator": "between",
    "value": { "start": "2026-10-01", "end": "2026-12-31" },
    "conjunction": "AND"
  }
]
The same filter over MCP
"filters": [
  {
    "field_name": "Bid Date",
    "operator": "between",
    "value": { "start": "2026-10-01", "end": "2026-12-31" }
  }
]

Four rules hold on both sides. One field takes one filter: over REST a second filter on the same field is a 400 whose message says to combine the two, and over MCP there is no error at all: the last filter on that field quietly wins. is_null and is_not_null take no value. A filter whose value is empty (null, a blank string, an empty array, a half-filled { start, end }) is dropped rather than applied, so a query that looks unfiltered usually is. And a value in the wrong shape is a 400 that names the shape it wanted.

One rule holds on one side only. Over REST each filter carries its own conjunction, either AND (the default) or OR: every OR filter joins one group, and that group is ANDed with the AND group. There is no nesting and no parentheses. Over MCP there is no conjunction at all and every filter is ANDed.

The operators, in full: 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. Which of them a given field takes, and what its value must look like, is below.

title, text

A name, a description, a number written as words. The value is always a string. Over MCP, eq with a string value on a text field is treated as contains.

OperatorValue
eq, ne, contains, not_contains, starts_with, ends_withA string.
is_null, is_not_nullNo value.

number, material

A quantity, a price, an estimate. The value is a number, not a string.

OperatorValue
eq, ne, gt, lt, gte, lteA number.
between{ start, end }. REST only.
is_null, is_not_nullNo value.

date

A letting date, a bid date, an award date. The value is an ISO date string or a keyword. See the dates section below for the three shapes.

OperatorValue
eq, gt, lt, gte, lteAn ISO date string, or a keyword.
neAn ISO date string, or a keyword. REST only.
between{ start, end }
relative_range{ period, timeframe, number }
is_null, is_not_nullNo value.

boolean

A yes or no. The value is a boolean, not the string "true".

OperatorValue
eq, netrue or false.

tag

One option off a list, such as a status. A relation that is limited to a single document is filtered as a tag too.

OperatorValue
eq, neThe option’s document id. Over REST, an array of ids; over MCP, a name or an id.
is_null, is_not_nullNo value.

tags, relation, people

A link to one or more documents in another collection: the contractor on a job, the county it sits in, the agency that let it. A people field holds user ids instead.

OperatorValue
contains, not_contains, eq, neDocument ids. Over REST, an array of ids; over MCP, a name, an id, or an array of either.
is_null, is_not_nullNo value.

state

A state. The value is the full name, not the two-letter abbreviation.

OperatorValue
contains, not_containsFull state names, such as ["Texas"].
is_null, is_not_nullNo value.

location

An address with coordinates, such as a plant or a yard. REST also takes within_area and outside_area with a GeoJSON polygon; see the REST API page for that shape.

OperatorValue
within_distance, outside_distance{ lat, lng, radiusInMeters }
is_null, is_not_nullNo value.

How do dates work?

A date value is an ISO date string or a keyword such as today, between takes a start and an end, and relative_range takes a period, a timeframe and a number. A letting window is a between; "the next three months" is a relative_range.

Three date filters, shown over MCP
{ "field_name": "Bid Date", "operator": "gte", "value": "2026-10-01" }

{ "field_name": "Bid Date", "operator": "between",
  "value": { "start": "2026-10-01", "end": "2026-12-31" } }

{ "field_name": "Bid Date", "operator": "relative_range",
  "value": { "period": "next", "timeframe": "month", "number": 3 } }

Both surfaces take today, yesterday, tomorrow in place of a date. REST also takes last_week, next_week, last_month, next_month, last_year, next_year.

relative_range takes three keys. period is past, next or this; timeframe is day, week, month or year; and number is required for past and next, which is how many of that timeframe to reach back or forward. this needs no number.

How do I filter by a contractor, a county or an agency?

Those fields link to a document in another collection, so the value is that document’s id; over MCP you can send the name instead and the server resolves it for you. A county on a job is a document in the Counties collection, the same as any other link.

Over MCP, a name is enough
"filters": [
  { "field_name": "County", "operator": "contains", "value": "Travis County" }
]
Over REST, the value is an array of ids
"filters": [
  {
    "field_id": "<field_id of your County field>",
    "operator": "contains",
    "value": ["<document_id of that county>"]
  }
]

Over MCP the server looks a name up in the collection the field points at, which is the one named by reference_collection_id on the field, and uses the match. A value that is already an id is used as given. If the name matches more than one document, the call comes back with an error naming the matches and telling you to resolve it yourself.

That is what resolve_documents_by_name is for, and it is also how you get ids for a REST call. Give it the referenced collection’s id and either a query to search on or a list of names to look up, and it answers with exact matches and near matches, each as an id and a name. It reads the first 10 names you send and returns 1 to 25 rows per name, 10 by default. It needs one of the two inputs: with neither, it errors.

Carry the ids you get back into the filter. Over REST a relation value is always an array of ids, even when there is one of them, and a bare id is read as an array of one.

How do I search near a place?

Filter a location field with within_distance and a value of lat, lng and radiusInMeters, where the radius is in metres. outside_distance is the same filter inverted.

Plants, pits and yards within 50 km of a point
"filters": [
  {
    "field_name": "Location",
    "operator": "within_distance",
    "value": { "lat": 30.2672, "lng": -97.7431, "radiusInMeters": 50000 }
  }
]

The radius is metres, so 50 km is 50000. A job’s own coordinates make the centre of the search when you are looking for who can reach it. is_null and is_not_null on the same field are how you tell which documents carry coordinates at all.

REST can also filter a location against a drawn area rather than a radius. The REST API page carries that shape.

How do sorts work?

A sort is a field and a direction of asc or desc, several are allowed in the order you send them, and with none the newest documents come first. A sort names its field the same way a filter does: by id over REST, by name over MCP.

One sort, on both surfaces
REST   "sorts": [{ "field_id": "<field_id of your Bid Date field>", "direction": "asc" }]

MCP    "sorts": [{ "field_name": "Bid Date", "direction": "asc" }]

Send several and they apply in the order of the array, so the first is the primary sort. One field takes one sort. Sorting on a location field is a sort by distance and needs a point to measure from, which REST takes on the sort itself. With no sorts at all the newest documents come first.

How do pages work?

Ask for a limit and a page number, starting at page 1; neither surface returns a total count, so you read until a page comes back short. The two surfaces allow different sizes.

limitpageKnowing when you are done
MCP1 to 100, 25 by defaultFrom 1, up to 20has_more and next_page come back with the results
REST1 to 2000, 100 by defaultFrom 1, echoed back in metaNothing is returned: read on until a page comes back short

A limit outside the range is refused rather than trimmed to fit: over REST it is a 400, and over MCP it is an input validation error on the tool call. Neither surface returns a total count of matching documents, and neither has a cursor, so paging is a page number you raise until the results run out.

What comes back?

A page of documents, each with its id, its name and one value per field, where the shape of a value follows the field’s type. The two surfaces key a document’s fields differently: REST by field id, MCP by field name.

A REST response
{
  "data": [
    {
      "id": "<document_id>",
      "name": "…",
      "collection_id": "<collection_id>",
      "created_at": "…",
      "fields": {
        "<field_id>": {
          "value": "2026-10-14",
          "field_name": "Bid Date",
          "field_type": "date",
          "field_id": "<field_id>"
        }
      }
    }
  ],
  "meta": { "page": 1, "collection_id": "<collection_id>" }
}
An MCP result
{
  "collection_id": "<collection_id>",
  "collection_name": "Projects",
  "page": 1,
  "selected_fields": [{ "field_id": "<field_id>", "field_name": "Bid Date" }],
  "documents": [
    {
      "document_id": "<document_id>",
      "document_name": "…",
      "fields": { "Bid Date": "2026-10-14" }
    }
  ],
  "returned_row_count": 1,
  "has_more": true,
  "next_page": 2
}

Over MCP, ask for the fields you want by name in return_fields. Leave it out and a capped default set of up to 8 fields comes back with a warning saying so, which is rarely the set you wanted. A text value longer than 400 characters is cut short. Over REST, fields takes a list of field ids and does the same job; leave it out and every field comes back.

The shape of a value follows the field’s type. These are the REST shapes; over MCP a linked document comes back as an id, a name and a mention.

Field typeValue
text, number, booleanAs stored.
dateAn ISO date string.
tag, a relation limited to one documentAn object with an id, a value and a document_id.
tags, a relation to manyAn array of those objects.
locationAn object with an id, a name, an address, a lat and a lng.
stateAn object with an id, a name and an abbreviation.
peopleIds.

One advanced case changes the shape of the whole answer: REST takes a grouping of a field id and a field type, and the response then comes back as groups of documents rather than a flat list, with page selecting groups. The REST API page covers it.

Where do I go next?

The MCP server page has the tools and their inputs, the REST API page has the one route and its parameters, and the access page has the key. Everything on this page is true of both; the reference pages carry what is true of one.

  • MCP server: the five tools, what each takes and returns, how to connect a client, and how errors come back.
  • REST API: the one route, its parameters, the response envelope and the status codes, plus /openapi.json.
  • Access and keys: how to get a key and how to send it.

If a read you need is not in the model above, mail support@bidlo.ai.

A Bidlo account is required. Anyone can get one by booking a call at https://cal.com/matt-wolfe-yecrho/30min — Bidlo sets the team up, and a team owner or admin then mints the API key in Settings → Features → API. There is no public tier and no self-serve signup.