Getting started
Create a website in the dashboard and paste the script in the <head> of every page:
<script
defer
data-website-id="dn_xxxxxxxxxxxxxxxx"
data-domain="example.com"
src="https://your-datanow-host/js/script.js"
></script>Pageviews, referrers, UTM campaigns, countries, devices and outbound link clicks are tracked automatically, including client-side navigation in single-page apps. Visitors are identified with the first-party cookie datanow_visitor_id (1 year) and sessions with datanow_session_id (30 minutes of inactivity).
Script configuration
| data-website-id | Required. Your website id (dn_…). |
| data-domain | Required. Your root domain; subdomains are tracked automatically and share the visitor cookie. |
| data-allowed-hostnames | Comma-separated extra root domains for cross-domain tracking. |
| data-api-url | Where events are sent. Defaults to the script origin + /api/events. Relative paths resolve against the page. |
| data-allow-localhost | "true" to track on localhost. |
| data-allow-file-protocol | "true" to track file:// pages. |
| data-debug | "true" logs every event to the console. |
| data-disable-console | "true" silences all console output. |
| data-cookieless | "true" tracks without cookies (see Cookieless & exclusions). |
| data-hash | "true" counts hash route changes (/#/pricing) as page views, for hash-based routers. |
To exclude your own visits, run localStorage.setItem("datanow_ignore", "true") in the browser console on your site.
Custom goals
Goals track conversions: signups, clicks, downloads, scroll depth. Names use lowercase letters, numbers, underscores, hyphens and colons (max 64 characters). Add up to 10 custom parameters.
// JavaScript (safe to call before the script loads if you add the queue stub)
window.datanow = window.datanow || function () { (window.datanow.q = window.datanow.q || []).push(arguments); };
window.datanow("signup", { plan: "pro" });<!-- Track clicks; data-dn-goal-* become parameters (kebab-case → snake_case) -->
<button data-dn-goal="initiate_checkout" data-dn-goal-plan="pro">Buy</button>
<!-- Track when a section scrolls into view -->
<section data-dn-scroll="viewed_pricing" data-dn-scroll-threshold="0.5" data-dn-scroll-delay="0">…</section>Record goals from your server (e.g. after a signup) with an API key:
curl -X POST https://your-datanow-host/api/v1/goals \
-H "Authorization: Bearer dn_key_..." -H "Content-Type: application/json" \
-d '{"datanow_visitor_id": "<cookie value>", "name": "signup", "metadata": {"plan": "pro"}}'Identify users
Attach an identity to the current visitor. Identified visitors show their name in live views and payments with a matching email are attributed to them automatically.
window.datanow("identify", { user_id: "u_123", email: "jane@example.com", name: "Jane" });Revenue attribution
Stripe. In website settings → Revenue, copy the webhook URL into Stripe (events checkout.session.completed, invoice.paid, charge.refunded) and paste the signing secret. Pass the visitor id in checkout metadata:
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: "price_123", quantity: 1 }],
metadata: { datanow_visitor_id: req.cookies.datanow_visitor_id },
subscription_data: { metadata: { datanow_visitor_id: req.cookies.datanow_visitor_id } },
success_url: "https://example.com/welcome",
});Any other provider. Send payments from your backend:
curl -X POST https://your-datanow-host/api/v1/payments \
-H "Authorization: Bearer dn_key_..." -H "Content-Type: application/json" \
-d '{"amount": 49, "currency": "USD", "transaction_id": "order_123", "datanow_visitor_id": "<cookie value>", "email": "jane@example.com"}'| amount | Required. Amount in major units (49.99). |
| transaction_id | Required. Unique per payment; re-sending updates it. |
| currency | 3-letter code (defaults to the website currency). |
| datanow_visitor_id | The visitor cookie. Without it, DataNow matches the email or customer_id of earlier payments. |
| email, customer_id | Optional, used for attribution fallback. |
| renewal / refunded | true for recurring charges / refunds. |
| timestamp | RFC 3339, defaults to now. |
Revenue of a row (referrer, page, country…) is the revenue of paying visitors seen in that row during the period. Conversion rate = paying visitors ÷ visitors. Delete a payment with DELETE /api/v1/payments/:transaction_id.
Payment providers
Connect a provider in website settings → Revenue: copy the webhook URL (https://your-datanow-host/api/webhooks/<provider>/<website id>) into the provider, select the events listed there, and paste the signing secret. Every webhook is signature-checked. Then pass the visitor id at checkout:
| Stripe | metadata.datanow_visitor_id on the Checkout Session (and subscription_data.metadata for subscriptions). Payments, renewals, refunds and MRR. |
| LemonSqueezy | checkout[custom][datanow_visitor_id]=… on checkout links, or checkout_data.custom via the API. Orders, renewals, refunds and MRR (from invoices). |
| Paddle | customData: { datanow_visitor_id } in Paddle.Checkout.open, or custom_data via the API. Transactions, refunds (approved adjustments) and MRR. |
| Polar | metadata: { datanow_visitor_id } when creating the checkout. Orders, refunds and MRR. |
In the browser, window.datanow.visitorId() returns the id; on your server, read the datanow_visitor_id cookie. Without it, payments are matched to identified visitors by email, then to earlier payments of the same customer.
MRR & subscriptions
Subscription changes from Stripe, LemonSqueezy, Paddle, Polar or the API are turned into MRR movements. Prices are normalized to a monthly amount (yearly ÷ 12). Active and past-due subscriptions count; trials, paused and ended subscriptions don't. Subscriptions canceled “at period end” count until they actually end.
| MRR | Monthly recurring revenue at the end of the period |
| New / reactivation | MRR from new subscriptions, and from subscriptions that had churned and came back |
| Expansion / contraction | Upgrades and downgrades of existing subscriptions |
| Churned MRR | MRR lost to cancellations that ended during the period |
| Churn rate | Subscriptions that churned ÷ subscriptions active at the start of the period |
| ARPA | MRR ÷ active subscriptions |
| LTV | Average all-time net revenue per paying customer |
LemonSqueezy doesn't include prices in subscription events, so MRR comes from the latest subscription invoice, and yearly plans are recognized from the renewal date.
# Custom billing: send the current state whenever it changes
curl -X POST https://your-datanow-host/api/v1/subscriptions \
-H "Authorization: Bearer dn_key_..." -H "Content-Type: application/json" \
-d '{"subscription_id": "sub_123", "status": "active", "amount": 490, "interval": "year", "currency": "USD", "datanow_visitor_id": "<cookie value>"}'#1 KPI
When revenue isn't the best signal yet, pick a goal (e.g. signup, trial_started) as your #1 KPI in settings → General, or with the star next to a goal in the dashboard. The dashboard then shows its conversions and conversion rate, charts it, and every source, page, country and device card shows how many of its visitors converted.
Filters & segments
Click any row to filter, or use Filter to pick a dimension and an operator: is, is not, contains, does not contain. Visit count filters by the visitor's session number (1 = first visit, at least 2 = returning visitors). Save filter combinations as segments to reapply them in one click. In the API, prefix the value with the operator:
filter_referrer=is_not:google.com&filter_page=contains:/blog&filter_visit_count=gte:2&filter_goal=signupCookieless mode & exclusions
With cookieless mode (settings → Tracking code, or data-cookieless="true"), the script stores nothing on the device. The server identifies visitors with a hash of a daily secret, the IP address and the user agent; the secret is deleted after a day and IPs are never stored. Trade-offs: a returning visitor counts as new each day, cross-domain linking is off, and revenue can only be attributed through identified emails.
Settings → Exclusions drops events before they're stored, by IP address or CIDR range, page (with * wildcards), country, or hostname. To ignore just your own browser, run localStorage.setItem("datanow_ignore", "true") on your site.
Cross-domain tracking
Tracking example.com and app.io as one journey? Use the same website id on both, add data-allowed-hostnames="app.io" to the script on example.com (and vice versa), and list the domains in settings → General. Links between them carry the visitor id.
Proxy through your domain
Serve the script and events from your own domain to avoid ad blockers. Proxy /js/script.js and /api/events to DataNow, then point src and data-api-url at your paths. Example for Next.js:
// next.config.js
module.exports = {
async rewrites() {
return [
{ source: "/stats/script.js", destination: "https://your-datanow-host/js/script.js" },
{ source: "/stats/events", destination: "https://your-datanow-host/api/events" },
];
},
};
// <script defer data-website-id="dn_..." data-domain="example.com"
// data-api-url="/stats/events" src="/stats/script.js"></script>REST API
Create an API key in website settings → API & MCP and send it as Authorization: Bearer dn_key_…. Responses are JSON. All analytics endpoints accept:
| period | today, yesterday, 24h, 7d, 30d (default), 12m, wtd, mtd, ytd, all, custom |
| from, to | YYYY-MM-DD in the website timezone (inclusive) with period=custom, or from=90d |
| shift | Move the window by whole periods, e.g. -1 for the previous 30 days |
| interval | hour, day, week, month (timeseries) |
| filter_<dimension> | value or op:value with op is_not, contains, not_contains (gte/lte for visit_count), e.g. filter_country=US&filter_referrer=is_not:google.com |
| GET /api/v1/analytics/overview | Visitors, pageviews, sessions, revenue, customers, conversion rate, revenue/visitor, bounce rate, session time, #1 KPI, online, and subscription metrics (MRR, churn, ARPA, LTV) — with the previous period |
| GET /api/v1/analytics/timeseries | Per-bucket metrics incl. new visitors and revenue (new / renewal / refund) |
| GET /api/v1/analytics/breakdown/:dimension | channel, referrer, utm_source, utm_medium, utm_campaign, hostname, page, entry_page, exit_link, country, region, city, browser, os, device, goal (sort=visitors|revenue|kpi|mrr, limit) |
| GET /api/v1/analytics/goals/timeseries | Completions of the top goals per bucket |
| GET /api/v1/analytics/realtime | Visitors online now and the latest events |
| GET /api/v1/analytics/funnels | Saved funnels |
| GET /api/v1/analytics/funnels/:id/results | Visitors per funnel step |
| GET /api/v1/visitors/:visitorId | Visitor profile, journey and payments |
| GET /api/v1/payments | Recent payments |
| POST /api/v1/payments | Record a payment |
| DELETE /api/v1/payments/:transactionId | Delete a payment |
| POST /api/v1/goals | Record a server-side goal |
| POST /api/v1/subscriptions | Record a subscription's current state (for MRR) |
curl "https://your-datanow-host/api/v1/analytics/breakdown/referrer?period=30d&sort=revenue&limit=5" \
-H "Authorization: Bearer dn_key_..."MCP for AI agents
DataNow exposes a Model Context Protocol server at https://your-datanow-host/api/mcp (Streamable HTTP). Authenticate with a website API key. Tools: get_website, get_overview, get_timeseries, get_breakdown, get_realtime, list_funnels, get_funnel, get_visitor, list_payments. All tools are read-only.
# Claude Code
claude mcp add --transport http datanow https://your-datanow-host/api/mcp --header "Authorization: Bearer dn_key_..."// Cursor, Claude Desktop (via mcp-remote), and other clients
{
"mcpServers": {
"datanow": {
"url": "https://your-datanow-host/api/mcp",
"headers": { "Authorization": "Bearer dn_key_..." }
}
}
}Self-hosting
DataNow is a Go API (backend/) and a Next.js app (frontend/) on PostgreSQL. The Next.js server proxies /api/* to the API, so you only expose one origin.
cp .env.example .env # set JWT_SECRET (openssl rand -hex 32)
docker compose up -d
docker compose exec backend ./seed # optional: public demo website