The Dory Data API
Every metric on your dashboard — pageviews, events, referrers, breakdowns, and raw rows — is available over a simple, read-only REST API. One API key per site, no SDK required.
Authentication
Every site has its own API key, scoped to that site's data only. Generate or copy it from Settings → API Access in your dashboard. Send it as a bearer token on every request — there is no separate siteId parameter, the key already identifies the site.
curl "https://your-dory-domain.com/api/v1/overview?range=30d" \ -H "Authorization: Bearer YOUR_API_KEY"
Base URL & versioning
All endpoints live under /api/v1/ on your own Dory deployment (e.g. https://analytics.yourdomain.com). The v1 prefix is a stable contract — breaking changes will ship as v2 rather than mutating this one. All responses are JSON, all endpoints are GET-only, and CORS is open so you can call the API directly from a browser dashboard.
Errors
Errors return a non-2xx status with a consistent shape:
{ "error": { "message": "Invalid API key.", "status": 401 } }Missing or invalid API key
Bad or missing query parameter
Unexpected server error
Rate limits
There is no hard rate limit today — this is a self-hosted deployment on your own database. Be a good citizen: cache responses you poll frequently, and prefer /timeseries or /overview over paging all of /export when a summary will do.
Endpoints
/api/v1/siteMetadata about the site the API key belongs to.
{
"data": { "id": "65f...", "name": "Marketing Site", "domain": "example.com", "createdAt": "2025-01-01T00:00:00.000Z" }
}/api/v1/overviewTotals for the period: views, events, unique visitors, and top 5 pages.
Query params
{
"range": "30d",
"data": {
"totalViews": 12045,
"totalEvents": 812,
"uniqueVisitors": 3021,
"topPages": [{ "route": "/", "views": 4210 }]
}
}/api/v1/timeseriesPageviews bucketed over time (hourly for 24h, daily for 7d/30d/3m, monthly for 12m/24m). Missing buckets are zero-filled.
Query params
{
"range": "7d",
"data": [{ "date": "2026-09-24", "views": 512 }, { "date": "2026-09-25", "views": 488 }]
}/api/v1/pagesTop 20 routes by views, with unique visitor counts.
Query params
{
"range": "30d",
"data": [{ "route": "/pricing", "views": 900, "unique": 640 }]
}/api/v1/referrersTop 10 external referrers by views.
Query params
{
"range": "30d",
"data": [{ "referrer": "https://instagram.com", "views": 300 }]
}/api/v1/eventsCustom events, grouped by name, with count, last-seen time, and the 5 most recent samples per event.
Query params
{
"range": "30d",
"data": [{
"name": "signup_clicked",
"count": 44,
"lastSeen": "2026-09-29T10:00:00.000Z",
"recentEvents": [{ "url": "https://example.com/pricing", "properties": { "plan": "Pro" }, "timestamp": "2026-09-29T10:00:00.000Z" }]
}]
}/api/v1/breakdownUnique visitors grouped by country, device, browser, OS, UTM campaign, and feature flag.
Query params
{
"range": "30d",
"data": {
"countries": [{ "name": "IN", "visitors": 900 }],
"devices": [{ "name": "Mobile", "visitors": 1200 }],
"browsers": [{ "name": "Chrome", "visitors": 2000 }],
"os": [{ "name": "Windows", "visitors": 1500 }],
"utms": [{ "name": "spring_launch", "visitors": 80 }],
"flags": [{ "name": "new-nav", "visitors": 300 }]
}
}/api/v1/realtimeActive visitors, live pages, and live countries for the last 5 minutes. No range param — always live.
{
"data": {
"activeVisitors": 12,
"livePages": [{ "route": "/", "unique": 8, "views": 20 }],
"liveCountries": [{ "name": "US", "visitors": 6 }]
}
}/api/v1/exportRaw document dump for pageviews or events — the underlying rows behind every other endpoint. Cursor-paginated, newest first.
Query params
{
"type": "pageviews",
"count": 100,
"nextCursor": "2026-09-28T04:12:00.000Z",
"data": [{ "_id": "...", "url": "https://example.com/", "route": "/", "country": "IN", "device": "Mobile", "timestamp": "2026-09-29T10:00:00.000Z" }]
}Paging through raw data
/api/v1/export is cursor-paginated: each response includes a nextCursor timestamp. Pass it back as before to fetch the next page, and stop when nextCursor is null.
let cursor;
do {
const url = new URL("https://your-dory-domain.com/api/v1/export");
url.searchParams.set("type", "pageviews");
if (cursor) url.searchParams.set("before", cursor);
const res = await fetch(url, { headers: { Authorization: "Bearer YOUR_API_KEY" } });
const { data, nextCursor } = await res.json();
// ...process data...
cursor = nextCursor;
} while (cursor);