Querying contracts
Send a structured query to a contract and get back governed rows.
The REST query API is the precise way to read a contract. You send a JSON request describing the dimensions, measures, filters, and sorting you want; Gaur returns governed rows. It's the right choice for dashboards and application features that need exact, repeatable results.
The endpoint
POST /v1/api/{consumer_slug}/query
Authorization: Bearer <api_key>
Content-Type: application/jsonFor the field-by-field schema, see the Query endpoint reference and the Query schema reference.
The request
A request has three parts:
{
"contract_name": "product_performance",
"query": {
"dimensions": [
{ "name": "ordered_at", "time_grain": "month" },
{ "name": "category" }
],
"measures": ["revenue_usd", "cogs_ratio"],
"filters": [
{ "field": "category", "operator": "eq", "value": "jaffle" },
{ "field": "ordered_at", "operator": "gte", "value": "2026-01-01" }
],
"sorts": [{ "field": "ordered_at", "direction": "asc" }],
"limit": 100,
"offset": 0
},
"context": { "store_id": "STR-008" }
}| Field | Purpose |
|---|---|
contract_name | The contract to query. Must be one the consumer exposes. |
query | What to select: dimensions, measures, filters, sorts, paging. |
context | Values used by the contract's row-level security. Optional. |
Building the query
Everything in query refers to dimensions and measures declared by the
contract. You can't select a column the contract doesn't expose; that's
what makes the result governed.
dimensions
The attributes to group by. Each entry has a name; a time dimension can
also take a time_grain:
"dimensions": [
{ "name": "ordered_at", "time_grain": "month" },
{ "name": "category" }
]time_grain is one of hour, day, week, month, quarter, year,
and has to be a grain the contract declared.
measures
A list of measure names to aggregate:
"measures": ["revenue_usd", "units_sold", "cogs_ratio"]filters
Conditions applied to the result. Each filter has a field, an
operator, and a value:
"filters": [
{ "field": "category", "operator": "in", "value": ["jaffle", "beverage"] },
{ "field": "revenue_usd", "operator": "gte", "value": 1000 }
]A few important caveats (full list in the query schema reference):
- Measure filters compile to
HAVING. Putting a measure infieldfilters the aggregated result, not the raw rows. in/not_inrequire non-empty arrays. An empty[]is rejected.- Multiple filters are ANDed. No
OR; useinor split into two queries.
For date ranges, prefer inclusive start, exclusive end so adjacent months don't double-count boundary rows:
"filters": [
{ "field": "ordered_at", "operator": "gte", "value": "2026-04-01" },
{ "field": "ordered_at", "operator": "lt", "value": "2026-05-01" }
]sorts
Ordering for the result. Each entry has a field and a direction:
"sorts": [{ "field": "revenue_usd", "direction": "desc" }]direction is asc or desc. Always specify a sort if you intend to
page; without one the order can shift between calls and pagination
becomes unreliable.
limit and offset
limit caps the number of rows returned; offset skips rows for paging.
Gaur clamps limit to 10,000. Asking for more silently returns
10,000 with pagination.limit: 10000 in the response. See
Pagination.
Row-level security context
If the contract uses
row-level security,
it needs values to decide which rows your request can see. Some come from
the API key (auth context, fixed per key). Others you supply per request
in the context field:
"context": { "store_id": "STR-008" }You send request context when one API key serves many end users, like a
customer-facing dashboard. Your application authenticates the user, your
backend derives the value that scopes them, and you put it in context.
Set request context on your trusted backend, never from the end user.
If a user can choose their own store_id, they can read other stores'
data. Derive the value from the authenticated session server-side, and
don't let the client set it or see it.
A caller can't widen its own scope. context only ever narrows results
to what the contract's RLS allows.
The response
A successful query returns 200 with a paginated result. Rows live at
data.pagination.items (not at the top of data):
{
"data": {
"pagination": {
"items": [
{ "ordered_at": "2026-04-01T00:00:00", "category": "jaffle", "revenue_usd": 84210.50, "cogs_ratio": 0.42 },
{ "ordered_at": "2026-04-01T00:00:00", "category": "beverage", "revenue_usd": 39780.25, "cogs_ratio": 0.18 }
],
"total_items": 24,
"page": 1,
"limit": 100,
"total_pages": 1,
"has_next_page": false,
"has_prev_page": false
},
"execution_time_ms": 42
},
"message": null
}data.pagination.items: the rows, one object per group, keyed by your selected dimensions and measures.data.pagination: paging metadata. See Pagination for the full envelope and the 10k clamp.data.execution_time_ms: query execution time in milliseconds.
A minimal query
You don't have to use every field. The smallest useful query is a contract name and a measure:
{
"contract_name": "product_performance",
"query": { "measures": ["revenue_usd"] }
}That returns one row: total revenue_usd across every product, store,
and date the contract reaches.