Documentation Index
Fetch the complete documentation index at: https://docs.grainql.com/llms.txt
Use this file to discover all available pages before exploring further.
Endpoint
POST /v1/api/query/{tenantId}
Query events with optional filters and pagination. Returns an array of event objects matching your criteria.
Parameters
Path Parameters
| Parameter | Type | Description |
|---|
tenantId | string | Your tenant identifier (use the alias shown on your dashboard) |
Request Body
| Field | Type | Required | Description |
|---|
event | string | No | Filter by specific event name |
after | string | No | Start date (YYYY-MM-DD format) |
before | string | No | End date (YYYY-MM-DD format) |
filterSet | array | No | Array of filter objects |
pagination | object | No | Pagination settings |
Filter Object
| Field | Type | Required | Description |
|---|
property | string | Yes | Property path to filter on |
comparison | string | Yes | Comparison operator |
value | any | Yes | Value to compare against |
| Field | Type | Required | Description |
|---|
offset | number | No | Number of records to skip (default: 0) |
size | number | No | Maximum records to return (default: 100, max: 1000) |
Response
Returns an array of event objects:
[
{
"eventName": "page_viewed",
"userId": "user_123",
"eventTs": "2024-01-15T10:30:00Z",
"properties": {
"page": "/home",
"referrer": "https://google.com",
"device": "desktop"
},
"eventDate": "2024-01-15",
"insertId": "unique-event-id"
}
]
Event Object Fields
| Field | Type | Description |
|---|
eventName | string | The type of event |
userId | string | User who performed the action |
eventTs | string | ISO 8601 timestamp |
properties | object | Custom event properties |
eventDate | string | Date portion (YYYY-MM-DD) |
insertId | string | Unique event identifier |
Examples
Basic Query
Get recent page view events:
curl -X POST https://queryapis.grainql.com/v1/api/query/your-tenant-id \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_SECRET_KEY" \
-d '{
"event": "page_viewed",
"after": "2024-01-01",
"before": "2024-01-31",
"pagination": { "offset": 0, "size": 100 }
}'
Filtered Query
Get high-value purchases:
curl -X POST https://queryapis.grainql.com/v1/api/query/your-tenant-id \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_SECRET_KEY" \
-d '{
"event": "purchase_completed",
"filterSet": [
{
"property": "properties.price",
"comparison": "GREATER_THAN",
"value": 100
}
],
"pagination": { "offset": 0, "size": 50 }
}'
Complex Filters
Get events from specific users on mobile devices:
curl -X POST https://queryapis.grainql.com/v1/api/query/your-tenant-id \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_SECRET_KEY" \
-d '{
"filterSet": [
{
"property": "userId",
"comparison": "IN",
"value": ["user_123", "user_456", "user_789"]
},
{
"property": "properties.device",
"comparison": "EQUALS",
"value": "mobile"
}
],
"pagination": { "offset": 0, "size": 200 }
}'
Date Range Query
Get all events from last week:
curl -X POST https://queryapis.grainql.com/v1/api/query/your-tenant-id \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_SECRET_KEY" \
-d '{
"after": "2024-01-08",
"before": "2024-01-15",
"pagination": { "offset": 0, "size": 1000 }
}'
Code Examples
JavaScript/TypeScript
async function queryEvents(tenantId: string, apiKey: string, filters: any) {
const response = await fetch(`https://queryapis.grainql.com/v1/api/query/${tenantId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey
},
body: JSON.stringify(filters)
});
if (!response.ok) {
throw new Error(`Query failed: ${response.status} ${response.statusText}`);
}
return response.json();
}
// Usage
const events = await queryEvents('your-tenant-id', 'your-api-key', {
event: 'button_clicked',
filterSet: [
{ property: 'properties.button_name', comparison: 'EQUALS', value: 'signup' }
],
pagination: { offset: 0, size: 100 }
});
console.log(`Found ${events.length} signup button clicks`);
Python
import requests
from typing import List, Dict, Any
def query_events(tenant_id: str, api_key: str, filters: Dict[str, Any]) -> List[Dict]:
"""Query events with filters and pagination"""
response = requests.post(
f'https://queryapis.grainql.com/v1/api/query/{tenant_id}',
headers={
'Content-Type': 'application/json',
'X-API-Key': api_key
},
json=filters
)
response.raise_for_status()
return response.json()
# Usage
events = query_events('your-tenant-id', 'your-api-key', {
'event': 'purchase_completed',
'filterSet': [
{
'property': 'properties.price',
'comparison': 'GREATER_THAN',
'value': 50
}
],
'pagination': {'offset': 0, 'size': 100}
})
print(f'Found {len(events)} high-value purchases')
Node.js
const fetch = require('node-fetch');
async function queryEvents(tenantId, apiKey, filters) {
const response = await fetch(`https://queryapis.grainql.com/v1/api/query/${tenantId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey
},
body: JSON.stringify(filters)
});
if (!response.ok) {
throw new Error(`Query failed: ${response.status} ${response.statusText}`);
}
return response.json();
}
// Usage
const events = await queryEvents('your-tenant-id', process.env.GRAIN_API_KEY, {
event: 'page_viewed',
after: '2024-01-01',
before: '2024-01-31',
pagination: { offset: 0, size: 100 }
});
console.log(`Found ${events.length} page views in January`);
For large datasets, use pagination to fetch data in chunks:
async function fetchAllEvents(tenantId: string, apiKey: string, filters: any) {
const allEvents = [];
let offset = 0;
const pageSize = 1000;
while (true) {
const response = await fetch(`https://queryapis.grainql.com/v1/api/query/${tenantId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey
},
body: JSON.stringify({
...filters,
pagination: { offset, size: pageSize }
})
});
const events = await response.json();
allEvents.push(...events);
// If we got fewer events than requested, we've reached the end
if (events.length < pageSize) {
break;
}
offset += pageSize;
}
return allEvents;
}
Property Paths
You can filter on various event properties:
Event Properties
Access custom properties you included when tracking events:
{
"property": "properties.price",
"comparison": "GREATER_THAN",
"value": 100
}
{
"property": "properties.category",
"comparison": "EQUALS",
"value": "electronics"
}
Filter on built-in event fields:
{
"property": "eventName",
"comparison": "EQUALS",
"value": "purchase_completed"
}
{
"property": "userId",
"comparison": "EQUALS",
"value": "user_123"
}
Nested Properties
Access nested object properties:
{
"property": "properties.user.plan",
"comparison": "EQUALS",
"value": "premium"
}
1. Use Date Ranges
Always include date ranges to limit the data scanned:
{
"after": "2024-01-01",
"before": "2024-01-31"
}
2. Limit Result Size
Use pagination to avoid fetching too much data at once:
{
"pagination": { "offset": 0, "size": 100 }
}
3. Use Specific Filters
Be specific with your filters to reduce the dataset:
{
"event": "purchase_completed",
"filterSet": [
{ "property": "properties.price", "comparison": "GREATER_THAN", "value": 100 }
]
}
4. Consider Using Count Endpoint
For aggregations, use the count endpoint instead:
// ✅ Good: Use count for totals
const response = await fetch('/v1/api/query/count/tenant', {
method: 'POST',
body: JSON.stringify({ event: 'purchase_completed' })
});
const { count } = await response.json();
// ❌ Avoid: Fetching all events just to count them
const events = await fetch('/v1/api/query/tenant', {
method: 'POST',
body: JSON.stringify({ event: 'purchase_completed', pagination: { size: 10000 } })
});
const count = (await events.json()).length;
Error Handling
Handle common error scenarios:
async function queryEventsWithErrorHandling(tenantId: string, apiKey: string, filters: any) {
try {
const response = await fetch(`https://queryapis.grainql.com/v1/api/query/${tenantId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey
},
body: JSON.stringify(filters)
});
if (response.status === 401) {
throw new Error('Invalid API key. Check your authentication settings.');
}
if (response.status === 403) {
throw new Error('Insufficient plan tier. Upgrade to Builder plan or higher.');
}
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
throw new Error(`Rate limit exceeded. Retry after ${retryAfter} seconds.`);
}
if (!response.ok) {
const error = await response.json();
throw new Error(`Query failed: ${error.error || response.statusText}`);
}
return response.json();
} catch (error) {
console.error('Query API error:', error);
throw error;
}
}
Next Steps
Count Events
Get event counts for aggregations
Filter Reference
Complete guide to all filter operators
Custom Dashboard
Build a custom analytics dashboard
Data Export
Export data for analysis