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://api.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://api.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://api.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://api.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://api.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://api.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://api.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://api.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://api.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:
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 [email protected] to discuss your requirements and get a custom quote.
Support