What is the Query API?
The Grain Query API allows you to programmatically access your analytics data using simple HTTP requests. While the Grain Dashboard provides a powerful visual interface for exploring your data, the Query API enables you to:
Build custom analytics dashboards tailored to your specific needs
Export data for external analysis or reporting
Integrate analytics into your own applications and workflows
Automate data processing with scheduled scripts and pipelines
Quick Start
Get up and running with the Query API in 3 steps:
Create an API Key
Navigate to your Dashboard Settings and create a secret with Query API permissions:
Go to Dashboard → Settings → Authentication
Click Generate New Secret
Enter a name like “Query API Key”
Check the “Query API” permission
Copy the generated secret (you won’t see it again!)
Authentication Strategy : The Query API authentication is independent of your SDK authentication strategy. You can use Query API keys regardless of whether your SDK is set to NONE, SERVER_SIDE, or JWT authentication.
Store your API key securely in environment variables. Never commit it to version control.
Make Your First Request
Query your events with a simple HTTP request: 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": 10 }
}'
This returns the last 10 page view events from January 2024.
Explore Your Data
Use the Query API to build custom analytics: // Get all events for a specific user
const userEvents = await fetch ( 'https://queryapis.grainql.com/v1/api/query/your-tenant-id' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
'X-API-Key' : process . env . GRAIN_API_KEY
},
body: JSON . stringify ({
filterSet: [
{ property: 'userId' , comparison: 'EQUALS' , value: 'user_123' }
],
pagination: { offset: 0 , size: 100 }
})
});
const events = await userEvents . json ();
console . log ( `Found ${ events . length } events for user` );
When to Use the Query API
✅ Perfect for:
Custom Dashboards : Build analytics views that match your specific workflow
Data Exports : Extract data for external analysis, reporting, or compliance
Integrations : Connect Grain data to other tools (Slack, email, BI platforms)
Automation : Scheduled reports, alerts, and data processing pipelines
Advanced Analysis : Complex queries that go beyond the dashboard interface
❌ Consider the Dashboard instead for:
Exploratory Analysis : The dashboard’s visual interface is great for discovering insights
One-off Queries : Quick questions that don’t need automation
Real-time Monitoring : The dashboard provides live event streams
Understanding Your Data
Before using the Query API, it helps to understand how your analytics data is structured:
Event Structure
Every event in Grain has this structure:
{
"eventName" : "button_clicked" ,
"userId" : "user_123" ,
"eventTs" : "2024-01-15T10:30:00Z" ,
"properties" : {
"button_name" : "signup" ,
"page" : "/home" ,
"referrer" : "https://google.com"
},
"eventDate" : "2024-01-15" ,
"insertId" : "unique-event-id"
}
Key Fields
eventName : The type of event (e.g., “page_viewed”, “purchase_completed”)
userId : The user who performed the action (set via grain.identify() or grain.setUserId())
eventTs : When the event occurred (ISO 8601 timestamp)
properties : Custom data you included when tracking the event
eventDate : Date portion for efficient querying
insertId : Unique identifier for the event
Property Paths
When filtering events, you can query:
Event properties : properties.price, properties.category
Event metadata : eventName, eventTs, userId
Date fields : Use after and before parameters for date ranges
Common Use Cases
1. Custom Analytics Dashboard
Build a dashboard that shows exactly what you need:
// Get conversion funnel data
const funnelData = await Promise . all ([
// Step 1: Page views
queryEvents ({
event: 'page_viewed' ,
filterSet: [{ property: 'properties.page' , comparison: 'EQUALS' , value: '/signup' }]
}),
// Step 2: Form submissions
queryEvents ({
event: 'form_submitted' ,
filterSet: [{ property: 'properties.form_type' , comparison: 'EQUALS' , value: 'signup' }]
}),
// Step 3: Account creations
queryEvents ({
event: 'account_created'
})
]);
// Calculate conversion rates
const pageViews = funnelData [ 0 ]. length ;
const formSubmissions = funnelData [ 1 ]. length ;
const accounts = funnelData [ 2 ]. length ;
console . log ( `Conversion rate: ${ ( accounts / pageViews * 100 ). toFixed ( 1 ) } %` );
2. Data Export for Analysis
Export data for external analysis:
import requests
import csv
from datetime import datetime, timedelta
def export_recent_events ( tenant_id , api_key , days = 30 ):
"""Export events from the last N days to CSV"""
end_date = datetime.now()
start_date = end_date - timedelta( days = days)
all_events = []
offset = 0
page_size = 1000
while True :
response = requests.post(
f 'https://queryapis.grainql.com/v1/api/query/ { tenant_id } ' ,
headers = {
'Content-Type' : 'application/json' ,
'X-API-Key' : api_key
},
json = {
'after' : start_date.strftime( '%Y-%m- %d ' ),
'before' : end_date.strftime( '%Y-%m- %d ' ),
'pagination' : { 'offset' : offset, 'size' : page_size}
}
)
events = response.json()
if not events:
break
all_events.extend(events)
offset += page_size
if len (events) < page_size:
break
# Write to CSV
with open ( 'analytics_export.csv' , 'w' , newline = '' ) as csvfile:
if all_events:
writer = csv.DictWriter(csvfile, fieldnames = all_events[ 0 ].keys())
writer.writeheader()
writer.writerows(all_events)
return len (all_events)
# Export last 30 days
event_count = export_recent_events( 'your-tenant-id' , 'your-api-key' )
print ( f 'Exported { event_count } events to analytics_export.csv' )
3. User Behavior Analysis
Analyze user journeys and behavior patterns:
// Get all events for a specific user to understand their journey
async function analyzeUserJourney ( tenantId : string , apiKey : string , userId : string ) {
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 ({
filterSet: [
{ property: 'userId' , comparison: 'EQUALS' , value: userId }
],
pagination: { offset: 0 , size: 1000 }
})
});
const events = await response . json ();
// Sort by timestamp
events . sort (( a , b ) => new Date ( a . eventTs ). getTime () - new Date ( b . eventTs ). getTime ());
// Analyze the journey
const journey = events . map ( event => ({
timestamp: event . eventTs ,
event: event . eventName ,
page: event . properties ?. page ,
action: event . properties ?. action
}));
console . log ( `User ${ userId } journey:` , journey );
return journey ;
}
4. Real-time Monitoring
Set up monitoring and alerts:
import time
import requests
from datetime import datetime
def monitor_error_rates ( tenant_id , api_key , threshold = 0.05 ):
"""Monitor error rates and alert if they exceed threshold"""
# Count total events in last hour
total_response = requests.post(
f 'https://queryapis.grainql.com/v1/api/query/count/ { tenant_id } ' ,
headers = { 'X-API-Key' : api_key},
json = {
'after' : (datetime.now() - timedelta( hours = 1 )).strftime( '%Y-%m- %d ' ),
'before' : datetime.now().strftime( '%Y-%m- %d ' )
}
)
total_events = total_response.json()[ 'count' ]
# Count error events
error_response = requests.post(
f 'https://queryapis.grainql.com/v1/api/query/count/ { tenant_id } ' ,
headers = { 'X-API-Key' : api_key},
json = {
'event' : 'error_occurred' ,
'after' : (datetime.now() - timedelta( hours = 1 )).strftime( '%Y-%m- %d ' ),
'before' : datetime.now().strftime( '%Y-%m- %d ' )
}
)
error_events = error_response.json()[ 'count' ]
error_rate = error_events / total_events if total_events > 0 else 0
if error_rate > threshold:
print ( f "🚨 High error rate detected: { error_rate :.2%} " )
# Send alert (Slack, email, etc.)
else :
print ( f "✅ Error rate normal: { error_rate :.2%} " )
# Run every 5 minutes
while True :
monitor_error_rates( 'your-tenant-id' , 'your-api-key' )
time.sleep( 300 ) # 5 minutes
Integration with Dashboard Features
The Query API works seamlessly with your existing Grain dashboard:
Matrices Integration
Use the Query API to build custom matrices that complement the Matrices page :
// Build a custom conversion matrix
async function buildConversionMatrix ( tenantId : string , apiKey : string ) {
const events = [ 'page_viewed' , 'form_started' , 'form_submitted' , 'purchase_completed' ];
const pages = [ '/home' , '/product' , '/checkout' , '/thank-you' ];
const matrix = {};
for ( const event of events ) {
matrix [ event ] = {};
for ( const page of pages ) {
const response = await fetch ( `https://queryapis.grainql.com/v1/api/query/count/ ${ tenantId } ` , {
method: 'POST' ,
headers: { 'X-API-Key' : apiKey },
body: JSON . stringify ({
event ,
filterSet: [{ property: 'properties.page' , comparison: 'EQUALS' , value: page }]
})
});
matrix [ event ][ page ] = ( await response . json ()). count ;
}
}
return matrix ;
}
Tracks Integration
Query data that powers your Tracks funnels :
// Get funnel data for a specific track
async function getFunnelData ( tenantId : string , apiKey : string , trackEvents : string []) {
const funnelData = [];
for ( const event of trackEvents ) {
const response = await fetch ( `https://queryapis.grainql.com/v1/api/query/count/ ${ tenantId } ` , {
method: 'POST' ,
headers: { 'X-API-Key' : apiKey },
body: JSON . stringify ({ event })
});
funnelData . push ({
event ,
count: ( await response . json ()). count
});
}
return funnelData ;
}
Plan Limits
Plan Requests per Minute Requests per Day Free Not available Not available Builder 2 20 Growth 10 200 Scale 100 2,000
Rate Limit Planning : These limits are designed for typical analytics use cases. For high-frequency applications, real-time dashboards, or bulk data exports, consider our custom plans with higher limits.
Best Practices
1. Use the Count Endpoint for Aggregations
// ✅ Good: Use count for totals
const totalUsers = await fetch ( '/v1/api/query/count/tenant' , {
method: 'POST' ,
body: JSON . stringify ({ event: 'user_signed_up' })
});
// ❌ Avoid: Fetching all events just to count them
const allEvents = await fetch ( '/v1/api/query/tenant' , {
method: 'POST' ,
body: JSON . stringify ({ event: 'user_signed_up' , pagination: { size: 10000 } }
});
2. Implement Pagination for Large Datasets
async function fetchAllEvents ( tenantId : string , apiKey : string ) {
const allEvents = [];
let offset = 0 ;
const pageSize = 1000 ;
while ( true ) {
const response = await fetch ( `/v1/api/query/ ${ tenantId } ` , {
method: 'POST' ,
headers: { 'X-API-Key' : apiKey },
body: JSON . stringify ({
pagination: { offset , size: pageSize }
})
});
const events = await response . json ();
allEvents . push ( ... events );
if ( events . length < pageSize ) break ;
offset += pageSize ;
}
return allEvents ;
}
3. Handle Rate Limits Gracefully
async function queryWithRetry ( url : string , options : RequestInit , maxRetries = 3 ) {
for ( let attempt = 1 ; attempt <= maxRetries ; attempt ++ ) {
try {
const response = await fetch ( url , options );
if ( response . status === 429 ) {
const retryAfter = response . headers . get ( 'Retry-After' );
const delay = retryAfter ? parseInt ( retryAfter ) * 1000 : Math . pow ( 2 , attempt ) * 1000 ;
console . log ( `Rate limited. Retrying in ${ delay } ms...` );
await new Promise ( resolve => setTimeout ( resolve , delay ));
continue ;
}
return response ;
} catch ( error ) {
if ( attempt === maxRetries ) throw error ;
await new Promise ( resolve => setTimeout ( resolve , Math . pow ( 2 , attempt ) * 1000 ));
}
}
}
Rate Limit Considerations : With the current rate limits (2-100 requests per minute), consider implementing request queuing or batching for high-frequency applications. For production applications with higher throughput needs, contact us about custom plans.
Security Best Practices
1. Never Expose API Keys in Client Code
❌ Don’t do this:
// NEVER put API keys in frontend code!
const apiKey = 'your-secret-key' ;
fetch ( '/v1/api/query/tenant' , { headers: { 'X-API-Key' : apiKey } });
✅ Do this instead:
// Backend API route
app . post ( '/api/query' , async ( req , res ) => {
const response = await fetch ( `https://queryapis.grainql.com/v1/api/query/ ${ req . body . tenantId } ` , {
headers: { 'X-API-Key' : process . env . GRAIN_API_KEY },
body: JSON . stringify ( req . body . query )
});
res . json ( await response . json ());
});
// Frontend
fetch ( '/api/query' , {
method: 'POST' ,
body: JSON . stringify ({ tenantId: 'your-tenant' , query: { event: 'page_viewed' } })
});
2. Use Environment Variables
# .env file (add to .gitignore!)
GRAIN_API_KEY = your-secret-key
GRAIN_TENANT_ID = your-tenant-id
3. Rotate Keys Regularly
Generate a new API key in the dashboard
Update your application with the new key
Deploy the changes
Revoke the old key after confirming the new one works
Next Steps
Now that you understand the basics, explore the detailed API reference:
API Reference Complete endpoint documentation
Filter Reference All filter operators and examples
Custom Dashboard Build a custom analytics dashboard
Data Export Export data for analysis
Custom Plans
Need higher rate limits or have specific usage requirements? We offer custom plans tailored to your needs:
Higher rate limits : Custom requests per minute/day limits
Dedicated support : Priority support and SLA guarantees
Custom features : Tailored analytics features and integrations
Volume discounts : Competitive pricing for high-volume usage
Contact us at support@grainql.com to discuss your requirements and get a custom quote.
Support