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

NODE
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);
PYTHON
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"
A server key belongs on a server. Anything shipped to a browser is public the moment it loads, so keys never go in front-end code. Where a call genuinely has to happen in a page, it uses a separate public key that can do only that one thing.
Reads are scoped to one owner. Every call takes 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.

NODE
npm install @bolrach/analytics
PYTHON
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.

STATUSCODEMEANING
400bad_requestThe body or a query parameter was missing or malformed. The message names the field.
401unauthorizedNo key, or a key this API does not recognise.
403forbiddenA valid key without the scope this call needs, or a key not bound to the resource.
404not_foundNo such resource, or one that belongs to somebody else. The two are deliberately indistinguishable.
409conflictThe same idempotency key was reused with a different body.
429rate_limitedToo many calls. Retry after the seconds in the Retry-After header, the SDKs already do.
5xxserver_errorSomething failed on our side. Safe to retry; the SDKs retry twice with backoff.
Retry safely. Every write accepts an 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

GET /v1/properties listProperties() · list_properties()

Properties you own.

NODE
await client.listProperties();
PYTHON
client.list_properties()
POST /v1/properties createProperty() · create_property()

Create a property.

NODE
await client.createProperty({ /* body */ });
PYTHON
client.create_property({...})
GET /v1/properties/{property_id} getProperty() · get_property()

One property.

NODE
await client.getProperty(propertyId);
PYTHON
client.get_property(property_id)
GET /v1/properties/{property_id}/overview overview() · overview()

Totals plus top pages and devices.

Query parameters: days, event, path, device, geo.

NODE
await client.overview(propertyId);
PYTHON
client.overview(property_id)
GET /v1/properties/{property_id}/report report() · report()

Daily rows of events and visitors per event name.

Query parameters: days.

NODE
await client.report(propertyId);
PYTHON
client.report(property_id)
GET /v1/properties/{property_id}/realtime realtime() · realtime()

Who is on the site right now.

Query parameters: minutes.

NODE
await client.realtime(propertyId);
PYTHON
client.realtime(property_id)
GET /v1/properties/{property_id}/realtime/series realtimeSeries() · realtime_series()

Minute-by-minute events and visitors for the realtime view.

Query parameters: minutes (max 60).

NODE
await client.realtimeSeries(propertyId);
PYTHON
client.realtime_series(property_id)
GET /v1/properties/{property_id}/live live() · live()

The most recent individual events.

Query parameters: limit.

NODE
await client.live(propertyId);
PYTHON
client.live(property_id)
GET /v1/properties/{property_id}/breakdown breakdown() · breakdown()

Group events by a dimension.

Query parameters: dim (path|referrer|device|geo|name|utm_source|utm_medium|utm_campaign), days, limit.

NODE
await client.breakdown(propertyId);
PYTHON
client.breakdown(property_id)
GET /v1/properties/{property_id}/goals listGoals() · list_goals()

Goals.

NODE
await client.listGoals(propertyId);
PYTHON
client.list_goals(property_id)
POST /v1/properties/{property_id}/goals createGoal() · create_goal()

Create a goal.

NODE
await client.createGoal(propertyId, { /* body */ });
PYTHON
client.create_goal(property_id, {...})
GET /v1/properties/{property_id}/goals/{goal_id}/report goalReport() · goal_report()

Conversions for one goal.

Query parameters: days.

NODE
await client.goalReport(propertyId, goalId);
PYTHON
client.goal_report(property_id, goal_id)
POST /v1/properties/{property_id}/funnel funnel() · funnel()

Step-by-step conversion.

Body: steps (event names), days, mode (ordered|set).

NODE
await client.funnel(propertyId, { /* body */ });
PYTHON
client.funnel(property_id, {...})
GET /v1/properties/{property_id}/segments listSegments() · list_segments()

Audience segments.

NODE
await client.listSegments(propertyId);
PYTHON
client.list_segments(property_id)
POST /v1/properties/{property_id}/segments createSegment() · create_segment()

Create a segment.

NODE
await client.createSegment(propertyId, { /* body */ });
PYTHON
client.create_segment(property_id, {...})
POST /v1/properties/{property_id}/ask ask() · ask()

Ask a question in plain English. The generated query is guarded and scoped to this property.

NODE
await client.ask(propertyId, { /* body */ });
PYTHON
client.ask(property_id, {...})