Preview
Runs an event filter against BigQuery and returns a bounded sample of rows plus the total it matches.
Preview
Runs an event filter against BigQuery and returns a sample of the real rows it selects, plus the total. They are two routes with the same response: one runs a definition sent in the body and the other an already saved filter.
| Route | Definition source | Success code |
|---|---|---|
POST /api/event-criterias/preview-from-body | The body | 201 |
POST /api/event-criterias/:id/preview | The saved filter | 201 |
Auth: Required — event-criteria:view-sql permission on both routes
Unlike Validate, which does a
dry-run, preview runs the query: it consumes BigQuery quota and its cost shows up
in bytesProcessed.
POST /api/event-criterias/preview-from-body
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
queryBlocks | QueryBlockDto[] | Yes | At least 1 element. The definition to run. |
limit | number | No | Sample size. Integer between 1 and 500. Default 100. |
The structure of each block is in Definition.
This endpoint receives only the definition and the limit: name, description and
format are not part of the body and sending them produces a 400 for an unknown property.
Example
curl -X POST https://api.reten.ai/api/event-criterias/preview-from-body \
-H "Authorization: Bearer <token>" \
-H "x-tenant-id: <tenant-id>" \
-H "x-project-id: <project-id>" \
-H "Content-Type: application/json" \
-d '{
"limit": 50,
"queryBlocks": [
{
"order": 0,
"from": { "table": "event_transactions", "joins": [] },
"select": {
"distinct": true,
"items": [
{ "kind": "column", "column": { "table": "event_transactions", "column": "event_id" } },
{ "kind": "column", "column": { "table": "event_transactions", "column": "event_date" } }
]
},
"timeWindow": {
"timeOperator": "IN_THE_LAST",
"reference": { "table": "event_transactions", "column": "event_date" },
"unit": "DAYS",
"value": 30
},
"where": {
"kind": "group",
"logicalOperator": "AND",
"children": [
{
"kind": "predicate",
"column": { "table": "event_transactions", "column": "status" },
"comparisonOperator": "EQUALS",
"value": "Facturada"
}
]
}
}
]
}'import axios from 'axios';
const body = {
limit: 50,
queryBlocks: [
{
order: 0,
from: { table: 'event_transactions', joins: [] },
select: {
distinct: true,
items: [
{ kind: 'column', column: { table: 'event_transactions', column: 'event_id' } },
{ kind: 'column', column: { table: 'event_transactions', column: 'event_date' } },
],
},
timeWindow: {
timeOperator: 'IN_THE_LAST',
reference: { table: 'event_transactions', column: 'event_date' },
unit: 'DAYS',
value: 30,
},
where: {
kind: 'group',
logicalOperator: 'AND',
children: [
{
kind: 'predicate',
column: { table: 'event_transactions', column: 'status' },
comparisonOperator: 'EQUALS',
value: 'Facturada',
},
],
},
},
],
};
const response = await axios.post(
'https://api.reten.ai/api/event-criterias/preview-from-body',
body,
{ headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>', 'x-project-id': '<project-id>' } },
);
const { rows, totalCount, bytesProcessed } = response.data;POST /api/event-criterias/:id/preview
Runs an already saved filter. The definition is read from the filter and validated before running it.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | UUID | The event filter id. |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
limit | number | No | Sample size. Integer between 1 and 500. Default 100. |
The body does not carry the definition: the saved filter is the definition. Sending
queryBlocks produces a 400 for an unknown property.
Example
curl -X POST https://api.reten.ai/api/event-criterias/6d2a9c41-7e35-48b0-b1c9-0f8e7d6c5b4a/preview \
-H "Authorization: Bearer <token>" \
-H "x-tenant-id: <tenant-id>" \
-H "x-project-id: <project-id>" \
-H "Content-Type: application/json" \
-d '{ "limit": 50 }'import axios from 'axios';
const id = '6d2a9c41-7e35-48b0-b1c9-0f8e7d6c5b4a';
const response = await axios.post(
`https://api.reten.ai/api/event-criterias/${id}/preview`,
{ limit: 50 },
{ headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>', 'x-project-id': '<project-id>' } },
);
const { rows, totalCount, bytesProcessed } = response.data;limit — sample size
limit bounds only how many rows the response brings in rows (the sample), not the
universe that the filter selects.
- Range: integer 1..500 (less than 1 or greater than 500 ⇒
400). - Default:
100when omitted.
Preview runs two queries over the same definition: one
SELECT … LIMIT <limit> for the sample and one SELECT COUNT(*) for the total. That is
why you can see rows.length = 50 with totalCount = 4821: rows is a glance,
totalCount is the real number of rows the filter selects.
Response
Status 201 Created on both routes.
| Field | Type | Description |
|---|---|---|
rows | Record<string, unknown>[] | Sample of real rows, bounded by limit. The columns depend on the definition's select (and on its aliases); that is why rows has no fixed shape. |
totalCount | number | Total rows the filter selects, not bounded by limit. |
bytesProcessed | number | Bytes processed by BigQuery in the run. Sums the bytes of both queries (sample + count). |
Example
201 response to the example above (limit: 50). The filter selects 4821 events;
rows shows the first 50 (trimmed to 3 here for brevity), each with the
projected columns:
{
"rows": [
{ "event_id": "evt_9f3a1c", "event_date": { "value": "2026-07-14T18:02:11.000Z" } },
{ "event_id": "evt_2b7e04", "event_date": { "value": "2026-07-14T17:45:03.000Z" } },
{ "event_id": "evt_c081ff", "event_date": { "value": "2026-07-13T09:21:58.000Z" } }
],
"totalCount": 4821,
"bytesProcessed": 5242880
}The keys of each object in rows (event_id, event_date) come from the definition's
select: another definition produces other columns.
A TIMESTAMP column does not arrive as a string: it arrives wrapped as
{ "value": "<ISO 8601>" }. The ISO date is in row.event_date.value, not in
row.event_date. The other catalog types arrive as JSON scalars.
Validation Rules
Before executing, the definition goes through the two layers of validation. Business
validation runs on both routes: preview-from-body validates the definition in the
body, and :id/preview validates the saved filter before executing it.
The complete catalog of rules and their exact messages is in Definition → Validation Rules.
If the definition passes the two layers but the BigQuery execution fails —by
exceeding the billable bytes limit (10 GB), running out of the 30 s timeout, or any other
engine error— the response is also 400, with a message of the form
Query execution failed: <detail>.
Error Responses
| Status | Description |
|---|---|
400 | Invalid DTO (limit outside 1..500, wrong type, enum out of range, unknown property), a violated business rule, or a BigQuery execution failure (Query execution failed: …). In :id/preview, also if the id is not a valid UUID. |
401 | The Authorization: Bearer is missing or invalid. |
403 | The user does not have the event-criteria:view-sql permission, or the x-tenant-id is not authorized. |
404 | In :id/preview, no event filter with that id exists in the tenant (Event criteria not found). On both routes, if the tenant has no dataset configured. |