Bolrach Analytics API
First-party, cookie-free web analytics with an AI query path: ask your own events a question in plain English.
Quick start
Base URL: https://api.bolrach.io/v1
import { BolrachAnalytics } from '@bolrach/analytics';
const analytics = new BolrachAnalytics({
apiKey: process.env.BOLRACH_ANALYTICS_KEY,
ownerUserId: process.env.BOLRACH_ANALYTICS_OWNER,
});
const overview = await analytics.overview(propertyId, { days: 30 });
console.log(overview.visitors, 'visitors,', overview.sessions, 'sessions');
const answer = await analytics.ask(propertyId, {
question: 'Which referrer sent the most signups last week?',
});
console.log(answer.answer);
import os
from bolrach_analytics import BolrachAnalytics
analytics = BolrachAnalytics(
api_key=os.environ["BOLRACH_ANALYTICS_KEY"],
owner_user_id=os.environ["BOLRACH_ANALYTICS_OWNER"],
)
overview = analytics.overview(property_id, days=30)
print(overview["visitors"], "visitors,", overview["sessions"], "sessions")
answer = analytics.ask(property_id, {"question": "Which referrer sent the most signups last week?"})
print(answer["answer"])
Authentication
A server key from analytics.bolrach.io/console. Reads are scoped to one owner, passed as owner_user_id. Send it as a bearer token:
curl https://api.bolrach.io/v1/... \
-H "authorization: Bearer $YOUR_KEY"
ownerUserId; the SDKs add it for you when you set it on the client. This is deliberate: an analytics API that could fall back to "all properties" would be one bug away from showing somebody else's traffic.SDKs
Official clients for Node and Python. Both retry on 429 and 5xx with exponential backoff,
obey Retry-After, accept idempotency keys on writes, and raise a typed
BolrachAnalyticsError carrying status, code, message
and requestId, so you branch on the failure instead of parsing a string.
npm install @bolrach/analytics
pip install bolrach-analytics
Every endpoint below lists its SDK method name. Anything not yet wrapped is still reachable without waiting for a release:
await client.request('GET', '/some/new/endpoint', { query: { days: 7 } }); // node
client.request('GET', '/some/new/endpoint', query={'days': 7}) # python
Errors and retries
Errors are JSON: {"error": {"code": "...", "message": "..."}}. The HTTP status
carries the category, the code carries the specific reason.
| STATUS | CODE | MEANING |
|---|---|---|
400 | bad_request | The body or a query parameter was missing or malformed. The message names the field. |
401 | unauthorized | No key, or a key this API does not recognise. |
403 | forbidden | A valid key without the scope this call needs, or a key not bound to the resource. |
404 | not_found | No such resource, or one that belongs to somebody else. The two are deliberately indistinguishable. |
409 | conflict | The same idempotency key was reused with a different body. |
429 | rate_limited | Too many calls. Retry after the seconds in the Retry-After header, the SDKs already do. |
5xx | server_error | Something failed on our side. Safe to retry; the SDKs retry twice with backoff. |
Idempotency-Key header. Reusing a key returns the original result rather than
applying the change twice, so a timeout you never saw the answer to is safe to repeat.Endpoints
Properties you own.
await client.listProperties();
client.list_properties()
Create a property.
await client.createProperty({ /* body */ });
client.create_property({...})
One property.
await client.getProperty(propertyId);
client.get_property(property_id)
Totals plus top pages and devices.
Query parameters: days, event, path, device, geo.
await client.overview(propertyId);
client.overview(property_id)
Daily rows of events and visitors per event name.
Query parameters: days.
await client.report(propertyId);
client.report(property_id)
Who is on the site right now.
Query parameters: minutes.
await client.realtime(propertyId);
client.realtime(property_id)
Minute-by-minute events and visitors for the realtime view.
Query parameters: minutes (max 60).
await client.realtimeSeries(propertyId);
client.realtime_series(property_id)
The most recent individual events.
Query parameters: limit.
await client.live(propertyId);
client.live(property_id)
Group events by a dimension.
Query parameters: dim (path|referrer|device|geo|name|utm_source|utm_medium|utm_campaign), days, limit.
await client.breakdown(propertyId);
client.breakdown(property_id)
Goals.
await client.listGoals(propertyId);
client.list_goals(property_id)
Create a goal.
await client.createGoal(propertyId, { /* body */ });
client.create_goal(property_id, {...})
Conversions for one goal.
Query parameters: days.
await client.goalReport(propertyId, goalId);
client.goal_report(property_id, goal_id)
Step-by-step conversion.
Body: steps (event names), days, mode (ordered|set).
await client.funnel(propertyId, { /* body */ });
client.funnel(property_id, {...})
Audience segments.
await client.listSegments(propertyId);
client.list_segments(property_id)
Create a segment.
await client.createSegment(propertyId, { /* body */ });
client.create_segment(property_id, {...})
Ask a question in plain English. The generated query is guarded and scoped to this property.
await client.ask(propertyId, { /* body */ });
client.ask(property_id, {...})