# Pieowl – Full documentation for LLMs Pieowl is simple, private product analytics you can just ask: visitors, funnels, revenue and session replay, plus answers from Claude or Cursor over MCP. No cookies, cloud in the EU. This file contains the full text of all documentation pages. Each section is separated by --- and includes a canonical URL. --- ## What is Pieowl? URL: https://pieowl.com/docs Pieowl is web and product analytics in one place. You get the simple traffic view you expect from a privacy-friendly tool, plus funnels, retention, cohorts, session replay and revenue. When you want an answer, ask Claude, Cursor or another AI assistant: it reads your data through the Pieowl MCP server. Key features [#key-features] * **Web analytics**: Visitors, sessions, page views, bounce rate, visit duration, sources, pages, locations and devices * **Product analytics**: [Funnels](/features/funnels), retention, cohorts, user profiles and session history * **Session replay**: [Watch real sessions](/features/session-replay), with text and form fields always hidden * **Revenue attribution**: [See which source, page and country makes you money](/features/revenue-attribution). Every purchase is credited to the visit where it happened * **MCP server**: [Ask your AI assistant about your users](/features/mcp) from Claude, Cursor or any MCP client * **Dashboards and reports**: Build your own charts and [dashboards](/features/data-visualization), and share them by link * **Realtime**: See who is on your site right now * **Notifications**: Get alerts for events and funnels in Slack, Discord or the app * **Integrations**: [Google Search Console](/features/integrations), plus data export to S3 or Google Cloud Storage * **SDKs and API**: Script tag, JavaScript, Next.js, React, Vue, Astro, Nuxt, Express and React Native, plus an HTTP API for any language * **Privacy**: No cookies and no stored IP addresses * **Hosting**: Our cloud in the EU Quick start [#quick-start] To start tracking, create an account. 1. **[Install Pieowl](/docs/get-started/install-pieowl)**: Add the script tag or use one of our SDKs 2. **[Track events](/docs/get-started/track-events)**: Measure what users do 3. **[Identify users](/docs/get-started/identify-users)**: Connect events to the people behind them 4. **[Track revenue](/docs/revenue-attribution)**: See which sources, pages and countries bring in money Built for privacy [#built-for-privacy] * **No cookies**: The tracker never sets a cookie and stores nothing on your visitors' devices, so in many cases you don't need a consent banner for analytics. [When you still do](/docs/consent-and-cookie-banners) * **No stored IP addresses**: The IP is used for the location lookup and a daily-rotating visitor hash, then dropped * **EU hosting**: Your data is stored in the EU Read [how Pieowl tracks visitors](/docs/how-it-works) for the details. Need help? [#need-help] Email us at [hello@pieowl.com](mailto:hello@pieowl.com). --- ## Avoid adblockers with proxy URL: https://pieowl.com/docs/adblockers This page covers why ad blockers block analytics, how they do it, and how to avoid it. Ad blockers started out blocking ads, but most now block tracking scripts too. That's mostly for privacy, and we respect it. Still, you have good reasons to understand your visitors. Pieowl sets no cookies and doesn't track people across sites, but generic blocklists often block every analytics tool the same way. The best fix is to send events through your own domain. Ad blockers usually can't block requests to your own domain (first-party requests) without breaking the site itself. Built-in support [#built-in-support] `@pieowl/bots` has a ready-made proxy. It serves the tracking script at `/pieowl.js`, passes on what the script should collect (`/config/tracking`, from Settings → Site) and forwards events (`/track`) to Pieowl with the visitor's IP and user agent, so locations and devices stay correct. ```bash npm install @pieowl/bots ``` **Next.js** (or any framework with Fetch `Request` handlers): ```typescript title="app/api/pieowl/[...pieowl]/route.ts" import { createRouteHandler } from '@pieowl/bots/route-handler'; export const { GET, POST } = createRouteHandler(); ``` The same handler works in Hono (`app.all('/api/pieowl/*', (c) => handler(c.req.raw))`), Astro endpoints, SvelteKit `+server.ts` files, Nuxt server routes (`export default fromWebHandler(createRouteHandler())`) and Cloudflare Workers. **Express**: ```typescript import { createRouteHandler } from '@pieowl/bots/express'; app.use('/api/pieowl', createRouteHandler()); ``` Then point the script at your route: set `apiUrl: '/api/pieowl'` and load `/api/pieowl/pieowl.js` (see [Frontend configuration](#frontend-configuration)). The proxy serves only `pieowl.js`. If you use [session replay](/docs/session-replay) with the proxy, set `sessionReplay.scriptUrl` to `https://pieowl.com/pieowl-replay.js`. Proxying in any framework [#proxying-in-any-framework] If your backend doesn't run JavaScript, build the same proxy yourself. Set up an endpoint on your domain (for example `api.domain.com` or `domain.com/api`) that forwards requests to Pieowl. Below is the logic written out with a [Hono](https://hono.dev/) server. It follows the same steps as `@pieowl/bots`. Hono example [#hono-example] ```typescript import { Hono } from 'hono' const app = new Hono() // 1. Serve the tracking script (and the session replay script) from your domain for (const file of ['pieowl.js', 'pieowl-replay.js']) { app.get(`/${file}`, async (c) => { try { const res = await fetch(`https://pieowl.com/${file}`) const text = await res.text() c.header('Content-Type', 'text/javascript') // Optional caching for 24 hours c.header('Cache-Control', 'public, max-age=86400, stale-while-revalidate=86400') return c.body(text) } catch (e) { return c.json({ error: 'Failed to fetch script' }, 500) } }) } // 2. Forward tracked events app.post('/track', async (c) => { const body = await c.req.json() // Forward the visitor's IP address (pick the right header for your hosting) const ip = c.req.header('cf-connecting-ip') ?? c.req.header('x-forwarded-for')?.split(',')[0] const headers = new Headers() headers.set('Content-Type', 'application/json') headers.set('Origin', c.req.header('origin') ?? '') headers.set('User-Agent', c.req.header('user-agent') ?? '') headers.set('pieowl-client-id', c.req.header('pieowl-client-id') ?? '') if (ip) { headers.set('pieowl-client-ip', ip) } try { const res = await fetch('https://api.pieowl.com/track', { method: 'POST', headers, body: JSON.stringify(body), }) return c.json(await res.text(), res.status) } catch (e) { return c.json(e, 500) } }) // 3. Pass on what the script should collect (Settings → Site) app.get('/config/tracking', async (c) => { const clientId = c.req.query('clientId') ?? '' const res = await fetch( `https://api.pieowl.com/config/tracking?clientId=${encodeURIComponent(clientId)}` ) return c.json(await res.json(), res.status) }) export default app ``` This sets up these endpoints: 1. `GET /pieowl.js` and `GET /pieowl-replay.js`: Fetch the Pieowl scripts and serve them from your domain. You only need the replay script if you use [session replay](/docs/session-replay). 2. `POST /track`: Receives events from the browser, adds the headers Pieowl needs (`User-Agent`, `Origin`, `Content-Type`, `pieowl-client-id` and `pieowl-client-ip`), and forwards them to the Pieowl API. 3. `GET /config/tracking`: Returns what the script collects by itself, as set in Settings → Site. Without it the script falls back to its defaults. Frontend configuration [#frontend-configuration] Once your proxy is running, point the Pieowl script at it instead of the default URLs. ```html ``` Now every request goes to your domain first, so ad blockers that look for third-party tracking domains let it through. --- ## Track AI crawlers and bots URL: https://pieowl.com/docs/ai-bots The **AI & bots** card on the overview shows which crawlers and AI assistants requested your pages, per company, over time. Bot requests are kept apart from your visitors: they never show up as sessions, page views or visitors. The three categories [#the-three-categories] Every recognised bot belongs to one category: * **AI answers**: fetched live because someone asked an AI assistant something, for example ChatGPT, Claude or Perplexity looking up your site to answer a question. * **Indexing**: search engines and AI search building their index, for example Googlebot, Bingbot, OAI-SearchBot or PerplexityBot. * **Training**: collected to train AI models, for example GPTBot, ClaudeBot, CCBot (Common Crawl) or Bytespider. Why you need a server-side snippet [#why-you-need-a-server-side-snippet] Bots don't run JavaScript, so the Pieowl tracking script never sees them. Your server sees every request, so it reports the bot requests to Pieowl. The snippets below check the user agent first and only send requests that look like a bot, so normal visitors cause no extra network calls. All options need your client secret. Keep it on the server and never put it in browser code. See [Authentication](/docs/api/authentication). @pieowl/bots [#pieowlbots] One package for every server that runs JavaScript: Node.js, edge runtimes and Cloudflare Workers. It uses only `fetch` and depends on no framework. ```bash npm install @pieowl/bots ``` It reports only requests whose user agent looks like a bot, never blocks or changes your response, and never throws. Reports are sent in the background, in batches of up to 50 requests or every second. Each report has the path, host and method, the response status where the framework gives it, and the client IP for [verification](#verification): the first address in `X-Forwarded-For`, else `X-Real-IP`, else `CF-Connecting-IP`. Pass `apiUrl` to send to a different API URL (default `https://api.pieowl.com`). Next.js [#nextjs] ```typescript title="proxy.ts" import { trackBots } from '@pieowl/bots/next'; export const proxy = trackBots({ clientId: process.env.PIEOWL_CLIENT_ID!, clientSecret: process.env.PIEOWL_CLIENT_SECRET!, }); ``` On Next.js 15 or older, name the file `middleware.ts` and use `export const middleware = trackBots({ ... })`. The request continues as if there were no proxy, and the report is kept alive with `event.waitUntil`. Already have a proxy? Call `trackBotRequest` in it and keep your own response: ```typescript title="proxy.ts" import { trackBotRequest } from '@pieowl/bots'; import { type NextFetchEvent, type NextRequest, NextResponse } from 'next/server'; export function proxy(request: NextRequest, event: NextFetchEvent) { trackBotRequest(request, { clientId: process.env.PIEOWL_CLIENT_ID!, clientSecret: process.env.PIEOWL_CLIENT_SECRET!, }, event); return NextResponse.next(); } ``` If your proxy or middleware has a `matcher`, make sure it covers the pages bots request. Requests the proxy doesn't run for are not reported. Express [#express] ```typescript import { trackBots } from '@pieowl/bots/express'; app.use(trackBots({ clientId: process.env.PIEOWL_CLIENT_ID, clientSecret: process.env.PIEOWL_CLIENT_SECRET, })); ``` The request is reported after the response is sent, with its status. The IP is `req.ip`: behind a proxy or load balancer, set Express's [`trust proxy`](https://expressjs.com/en/guide/behind-proxies.html) so `req.ip` is the client and not the proxy. It also works with Connect and other `(req, res, next)` servers. Hono [#hono] ```typescript import { trackBots } from '@pieowl/bots/hono'; app.use(trackBots({ clientId: process.env.PIEOWL_CLIENT_ID!, clientSecret: process.env.PIEOWL_CLIENT_SECRET!, })); ``` On Cloudflare Workers, read the keys with `import { env } from 'cloudflare:workers'`. The report is kept alive with `executionCtx.waitUntil`. Cloudflare Workers and Pages [#cloudflare-workers-and-pages] Wrap the Worker's fetch handler: ```typescript title="src/index.ts" import { env } from 'cloudflare:workers'; import { withBotTracking } from '@pieowl/bots'; export default { fetch: withBotTracking((request) => fetch(request), { clientId: env.PIEOWL_CLIENT_ID, clientSecret: env.PIEOWL_CLIENT_SECRET, }), }; ``` `withBotTracking` wraps any `(request, ...) => Response` handler (Deno, Bun, Vercel Edge Functions) and uses the first argument that has `waitUntil`. For Cloudflare Pages, add a middleware: ```typescript title="functions/_middleware.ts" import { trackBotRequest } from '@pieowl/bots'; export const onRequest: PagesFunction = (context) => { trackBotRequest(context.request, { clientId: context.env.PIEOWL_CLIENT_ID, clientSecret: context.env.PIEOWL_CLIENT_SECRET, }, context); return context.next(); }; ``` Astro, Nuxt, SvelteKit and others [#astro-nuxt-sveltekit-and-others] Call `trackBotRequest(request, options, context?)` in your framework's server middleware. It takes a Fetch `Request`, or `{ url, headers, method?, ip?, status? }`. On serverless and edge platforms, pass anything with `waitUntil` as `context`. ```typescript title="Astro: src/middleware.ts" import { trackBotRequest } from '@pieowl/bots'; import { defineMiddleware } from 'astro:middleware'; export const onRequest = defineMiddleware((context, next) => { trackBotRequest(context.request, { clientId: import.meta.env.PIEOWL_CLIENT_ID, clientSecret: import.meta.env.PIEOWL_CLIENT_SECRET, }); return next(); }); ``` ```typescript title="Nuxt: server/middleware/pieowl-bots.ts" import { trackBotRequest } from '@pieowl/bots'; export default defineEventHandler((event) => { trackBotRequest({ url: getRequestURL(event).href, headers: getRequestHeaders(event) }, { clientId: process.env.PIEOWL_CLIENT_ID!, clientSecret: process.env.PIEOWL_CLIENT_SECRET!, }); }); ``` ```typescript title="SvelteKit: src/hooks.server.ts" import { trackBotRequest } from '@pieowl/bots'; import { env } from '$env/dynamic/private'; export const handle = async ({ event, resolve }) => { trackBotRequest(event.request, { clientId: env.PIEOWL_CLIENT_ID, clientSecret: env.PIEOWL_CLIENT_SECRET, }); return resolve(event); }; ``` In Remix and React Router, call it at the top of your root `loader` with `request`. HTTP API [#http-api] From any other server, send bot requests to the API yourself: ```bash curl -X POST 'https://api.pieowl.com/bot' \ -H 'Content-Type: application/json' \ -H 'pieowl-client-id: YOUR_CLIENT_ID' \ -H 'pieowl-client-secret: YOUR_CLIENT_SECRET' \ -d '{ "requests": [ { "userAgent": "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.1; +https://openai.com/gptbot)", "path": "/pricing", "host": "example.com", "method": "GET", "status": 200, "timestamp": "2026-01-15T10:30:00.000Z", "ip": "20.171.206.17" } ] }' ``` The body has a `requests` array with 1 to 100 items. Each item: | Field | Type | Required | Description | | ----------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `userAgent` | string | Yes | The `User-Agent` header of the request, 1 to 1024 characters. | | `path` | string | Yes | Requested path, 1 to 2048 characters. | | `host` | string | No | Requested host, up to 255 characters. | | `method` | string | No | HTTP method, up to 16 characters. | | `status` | integer | No | Response status, 100 to 599. | | `timestamp` | string | No | ISO 8601 date-time with an offset, for example `2026-01-15T10:30:00.000Z`. Defaults to the time Pieowl receives it. Times more than 1 hour in the past are moved to 1 hour ago, and future times to now. | | `ip` | string | No | IPv4 or IPv6 address the request came from, without a port. Used to [verify](#verification) the bot, not stored. | A valid request returns `202` with the number of stored and ignored items: ```json { "accepted": 1, "ignored": 0 } ``` Requests without a valid client secret get `401`. The endpoint allows 600 calls per 10 seconds per client ID, so batch bot requests (up to 100 per call) instead of sending one call each. Verification [#verification] Anyone can put `GPTBot` in a user agent. When you send the client IP, Pieowl checks it against what the bot's company publishes: its official IP list, or a reverse DNS lookup confirmed by a forward lookup. Each request gets one of three results: * **Verified**: the IP belongs to the company. Shown with a check mark in the card. * **Fake**: the company publishes a way to check its bots, and the IP fails it. Fake requests are left out of the card's charts and totals. A line under the tabs says how many were hidden and which bots they claimed to be. * **Can't be verified**: the company publishes no IP list or DNS name for its bot, the request was sent without an IP, or the check could not run (for example a DNS timeout). These requests are counted as normal. Meta, ByteDance (Bytespider), DeepSeek and xAI publish no addresses for their bots, so their requests are always shown as "can't be verified". Companies Pieowl verifies include OpenAI, Anthropic, Google, Microsoft (Bing), Apple, Amazon, Perplexity, Mistral, DuckDuckGo, Common Crawl and Yandex. The IP is only used for this check. Pieowl stores the result, not the IP. The IP must be the bot's, not your proxy's. `@pieowl/bots` takes it from the usual proxy headers (Express: `req.ip`); if you call the HTTP API yourself, forward the client IP your load balancer or CDN reports. Notes [#notes] * **Bot requests don't count toward your event limit.** * **Requests are classified by user agent.** Pieowl matches the user agent against a list of known AI and search bots and assigns each one a company and a category. The IP then [confirms](#verification) the claim where the company makes that possible. * **Unknown bots are ignored.** Requests from user agents that aren't on the list are not stored. In the HTTP API response they count as `ignored`. --- ## Authentication URL: https://pieowl.com/docs/api-reference/authentication Client ID and secret [#client-id-and-secret] Pieowl uses client credentials. Every API client has a **client ID** and a **client secret**, which you create in the dashboard. Send them as request headers: ```http pieowl-client-id: pieowl-client-secret: ``` The base URL is `https://api.pieowl.com`. API client types [#api-client-types] | Type | What it can do | | ------- | ------------------------------------------------------- | | `write` | Send events and profile updates | | `read` | Read analytics data (Insights, Export, Import) | | `root` | Full access, including the Manage API. Server use only. | Create a client [#create-a-client] In the dashboard, go to **Settings → Developers → Keys**. Create a client with the type you need. Copy the secret right away, because it is only shown once. For permissions, error responses and security tips, see the [Authentication guide](/docs/api/authentication). --- ## Rate Limits URL: https://pieowl.com/docs/api-reference/rate-limits Limits [#limits] | Endpoint group | Limit | | -------------- | -------------------- | | Insights | 100 req / 10 seconds | | Export | 100 req / 10 seconds | | Manage | 20 req / 10 seconds | Limits apply per **client ID**. A request without a client ID is counted against its IP address. Track, Profile and Import endpoints have no rate limit. Handling 429s [#handling-429s] When you go over the limit, the API returns `429 Too Many Requests`: ```json { "status": 429, "error": "Too Many Requests", "message": "You have exceeded the rate limit for this endpoint." } ``` Wait for the 10-second window to reset, then retry. Exponential backoff works well. --- ## Authentication URL: https://pieowl.com/docs/api/authentication Authentication [#authentication] Every request to the Pieowl API is tied to a client. A client has a client ID and a client secret. Each API needs a certain client type: * **Track API**: any client. The default `write` client works. * **Export API**: `read` or `root` * **Insights API**: `read` or `root` * **Import API**: `read` or `root` * **Manage API**: `root` only When you add a site, Pieowl creates a `write` client for it. That client can send events, but it can't use the Export, Insights, Import or Manage APIs. Create another client with the right type for those. Headers [#headers] Send these headers with every request: * `pieowl-client-id`: your client ID * `pieowl-client-secret`: your client secret Example [#example] ```bash curl 'https://api.pieowl.com/insights/YOUR_PROJECT_ID/metrics?range=30d' \ -H 'pieowl-client-id: YOUR_CLIENT_ID' \ -H 'pieowl-client-secret: YOUR_CLIENT_SECRET' ``` Replace `YOUR_PROJECT_ID` with your site ID (`projectId`), and `YOUR_CLIENT_ID` and `YOUR_CLIENT_SECRET` with your own values. Create a client [#create-a-client] In the dashboard, go to **Settings → Developers → Keys**. Create a client and pick its type. Copy the secret right away, because it is only shown once. Client types [#client-types] | Type | What it can do | | ------- | ----------------------------------------------------------- | | `write` | Send events and profile updates (Track API) | | `read` | Read data for its own site (Export, Insights, Import) | | `root` | Read data for any site in your team, and use the Manage API | A `root` client has access to every site in your team. Use it only on a server, and keep its secret safe. Keep your credentials safe [#keep-your-credentials-safe] 1. **Keep secrets on the server.** Never put a client secret in browser or app code. 2. **Use HTTPS** for every request. 3. **Rotate secrets** from time to time. Create a new client, switch over, then delete the old one. 4. **Use the smallest access you need.** Pick `read` over `root` when you only read data. Error responses [#error-responses] If authentication fails on the Export, Insights, Import or Manage APIs, you get `401 Unauthorized`. The message tells you what went wrong: ```json { "error": "Unauthorized", "message": "Export: Invalid client secret" } ``` Common causes: * The client ID or secret is wrong. * The client ID is not a valid UUID. * The client type is too low, for example a `write` client calling the Export API, or a `read` client calling the Manage API. * The client has no secret. The Track API also returns `401`, with a plain-text message such as `Ingestion: Invalid cors or secret`. Rate limits [#rate-limits] Some APIs limit how many requests one client can make: * **Export and Insights APIs**: 100 requests per 10 seconds * **Manage API**: 20 requests per 10 seconds * **Track, Profile and Import APIs**: no rate limit If you go over the limit, you get `429 Too Many Requests`. Wait a little, then retry with exponential backoff. See [Rate limits](/docs/api-reference/rate-limits) for details. --- ## Export URL: https://pieowl.com/docs/api/export Authentication [#authentication] Requires a `read` or `root` client. The default `write` client does not have access. See the [Authentication](/docs/api/authentication) guide. A `read` client can only export its own site. A `root` client can export any site in your team by passing its site ID (`projectId`). Endpoints [#endpoints] | Endpoint | What it returns | | -------------------- | ------------------------------------------------- | | `GET /export/events` | A paged list of raw events, with optional filters | | `GET /export/charts` | Time-series chart data, with optional breakdowns | Export events [#export-events] `/export/events` accepts these query parameters: | Parameter | Description | | -------------- | --------------------------------------------------------------------------------------- | | `projectId` | The site ID of the site to export. Optional for a `read` client. | | `event` | Only return events with this name. Repeat it for more names. | | `start`, `end` | Date range, for example `2026-09-01` | | `profileId` | Only return events for one profile | | `page` | Page number, starting at `1` | | `limit` | Events per page. Default `50`, max `1000`. | | `filters` | JSON array of property filters | | `includes` | Extra fields to return, comma separated, for example `profile,meta,properties,referrer` | ```bash curl 'https://api.pieowl.com/export/events?projectId=YOUR_PROJECT_ID&event=signup&start=2026-09-01&end=2026-09-30&limit=100&includes=properties' \ -H 'pieowl-client-id: YOUR_CLIENT_ID' \ -H 'pieowl-client-secret: YOUR_CLIENT_SECRET' ``` The response has a `meta` object (`count`, `totalCount`, `pages`, `current`) and a `data` array with the events. Export chart data [#export-chart-data] `/export/charts` takes a `series` array. Each item names an event and can add filters, a segment (`event`, `user`, `session`, `property_sum` and more) and a property. You can also pass `range` or `startDate` and `endDate`, an `interval` and `breakdowns`. For every query parameter and the full response schema, see the [API Reference](/docs/api-reference). Other ways to get your data out [#other-ways-to-get-your-data-out] You can also send raw events to your own S3 or Google Cloud Storage bucket. Set this up under **Integrations** in the dashboard. --- ## Insights URL: https://pieowl.com/docs/api/insights Authentication [#authentication] Requires a `read` or `root` client. The default `write` client does not have access. See the [Authentication](/docs/api/authentication) guide. Base URL [#base-url] ``` https://api.pieowl.com/insights/{projectId} ``` `{projectId}` is your site ID. Example [#example] ```bash curl 'https://api.pieowl.com/insights/YOUR_PROJECT_ID/metrics?range=30d' \ -H 'pieowl-client-id: YOUR_CLIENT_ID' \ -H 'pieowl-client-secret: YOUR_CLIENT_SECRET' ``` Common endpoints [#common-endpoints] | Endpoint | What it returns | | ---------------------------------------------------- | ------------------------------------------------- | | `GET /metrics` | Sessions, pageviews, bounce rate and other totals | | `GET /live` | The number of visitors on your site right now | | `GET /pages` | Top pages | | `GET /referrer`, `/referrer_name`, `/referrer_type` | Traffic sources | | `GET /country`, `/region`, `/city` | Location breakdown | | `GET /device`, `/browser`, `/os` | Device and technology breakdown | | `GET /utm_source`, `/utm_medium`, `/utm_campaign`, … | UTM parameter breakdown | | `GET /funnel` | Conversion for a sequence of events | | `GET /retention` | Retention over time | There are more endpoints for events, profiles, sessions, groups, saved reports and Google Search Console data. The [API Reference](/docs/api-reference) lists them all. Query parameters [#query-parameters] Most endpoints accept: * `range`: a preset such as `today`, `7d`, `30d`, `3m`, `6m` or `12m`. The default is `7d`. * `startDate` and `endDate`: a custom date range. * `filters`: a JSON array of filters. * `cursor` and `limit`: paging for list endpoints. For full schemas, see the [API Reference](/docs/api-reference). --- ## Manage API Overview URL: https://pieowl.com/docs/api/manage Authentication [#authentication] The Manage API requires a **root** client. A root client has access to every site in your team and can manage all of their resources. See the [Authentication](/docs/api/authentication) guide. Base URL [#base-url] ``` https://api.pieowl.com/manage ``` Example [#example] ```bash curl 'https://api.pieowl.com/manage/projects' \ -H 'pieowl-client-id: YOUR_ROOT_CLIENT_ID' \ -H 'pieowl-client-secret: YOUR_ROOT_CLIENT_SECRET' ``` Resources [#resources] | Resource | What you can do | | --------------------------------------------- | -------------------------------------------------------------- | | [Sites (projects)](/docs/api/manage/projects) | Create, update and delete sites | | [Clients](/docs/api/manage/clients) | Manage API clients (`read`, `write`, `root`) and their secrets | | [References](/docs/api/manage/references) | Mark important dates on your charts | You can also list a site's dashboards with `GET /manage/projects/{projectId}/dashboards`, and the reports on a dashboard with `GET /manage/projects/{projectId}/dashboards/{dashboardId}/reports`. Rate limiting [#rate-limiting] 20 requests per 10 seconds per client. For full endpoint schemas, see the [API Reference](/docs/api-reference). --- ## Clients URL: https://pieowl.com/docs/api/manage/clients Authentication [#authentication] Requires a `root` client. See the [Authentication](/docs/api/authentication) guide. Base URL [#base-url] ``` https://api.pieowl.com/manage/clients ``` Client types [#client-types] | Type | What it can do | | ------- | --------------------------------------- | | `write` | Send events and profile updates | | `read` | Read analytics data (Insights, Export) | | `root` | Full access: Manage API, read and write | Endpoints [#endpoints] | Method | Path | What it does | | -------- | ---------------------- | ------------------------------------------------------- | | `GET` | `/manage/clients` | Lists all clients. Add `projectId` (site ID) to filter. | | `GET` | `/manage/clients/{id}` | Gets one client | | `POST` | `/manage/clients` | Creates a client | | `PATCH` | `/manage/clients/{id}` | Renames a client | | `DELETE` | `/manage/clients/{id}` | Deletes a client for good | Create a client [#create-a-client] ```bash curl -X POST 'https://api.pieowl.com/manage/clients' \ -H 'Content-Type: application/json' \ -H 'pieowl-client-id: YOUR_ROOT_CLIENT_ID' \ -H 'pieowl-client-secret: YOUR_ROOT_CLIENT_SECRET' \ -d '{ "name": "Reporting script", "projectId": "YOUR_PROJECT_ID", "type": "read" }' ``` If you leave out `type`, the client is a `write` client. The client secret is only returned when you create the client. You can't get it again later. For full request and response schemas, see the [API Reference](/docs/api-reference). --- ## Sites (projects) URL: https://pieowl.com/docs/api/manage/projects In the API, a site is called a project: the path is `/manage/projects`, and a site ID is `projectId`. Authentication [#authentication] Requires a `root` client. See the [Authentication](/docs/api/authentication) guide. Base URL [#base-url] ``` https://api.pieowl.com/manage/projects ``` Endpoints [#endpoints] | Method | Path | What it does | | -------- | ----------------------- | ------------------------------------------- | | `GET` | `/manage/projects` | Lists all sites in your team | | `GET` | `/manage/projects/{id}` | Gets one site | | `POST` | `/manage/projects` | Creates a site | | `PATCH` | `/manage/projects/{id}` | Updates a site | | `DELETE` | `/manage/projects/{id}` | Schedules the site for deletion in 24 hours | Create a site [#create-a-site] ```bash curl -X POST 'https://api.pieowl.com/manage/projects' \ -H 'Content-Type: application/json' \ -H 'pieowl-client-id: YOUR_ROOT_CLIENT_ID' \ -H 'pieowl-client-secret: YOUR_ROOT_CLIENT_SECRET' \ -d '{ "name": "My website", "domain": "https://example.com", "cors": ["https://example.com"], "types": ["website"] }' ``` When you create a site, Pieowl also creates a default `write` client and returns it in the response. The client secret is only shown once, so save it right away. For full request and response schemas, see the [API Reference](/docs/api-reference). --- ## References URL: https://pieowl.com/docs/api/manage/references Authentication [#authentication] Requires a `root` client. See the [Authentication](/docs/api/authentication) guide. Base URL [#base-url] ``` https://api.pieowl.com/manage/references ``` What are references? [#what-are-references] References are markers on your charts. Use them for product launches, campaign starts, feature releases, or any date you want to compare against changes in your numbers. Endpoints [#endpoints] | Method | Path | What it does | | -------- | ------------------------- | ---------------------------------------------------------- | | `GET` | `/manage/references` | Lists all references. Add `projectId` (site ID) to filter. | | `GET` | `/manage/references/{id}` | Gets one reference | | `POST` | `/manage/references` | Creates a reference | | `PATCH` | `/manage/references/{id}` | Updates a reference | | `DELETE` | `/manage/references/{id}` | Deletes a reference | Create a reference [#create-a-reference] ```bash curl -X POST 'https://api.pieowl.com/manage/references' \ -H 'Content-Type: application/json' \ -H 'pieowl-client-id: YOUR_ROOT_CLIENT_ID' \ -H 'pieowl-client-secret: YOUR_ROOT_CLIENT_SECRET' \ -d '{ "projectId": "YOUR_PROJECT_ID", "title": "New pricing page", "description": "Launched the new pricing page", "datetime": "2026-09-15T09:00:00Z" }' ``` For full request and response schemas, see the [API Reference](/docs/api-reference). --- ## Track URL: https://pieowl.com/docs/api/track Good to know [#good-to-know] * Send the `pieowl-client-ip` header with the visitor's IP address to get their location. Pieowl uses the IP only for the location lookup and the visitor hash. It does not store it. * Send the `user-agent` header with the visitor's user agent to get their device, browser and OS. Authentication [#authentication] Any client type works. From a server, send your client ID and client secret. See the [Authentication](/docs/api/authentication) guide. ```bash -H "pieowl-client-id: YOUR_CLIENT_ID" \ -H "pieowl-client-secret: YOUR_CLIENT_SECRET" ``` Base URL [#base-url] ``` https://api.pieowl.com ``` Event types [#event-types] Send a `POST` request to `/track`. The `type` field decides what gets recorded, and `payload` holds the data: | Type | What it does | | -------------- | ---------------------------------------------- | | `track` | Records a named event with optional properties | | `identify` | Creates or updates a user profile | | `increment` | Adds to a number property on a profile | | `decrement` | Subtracts from a number property on a profile | | `group` | Creates or updates a group | | `assign_group` | Links a profile to one or more groups | Example: track an event [#example-track-an-event] ```bash curl -X POST 'https://api.pieowl.com/track' \ -H 'Content-Type: application/json' \ -H 'pieowl-client-id: YOUR_CLIENT_ID' \ -H 'pieowl-client-secret: YOUR_CLIENT_SECRET' \ -H 'pieowl-client-ip: 203.0.113.42' \ -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)' \ -d '{ "type": "track", "payload": { "name": "signup", "properties": { "plan": "pro" }, "profileId": "user_123" } }' ``` A successful request returns `200` with the `deviceId` and `sessionId` the event was linked to. Example: send a purchase [#example-send-a-purchase] A purchase is a `track` event named `revenue`. `__amount` is in normal units, like `49.99`. `__currency` is optional and defaults to your site currency. `__orderId` is your order number: the same order sent twice counts once. `__deviceId` is the visitor's device ID from `pieowl.getDeviceId()` in the browser, so the purchase is credited to their visit. ```bash curl -X POST 'https://api.pieowl.com/track' \ -H 'Content-Type: application/json' \ -H 'pieowl-client-id: YOUR_CLIENT_ID' \ -H 'pieowl-client-secret: YOUR_CLIENT_SECRET' \ -d '{ "type": "track", "payload": { "name": "revenue", "properties": { "__amount": 49.99, "__currency": "EUR", "__orderId": "A-1001", "__deviceId": "DEVICE_ID" } } }' ``` For a refund, send the same event with `"__refund": true` and the `__orderId`. Leave out `__amount` to refund the whole order. See [Revenue attribution](/docs/revenue-attribution). Example: identify a user [#example-identify-a-user] ```bash curl -X POST 'https://api.pieowl.com/track' \ -H 'Content-Type: application/json' \ -H 'pieowl-client-id: YOUR_CLIENT_ID' \ -H 'pieowl-client-secret: YOUR_CLIENT_SECRET' \ -d '{ "type": "identify", "payload": { "profileId": "user_123", "email": "jane@example.com", "firstName": "Jane", "properties": { "company": "Example Inc" } } }' ``` Groups and events [#groups-and-events] Pieowl never adds groups to events on its own, not even after `assign_group`. Pass `groups` on each `track` call that needs group data. For the full request and response schema of every event type, see the [API Reference](/docs/api-reference/track/track/post). --- ## Do you need a cookie banner? URL: https://pieowl.com/docs/consent-and-cookie-banners Pieowl sets no cookies, so in many cases you don't need a consent banner for analytics. But "no cookies" doesn't settle it on its own. The answer depends on your country, on the features you turn on and on the other tools on your site. This page is information, not legal advice. Ask your legal advisor about your own site. What Pieowl stores and reads [#what-pieowl-stores-and-reads] * **No cookies.** The script never sets a cookie. * **Nothing stored on your visitors' devices.** No local storage, no session storage, no IndexedDB. * **IP addresses are never stored.** The IP is used to look up an approximate location and to build the visitor ID, then dropped. * **Visitor IDs reset every day.** The ID is a hash of the browser's user agent, the IP, your site and a secret salt. The salt changes every day. * **No cross-site tracking.** The ID includes your site, so the same visitor gets a different ID on every site. * **Session replay is off unless you turn it on.** Text and form fields are always hidden in session replays. The script does read some things in the browser and send them to Pieowl: the page address and title, the referrer, and the clicks and events you track. See [What gets collected](/docs/sdks/script#what-gets-collected) and our [data policy](/data-policy) for the full list. Why the rules can apply without cookies [#why-the-rules-can-apply-without-cookies] The EU rule on devices (ePrivacy Directive, Article 5(3)) covers reading information from a device, not only storing cookies. The EDPB, the board of EU data protection regulators, says a script that makes the browser send information falls under that rule (Guidelines 2/2023, paragraph 33). So the question is not only "are there cookies?" but "does an exemption cover this?". The EU has no single exemption for analytics. Some countries have one. By country [#by-country] * **France.** The CNIL (the French regulator) lets audience measurement run without consent if it meets its conditions. The main ones: it only measures your own site's audience, for you alone; it produces only anonymous statistics; the data isn't combined with other data or passed on; and it doesn't follow people across sites. The CNIL also expects an opt-out and recommends keeping the data no longer than 25 months. Session replay, revenue and conversion tracking, and campaign (UTM) tracking fall outside this exemption. * **Netherlands.** The law has an exemption for analytics with little or no impact on privacy (Telecommunicatiewet, Article 11.7a). * **Germany, Poland and Ireland.** There is no general exemption for analytics. Germany's regulators give no blanket answer. Poland's law (Prawo komunikacji elektronicznej, Article 399) only exempts what is needed to send a message or provide the service the visitor asked for. Ireland's regulator says analytics needs consent. Ask your advisor. You may need a consent tool: see [Consent management](/docs/consent-management). * **Other countries.** Check your own regulator's guidance. Features that need consent [#features-that-need-consent] Session replay [#session-replay] Recording what a visitor does needs their consent. When you turn session replay on, keep **Record only after visitors agree** on in **Settings → Site → What Pieowl counts**. It's on by default. Pieowl then records nothing until your site calls `allowReplay` after the visitor accepts in your own consent banner: ```js // Script tag window.pieowl('allowReplay'); // npm package pieowl.allowReplay(); ``` Pieowl stores nothing in the browser, so it can't remember the choice for you. Call `allowReplay` on every page where the visitor has agreed. See [Consent management](/docs/consent-management#session-replay-only-after-consent). Revenue sent from the browser [#revenue-sent-from-the-browser] A `purchase()` call from the browser links a sale to the visit. That is conversion measurement, and France's exemption doesn't cover it. Treat it as needing consent. Prefer sending purchases from your server or with your payment provider's webhooks: see [Revenue attribution](/docs/revenue-attribution#from-your-payment-provider-no-code). What you must do anyway [#what-you-must-do-anyway] Even where you don't need a banner: * **Name Pieowl in your privacy policy.** Say that you use Pieowl for analytics and what it collects. Our [data policy](/data-policy#copy-this-into-your-privacy-policy) has text you can copy. * **Sign the DPA.** Pieowl processes visit data for you. Our [data processing agreement](/dpa) covers that. * **Keep an opt-out.** Give visitors a way to say no, for example a link in your privacy policy. The script's [`filter` option](/docs/sdks/script#filtering-events) can check their choice. * **Have a legal basis under GDPR.** Most sites rely on legitimate interest for analytics. Sources [#sources] * EDPB Guidelines 2/2023 on the technical scope of Art. 5(3) ePrivacy Directive: [https://www.edpb.europa.eu/system/files/2024-10/edpb\_guidelines\_202302\_technical\_scope\_art\_53\_eprivacydirective\_v2\_en\_0.pdf](https://www.edpb.europa.eu/system/files/2024-10/edpb_guidelines_202302_technical_scope_art_53_eprivacydirective_v2_en_0.pdf) * CNIL, audience measurement tools (in French): [https://www.cnil.fr/fr/cookies-et-autres-traceurs/regles/cookies-solutions-pour-les-outils-de-mesure-daudience](https://www.cnil.fr/fr/cookies-et-autres-traceurs/regles/cookies-solutions-pour-les-outils-de-mesure-daudience) * CNIL, self-assessment for consent-exempt audience measurement (in French): [https://www.cnil.fr/sites/default/files/2025-07/outil\_d\_auto-evaluation\_mesure\_d\_audience.pdf](https://www.cnil.fr/sites/default/files/2025-07/outil_d_auto-evaluation_mesure_d_audience.pdf) * CNIL, draft recommendation on session replay (in French): [https://www.cnil.fr/sites/default/files/2026-02/projet\_de\_recommandation\_rejeu\_session.pdf](https://www.cnil.fr/sites/default/files/2026-02/projet_de_recommandation_rejeu_session.pdf) * Netherlands, Telecommunicatiewet Article 11.7a: [https://wetten.overheid.nl/BWBR0009950/2024-01-01#Hoofdstuk11\_Artikel11.7a](https://wetten.overheid.nl/BWBR0009950/2024-01-01#Hoofdstuk11_Artikel11.7a) * Germany, DSK guidance on digital services: [https://datenschutzkonferenz-online.de/media/oh/OH\_Digitale\_Dienste.pdf](https://datenschutzkonferenz-online.de/media/oh/OH_Digitale_Dienste.pdf) * Poland, Prawo komunikacji elektronicznej: [https://isap.sejm.gov.pl/isap.nsf/DocDetails.xsp?id=WDU20240001221](https://isap.sejm.gov.pl/isap.nsf/DocDetails.xsp?id=WDU20240001221) * Ireland, DPC guidance on cookies and other tracking technologies: [https://www.dataprotection.ie/sites/default/files/uploads/2020-04/Guidance%20note%20on%20cookies%20and%20other%20tracking%20technologies.pdf](https://www.dataprotection.ie/sites/default/files/uploads/2020-04/Guidance%20note%20on%20cookies%20and%20other%20tracking%20technologies.pdf) Related [#related] * [Consent management](/docs/consent-management): hold tracking until the visitor agrees * [Session replay](/docs/session-replay): privacy controls for recordings * [Data policy](/data-policy): everything Pieowl collects --- ## Consent management URL: https://pieowl.com/docs/consent-management Some places require explicit consent before you track events or record sessions. Pieowl supports both: * **Hold all tracking:** initialize with `disabled: true` and nothing is sent until you call `ready()`. * **Hold only session replay:** keep **Record only after visitors agree** on, and call `allowReplay()` when the visitor accepts. Analytics keeps running. Not sure which you need? See [Do you need a cookie banner?](/docs/consent-and-cookie-banners). How it works [#how-it-works] When `disabled: true` is set, all calls to `track`, `identify`, `screenView`, and session replay chunks are held in an in-memory queue instead of being sent to the API. Once the user consents, call `ready()` and the entire queue is flushed immediately. ```ts const pieowl = new Pieowl({ clientId: 'YOUR_CLIENT_ID', disabled: true, // nothing sent until ready() is called }); // Later, when the user accepts your consent banner: pieowl.ready(); ``` If the user declines, don't call `ready()`. The queue is thrown away when the page unloads. With the script tag, use the [`init` form](/docs/sdks/script#options-in-code), add `disabled: true` to the `init` options, and call `window.pieowl('ready')` when the user accepts. Session replay only after consent [#session-replay-only-after-consent] Session replay needs your visitors' consent. In **Settings → Site → What Pieowl counts**, keep **Record only after visitors agree** on when you turn replay on. It's on by default. Pieowl then doesn't start the recorder until your site calls `allowReplay()`. Page views and events are still counted. ```ts // Script tag: when the visitor accepts in your consent banner window.pieowl('allowReplay'); // npm package pieowl.allowReplay(); ``` If the visitor declines, don't call it. Nothing is recorded. Pieowl stores nothing in the browser, so it doesn't remember the choice. Your consent tool does. Call `allowReplay()` on every page where the visitor has agreed, for example right after the script loads when your consent tool says yes. With session replay and `disabled` [#with-session-replay-and-disabled] You can use both. With `disabled: true`, replay chunks are queued like events and sent when you call `ready()`. With **Record only after visitors agree** on, the recorder also waits for `allowReplay()`. If one banner covers both, call both when the visitor accepts: ```ts const pieowl = new Pieowl({ clientId: 'YOUR_CLIENT_ID', disabled: true, }); // User accepts consent: pieowl.ready(); pieowl.allowReplay(); ``` If you turn **Record only after visitors agree** off, the recorder starts as soon as the page loads and only `ready()` holds the data back. Keep the setting on so nothing is recorded before the visitor agrees. Waiting for a user profile [#waiting-for-a-user-profile] If you want to hold events until you know who the user is, rather than waiting for consent, use `waitForProfile`. Events are queued until `identify()` is called with a `profileId`. `waitForProfile` is deprecated. It still works, but for new code we recommend `disabled: true` and calling `ready()` yourself. ```ts const pieowl = new Pieowl({ clientId: 'YOUR_CLIENT_ID', waitForProfile: true, }); // Events queue here... pieowl.track('page_view'); // Queue is flushed once a profileId is set: pieowl.identify({ profileId: 'user_123' }); ``` If the user never authenticates, the queue is never flushed automatically — no events will be sent. To handle anonymous users or guest flows, call `ready()` explicitly when you know the user won't identify: ```ts // User skipped login — flush queued events without a profileId pieowl.ready(); ``` `ready()` always releases the queue regardless of whether `waitForProfile` or `disabled` is set. Related [#related] * [Do you need a cookie banner?](/docs/consent-and-cookie-banners) — what Pieowl stores and when you need consent * [Consent management guide](/guides/consent-management) — full walkthrough with a cookie banner example * [Session replay](/docs/session-replay) — privacy controls for replay recordings * [Identify users](/docs/get-started/identify-users) — link events to a user profile --- ## How to set up notifications and integrations URL: https://pieowl.com/docs/dashboard/notifications-and-integrations How it works [#how-it-works] There are two separate concepts to understand before you start: * **Integrations** are connections to external services like Slack, Discord, or a custom webhook. Each integration belongs to one site. * **Notification rules** are the conditions that trigger a notification. Rules live inside individual sites and reference one or more integrations. A rule does nothing until it has an integration attached—and an integration does nothing until a rule uses it. * **Notifications** are the messages that are sent when a rule is triggered. A notification can be sent as a json object or a template with variables. Step 1: Create an integration [#create-integration] Open the site you want alerts for and click **Integrations** in the sidebar. On the **Available** tab, choose the service you want to connect. For notifications, Pieowl supports: * **Slack** — authenticate via OAuth and pick a channel * **Discord** — paste a Discord webhook URL for a channel * **Webhook** — send an HTTP POST to any URL you control Fill in the required details and save. The integration is now available to the notification rules for this site. The same page also has S3 and Google Cloud Storage integrations. They export your raw events to your own storage instead of sending alerts. Step 2: Go to your site's notification rules [#create-rule] Integrations alone don't do anything. To start receiving alerts, open the site you want to monitor, click **Notifications** in the left sidebar, and switch to the **Rules** tab. Click **Add Rule** to open the rule editor on the right side of the screen. Give your rule a name, then choose a **Type**. There are two types: | Type | When it triggers | | ---------- | ---------------------------------------------------------------------- | | **Event** | Immediately when a matching event is received | | **Funnel** | After a session ends and all funnel steps have been completed in order | Event rules [#event-rules] Event rules fire in real time. The moment Pieowl receives an event that matches your filters, the notification is sent. In the rule editor: 1. Set **Type** to **Events** 2. Add one or more events from the **Events** list. You can filter each event by its properties (for example, only trigger when `path` starts with `/onboarding`) 3. Write a **Template** for the notification message. Use `{{property_name}}` to insert event properties dynamically—for example, `New user with their first event from {{country}}`. 4. Under **Integrations**, select which integration(s) should receive the notification Click **Update** to save the rule. Templates are not used if your webhook integration uses a JavaScript transformer. Funnel rules [#funnel-rules] Funnel rules let you track multi-step flows and notify you only when a user completes every step in the correct sequence—for example, `session_start` → `subscription_checkout` → `subscription_created`. In the rule editor: 1. Set **Type** to **Funnel** 2. Add each event in the funnel, in the order they must occur. You can optionally add property filters to each step 3. Write a **Template** for the notification message 4. Select your **Integration(s)** Click **Update** to save. **Important:** Funnel rule notifications are sent after the session ends, not immediately when the last step fires. Pieowl waits until the session is complete before checking the funnel sequence. Templates are not used if your webhook integration uses a JavaScript transformer. View notifications [#view-notifications] Switch to the **Notifications** tab (the default view) to see every notification that has been triggered for your site. Each row shows the notification title alongside the country, OS, browser, and profile of the user who triggered it. You can filter the list by creation date or search by title to find specific events. Frequently asked questions [#frequently-asked-questions] No. Each integration belongs to one site. To get alerts from another site, add the integration to that site too. Funnel rules trigger after the session ends, not when the last event fires. If the user's session is still active, the notification is queued until the session closes. Make sure the full funnel sequence was completed within a single session. Yes. For each event in the rule, click the filter icon to add property conditions—for example, only trigger when `plan` equals `enterprise` or `country` equals `US`. For notifications: Slack, Discord, and custom webhooks. For data export: S3 and Google Cloud Storage. Yes. The integrations selector on each rule allows you to pick multiple destinations. A single triggered rule will send a notification to all selected integrations simultaneously. --- ## Understand the overview URL: https://pieowl.com/docs/dashboard/understand-the-overview Top stats [#top-stats] The row of metric cards at the top of the page is the fastest way to understand the health of your site. Each card shows the value for the selected time range and a comparison to the previous period of the same length. Unique Visitors [#unique-visitors] The number of distinct profile IDs recorded in the selected period. How accurate this is depends on whether you use [identify](/docs/get-started/identify-users): * **Without identify**: Pieowl generates a profile ID that rotates every day. A visitor returning on 10 different days will be counted as 10 unique visitors, because each day produces a new ID. * **With identify**: The profile ID is tied to the user's real identity. The same person visiting on 10 different days is counted as 1 unique visitor across the entire period. If cross-day deduplication matters to your analysis, set up [user identification](/docs/get-started/identify-users). Sessions [#sessions] The total number of sessions in the selected period. A session begins when someone arrives on your site and ends after 30 minutes without activity. One visitor can have many sessions across a day. Pageviews [#pageviews] The total number of page views (`screen_view` events) recorded across all sessions. Every time a visitor loads a page—including navigating between pages in a single session—it counts as one pageview. Pages per Session [#pages-per-session] The average number of pages viewed within a single session, calculated as `total pageviews / total sessions`. A higher number means visitors are exploring more of your site before leaving. Bounce Rate [#bounce-rate] The percentage of sessions where a visitor viewed only a single page and left. Calculated as `single-page sessions / total sessions × 100`. Lower is generally better—it means more visitors are engaging beyond the first page. > A session is counted as a bounce if the visitor triggered exactly one `screen_view` event before the session ended. Sessions where visitors read one article deeply and leave still count as bounces. Session Duration [#session-duration] The average length of a session, shown in minutes, calculated only from sessions where the visitor did something after the first page load (duration > 0). Sessions where a visitor immediately left are excluded from the average to avoid skewing the number. Revenue [#revenue] The total value of purchases credited to visits in the selected period. It's shown in your site currency, set in Settings → Site → Revenue. Purchases in other currencies are converted. Revenue only appears if you track revenue events. See [Revenue attribution](/docs/revenue-attribution) for setup and for how purchases are credited to sources, pages and countries. *** The time-series chart [#the-time-series-chart] Directly below the stat cards is a line chart that shows how the selected metric changes over time. Click any stat card to switch the chart to that metric. The chart uses the **interval** you select (hour, day, week, or month) to group data points. A faint dashed line shows the equivalent period from the previous comparison window, so you can spot trends at a glance. When any metric other than Revenue is active, the chart also overlays revenue as green bars on a secondary Y-axis—this lets you correlate traffic patterns with revenue without switching cards. The trailing edge of the line (the current, incomplete interval) is shown as a dashed segment to remind you that the period is still accumulating data. *** Insights [#insights] A scrollable row of insight cards appears below the chart once Pieowl has found notable changes in your data. Pieowl detects significant trends across pages, entry pages, referrers, locations and devices on its own—no setup needed. Each card shows: * **Share**: The percentage of total traffic that property represents (e.g., "United States: 42% of all sessions") * **Absolute change**: The raw increase or decrease in sessions compared to the previous period * **Percentage change**: How much that property grew or declined relative to its own previous value For example, if the US had 1,000 sessions last week and 1,200 this week, the card shows "+200 sessions (+20%)". Clicking any insight card filters the entire overview page to show only data matching that property—letting you drill into what's driving the trend. *** Sources [#sources] The Sources widget shows where your visitors came from. Switch between tabs to see different dimensions: | Tab | What it shows | | ------------ | ----------------------------------------------------------------- | | **Refs** | Grouped referrer names (e.g., "Google", "Twitter", "Hacker News") | | **Urls** | Raw referrer URLs | | **Types** | Referrer categories: `search`, `social`, `email`, `unknown` | | **Source** | `utm_source` query parameter values | | **Medium** | `utm_medium` query parameter values | | **Campaign** | `utm_campaign` query parameter values | | **Term** | `utm_term` query parameter values | | **Content** | `utm_content` query parameter values | Referrer names and types are resolved automatically from the raw referrer URL using a built-in lookup table. Direct traffic (no referrer) appears as `(not set)`. Each row shows sessions and pageviews. Clicking a row filters the entire overview page to only show data from that source. *** Pages [#pages] The Pages widget shows which URLs your visitors are landing on, exiting from, and spending time on. | Tab | What it shows | | --------------- | --------------------------------------------------------------------------- | | **Top pages** | Pages ranked by unique sessions. Each row is a `origin + path` combination. | | **Entry pages** | The first page of each session—the page where visitors arrived. | | **Exit pages** | The last page of each session—the page where visitors left. | High exit rates on a page are not always bad—they can reflect a page that successfully answers a question. High bounce on an entry page is more diagnostic. Compare entry and exit distributions to understand the shape of your user journeys. Clicking a page row filters the whole overview to sessions that included that page. *** Devices [#devices] The Devices widget breaks down your audience by hardware and software. Switch between tabs: | Tab | What it shows | | ---------------- | ----------------------------------------------------- | | **Device** | Device type: Desktop, Mobile, Tablet | | **Brand** | Hardware brand (Apple, Samsung, etc.) | | **Model** | Specific device model | | **Browser** | Browser name (Chrome, Safari, Firefox, etc.) | | **Browser ver.** | Browser version number | | **OS** | Operating system (macOS, Windows, iOS, Android, etc.) | | **OS ver.** | Operating system version | Each row shows sessions and pageviews. Use this widget to prioritize which browsers and operating systems to test and optimize for. *** Events [#events] The Events widget shows the most frequent custom events fired in the selected period, ranked by count. System events (`session_start`, `session_end`, `screen_view`) are excluded—only the events you instrument yourself appear here. Click any event to filter the overview to sessions where that event was fired. *** Geo [#geo] The Geo widget shows the geographic distribution of your visitors. Switch between tabs: | Tab | What it shows | | ----------- | -------------------------------------------- | | **Country** | Visitor country, derived from IP geolocation | | **Region** | State or province | | **City** | City level | Below the table, a world map plots the same data as a heatmap—darker areas represent more sessions. This gives you a quick visual of where your audience is concentrated. Clicking a country, region, or city filters the whole overview to that location. *** Activity heatmap [#activity-heatmap] The activity heatmap at the bottom of the page shows when your visitors are most active, broken down by day of the week (Monday through Sunday) and hour of the day (00:00–23:00). Each cell shows the **average** of the selected metric at that day-and-hour combination, averaged across all weeks in the selected period. Darker cells indicate higher average values. Hover any cell to see the exact average. You can switch the metric being visualized using the tabs above the heatmap: * **Unique Visitors** * **Sessions** * **Pageviews** * **Bounce Rate** * **Pages / Session** * **Session Duration** Use the heatmap to identify peak traffic windows, plan campaigns, and schedule maintenance during quiet periods. *** User Journey [#user-journey] The User Journey (Sankey) diagram at the very bottom visualizes how visitors flow through your site within a session. It answers the question: after landing on page A, where do visitors go next? **How it works:** 1. Pieowl finds the top 3 most common entry pages in the selected period. 2. From each entry page, it finds the top 3 most frequent next pages (step 2), then the top 3 from those (step 3), and so on up to the configured number of steps (default 5, adjustable to a maximum of 10). 3. Paths that represent less than 0.25% of total sessions are filtered out to reduce visual noise. 4. Consecutive duplicate pages within a session are collapsed into one step (e.g., if someone refreshed a page, it only counts once in the journey). Each node shows the page URL. The width of the connecting flows is proportional to the number of sessions that followed that path. Use the User Journey to find drop-off points, discover unexpected popular paths, and understand whether visitors are reaching your key conversion pages. *** Filters and time controls [#filters-and-time-controls] Every widget on the overview page responds to the same set of global filters and time controls at the top of the page. **Range**: choose a preset (like Today or Last 7 days) or a custom date range. **Interval**: controls how data is grouped in the time-series chart (hour, day, week, month). **Event filter**: narrow the entire overview to sessions that include a specific event—useful for analyzing the behavior of users who completed a particular action. **Dimension filters**: clicking any row in any widget (a country, a source, a page) applies that value as a filter. Active filters are shown as chips below the time controls. Remove a filter by clicking the × on its chip. **Live counter**: a green badge in the top-right corner shows the number of active visitors (visitors who fired an event in the last 5 minutes). Click it for a 30-minute session histogram. --- ## Groups URL: https://pieowl.com/docs/get-started/groups Groups let you associate users with a shared entity — like a company, workspace, or team — and analyze behavior at that level. Instead of asking "what did Jane do?", you can ask "what is Acme Inc doing?" This is especially useful for B2B SaaS products where a single paying account has many users. How Groups work [#how-groups-work] There are two separate concepts: 1. **The group entity** — created/updated with `upsertGroup()`. Stores metadata about the group (name, plan, etc.). 2. **Group membership** — set with `setGroup()` / `setGroups()`. Links a user profile to one or more groups, and automatically attaches those group IDs to every subsequent `track()` call. The examples use `pieowl`, a `Pieowl` instance from `@pieowl/web` or `@pieowl/sdk`. With the script tag, call the same methods on `window.pieowl`, for example `window.pieowl('setGroup', 'org_acme')`. Creating or updating a group [#creating-or-updating-a-group] Call `upsertGroup()` to create a group or update its properties. The group is identified by its `id` and `type`. ```typescript pieowl.upsertGroup({ id: 'org_acme', // Your group's unique ID type: 'company', // Group type (company, workspace, team, etc.) name: 'Acme Inc', // Display name properties: { plan: 'enterprise', seats: 25, industry: 'logistics', }, }); ``` Group payload [#group-payload] | Field | Type | Required | Description | | ------------ | -------- | -------- | --------------------------------------------------- | | `id` | `string` | Yes | Unique identifier for the group | | `type` | `string` | Yes | Category of group (e.g. `"company"`, `"workspace"`) | | `name` | `string` | Yes | Human-readable display name | | `properties` | `object` | No | Custom metadata about the group | Managing groups in the dashboard [#managing-groups-in-the-dashboard] The easiest way to create, edit, and delete groups is directly in the Pieowl dashboard. Open your site and go to the **Groups** section — from there you can manage group names, types, and properties without touching any code. `upsertGroup()` is the right tool when your group properties are **dynamic and driven by your own data** — for example, syncing a customer's current plan, seat count, or MRR from your backend at login time. A good rule of thumb: call `upsertGroup()` on login or when group properties change — not on every request or page view. If you find yourself calling it frequently with the same data, the dashboard is probably the better place to manage that group. Assigning a user to a group [#assigning-a-user-to-a-group] After identifying a user, call `setGroup()` to link them to a group. This also attaches the group ID to all future `track()` calls for the current session. ```typescript // After login pieowl.identify({ profileId: 'user_123' }); // Link the user to their organization pieowl.setGroup('org_acme'); ``` For users that belong to multiple groups: ```typescript pieowl.setGroups(['org_acme', 'team_engineering']); ``` `setGroup()` and `setGroups()` persist group IDs on the SDK instance. All subsequent `track()` calls will automatically include these group IDs until `clear()` is called. Full login flow example [#full-login-flow-example] `setGroup()` doesn't require the group to exist first. You can call it with just an ID — events will be tagged with that group ID, and you can create the group later in the dashboard or via `upsertGroup()`. ```typescript // 1. Identify the user pieowl.identify({ profileId: 'user_123', firstName: 'Jane', email: 'jane@acme.com', }); // 2. Assign the user to the group — the group doesn't need to exist yet pieowl.setGroup('org_acme'); // 3. All subsequent events are now tagged with the group pieowl.track('dashboard_viewed'); // → includes groups: ['org_acme'] pieowl.track('report_exported'); // → includes groups: ['org_acme'] ``` If you want to sync dynamic group properties from your own data (plan, seats, MRR), add `upsertGroup()` to the flow: ```typescript pieowl.identify({ profileId: 'user_123', email: 'jane@acme.com' }); // Sync group metadata from your backend pieowl.upsertGroup({ id: 'org_acme', type: 'company', name: 'Acme Inc', properties: { plan: 'pro' }, }); pieowl.setGroup('org_acme'); ``` Per-event group override [#per-event-group-override] You can attach group IDs to a specific event without affecting the SDK's persistent group state: ```typescript pieowl.track('file_shared', { filename: 'q4-report.pdf', groups: ['org_acme', 'org_partner'], // Only applies to this event }); ``` Groups passed in `track()` are **merged** with any groups already set on the SDK instance. Clearing groups on logout [#clearing-groups-on-logout] `clear()` resets the profile, device, session, and all groups. Always call it on logout. ```typescript function handleLogout() { pieowl.clear(); // redirect to login... } ``` Common patterns [#common-patterns] B2B SaaS — company accounts [#b2b-saas--company-accounts] ```typescript // On login pieowl.identify({ profileId: user.id, email: user.email }); pieowl.upsertGroup({ id: user.organizationId, type: 'company', name: user.organizationName, properties: { plan: user.plan, mrr: user.mrr }, }); pieowl.setGroup(user.organizationId); ``` Multi-tenant — workspaces [#multi-tenant--workspaces] ```typescript // When user switches workspace pieowl.upsertGroup({ id: workspace.id, type: 'workspace', name: workspace.name, }); pieowl.setGroup(workspace.id); ``` Teams within a company [#teams-within-a-company] ```typescript // User belongs to a company and a specific team pieowl.setGroups([user.organizationId, user.teamId]); ``` API reference [#api-reference] `upsertGroup(payload)` [#upsertgrouppayload] Creates the group if it doesn't exist, or merges properties into the existing group. ```typescript pieowl.upsertGroup({ id: string; // Required type: string; // Required name: string; // Required properties?: Record; }); ``` `setGroup(groupId)` [#setgroupgroupid] Adds a single group ID to the SDK's internal group list and sends an `assign_group` event to link the current profile to that group. ```typescript pieowl.setGroup('org_acme'); ``` `setGroups(groupIds)` [#setgroupsgroupids] Same as `setGroup()` but for multiple group IDs at once. ```typescript pieowl.setGroups(['org_acme', 'team_engineering']); ``` What to avoid [#what-to-avoid] * **Calling `upsertGroup()` on every event or page view** — call it on login or when group properties actually change. For static group management, use the dashboard instead. * **Not calling `setGroup()` after `identify()`** — without it, events won't be tagged with the group and you won't see group-level data in the dashboard. * **Forgetting `clear()` on logout** — groups persist on the SDK instance, so a new user logging in on the same session could inherit the previous user's groups. * **Using `upsertGroup()` to link a user to a group** — `upsertGroup()` manages the group entity only. Use `setGroup()` to link a user profile to it. --- ## Identify Users URL: https://pieowl.com/docs/get-started/identify-users By default, Pieowl doesn't know which of your users a visitor is. To connect their events to a user in your database, identify them. How it works [#how-it-works] When a user logs in or signs up, call `identify`. This links their current session and all later events to the ID from your system. ```javascript pieowl.identify({ profileId: 'user_123' }); ``` Adding user traits [#adding-user-traits] You can also pass traits like name, email or plan. They show up on the user's profile in your dashboard. ```javascript pieowl.identify({ profileId: 'user_123', firstName: 'Jane', lastName: 'Doe', email: 'jane@example.com', properties: { company: 'Acme Inc', plan: 'pro', }, }); ``` Standard traits [#standard-traits] These keys have their own fields on the profile, so they display correctly in the dashboard: * `firstName` * `lastName` * `email` (must be a valid email address) * `avatar` (must be a URL) Put everything else, like company or plan, inside `properties`. Best practices [#best-practices] 1. **Call on login**: Identify the user right after they log in. 2. **Call on update**: If a user changes their profile, call `identify` again with the new details. 3. **Unique IDs**: Use a stable ID from your database (like a UUID), not an email address or username that might change. --- ## Install Pieowl URL: https://pieowl.com/docs/get-started/install-pieowl The fastest way to start is the script tag. It works on any website. Quick start [#quick-start] Add this tag to the `` of your site. Replace `YOUR_CLIENT_ID` with the client ID from your site settings. ```html title="index.html" ``` That's it. Pieowl now tracks: * Page views * Visit duration * Referrers * Device and browser * Location (country and city) What gets collected [#what-gets-collected] You choose what the script collects by itself in **Settings → Site → What Pieowl counts**: * **Page views**: every page someone opens. On by default. * **Clicks on links to other sites**: which outside links people click. On by default. * **Clicks you marked in your code**: elements with `data-track="…"`. On by default. * **Address changes after #**: for one-page sites that change only the part after `#`. Off by default. The script loads these settings on every page. A change reaches your site within about a minute, with no code to edit. To send your own events, see [Track events](/docs/get-started/track-events). Using a framework? [#using-a-framework] Pick the SDK for your stack. It gives you typed methods and a setup that fits the framework. See all options [#see-all-options] Read the [SDKs overview](/docs/sdks) for every framework and language. --- ## Track Events URL: https://pieowl.com/docs/get-started/track-events Events are the core of Pieowl. An event records one action a user takes, like clicking a button, sending a form or buying something. The examples below use `pieowl`, the SDK instance you created during [install](/docs/get-started/install-pieowl). With the script tag, use `window.pieowl` instead. It works once `pieowl.js` has loaded; to call it earlier, use the `window.pieowl('track', ...)` form and add the [queue line](/docs/sdks/script#calling-pieowl-before-the-script-loads) before the script tag. Tracking an event [#tracking-an-event] Call `track` with an event name. ```javascript pieowl.track('button_clicked'); ``` With the script tag: ```javascript window.pieowl('track', 'button_clicked'); ``` Adding properties [#adding-properties] Pass a properties object to add context to the event. ```javascript pieowl.track('signup_button_clicked', { location: 'header', color: 'blue', variant: 'primary' }); ``` Common property types [#common-property-types] * **Strings**: Text values like names, categories or IDs. * **Numbers**: Numeric values like price, quantity or score. * **Booleans**: True or false values. Using data attributes [#using-data-attributes] You can track clicks without writing JavaScript. Add a `data-track` attribute to a button or link. This needs **Clicks you marked in your code** to be on in Settings → Site (it is by default). ```html ``` When a user clicks this button, Pieowl tracks a `signup_clicked` event with the property `location: 'header'`. Every other `data-*` attribute on the element becomes a property, with its name in camelCase (`data-plan-name` becomes `planName`). --- ## How it works URL: https://pieowl.com/docs/how-it-works Device ID [#device-id] A **device ID** is a unique identifier generated for each device/browser combination. It's calculated using a hash function that combines: * **User Agent** (browser/client information) * **IP Address** * **Origin** (site ID) * **Salt** (a rotating secret key) ```typescript export function generateDeviceId({ salt, ua, ip, origin, }: GenerateDeviceIdOptions) { return createHash(`${ua}:${ip}:${origin}:${salt}`, 16); } ``` Salt Rotation [#salt-rotation] The salt used for device ID generation rotates **daily at midnight** (00:00 server time). This means: * Device IDs remain consistent throughout a single day * Device IDs reset each day for privacy purposes * The system maintains both the current and previous day's salt to handle events that may arrive slightly after midnight ```typescript // Salt rotation happens daily at midnight (pattern: '0 0 * * *') ``` When the salt rotates, all device IDs change. The same visitor can't be linked from one day to the next, while sessions within a day still hold together. No cookies, no stored IPs [#no-cookies-no-stored-ips] The device ID is computed on the server, so the tracker never sets a cookie. The IP address is only used for this hash and for the location lookup (country and city). It is not stored with your events. The tracker stores nothing on the visitor's device. See [Do you need a cookie banner?](/docs/consent-and-cookie-banners). Session ID [#session-id] A **session** represents a continuous period of user activity. Sessions are used to group related events together and understand user behavior patterns. Session Duration [#session-duration] Sessions have a **30-minute timeout**. If no events are received for 30 minutes, the session automatically ends. Each new event resets this 30-minute timer. ```typescript export const SESSION_TIMEOUT = 1000 * 60 * 30; // 30 minutes ``` Session Creation Rules [#session-creation-rules] Sessions are **only created for client events**, not server events. This means: * Events sent from browsers, mobile apps, or client-side SDKs will create sessions * Events sent from backend servers, scripts, or server-side SDKs will **not** create sessions * If you only track events from your backend, no sessions will be created Additionally, sessions are **not created for events older than 15 minutes**. This prevents historical data imports from creating artificial sessions. ```typescript // Sessions are not created if: // 1. The event is from a server (uaInfo.isServer === true) // 2. The timestamp is from the past (isTimestampFromThePast === true) if (uaInfo.isServer || isTimestampFromThePast) { // Event is attached to existing session or no session } ``` Profile ID [#profile-id] A **profile ID** is a persistent identifier for a user across multiple devices and sessions. It allows you to track the same user across different browsers, devices, and time periods. Profile ID Assignment [#profile-id-assignment] If a `profileId` is provided when tracking an event, it will be used to identify the user. However, **if no `profileId` is provided, it defaults to the `deviceId`**. This means: * Anonymous users (without a profile ID) are tracked by their device ID * Once you identify a user (by providing a profile ID), all their events will be associated with that profile * The same user can be tracked across multiple devices by using the same profile ID ```typescript // If no profileId is provided, it defaults to deviceId if (!payload.profileId && payload.deviceId) { payload.profileId = payload.deviceId; } ``` Client Events vs Server Events [#client-events-vs-server-events] Pieowl distinguishes between **client events** and **server events** based on the User-Agent header. Client Events [#client-events] Client events are sent from: * Web browsers (Chrome, Firefox, Safari, etc.) * Mobile apps using client-side SDKs * Any client that sends a browser-like User-Agent Client events: * Create sessions * Generate device IDs * Support full [session tracking](/features/session-tracking) Server Events [#server-events] Server events are detected when the User-Agent matches server patterns, such as: * `Go-http-client/1.0` * `node-fetch/1.0` * Other single-name/version patterns (e.g., `LibraryName/1.0`) Server events: * Do **not** create sessions * Are attached to existing sessions if available * Are useful for backend tracking without session management ```typescript // Server events are detected by patterns like "Go-http-client/1.0" function isServer(res: UAParser.IResult) { if (SINGLE_NAME_VERSION_REGEX.test(res.ua)) { return true; } // ... additional checks } ``` The distinction is made in the event processing pipeline: ```typescript const uaInfo = parseUserAgent(userAgent, properties); // Only client events create sessions if (uaInfo.isServer || isTimestampFromThePast) { // Server events or old events don't create new sessions } ``` Timestamps [#timestamps] Events can include custom timestamps to track when events actually occurred, rather than when they were received by the server. Setting Custom Timestamps [#setting-custom-timestamps] You can provide a custom timestamp using the `__timestamp` property in your event properties: ```javascript track('page_view', { __timestamp: '2024-01-15T10:30:00Z' }); ``` Timestamp Validation [#timestamp-validation] The system validates timestamps to prevent abuse and ensure data quality: 1. **Future timestamps**: If a timestamp is more than **1 minute in the future**, the server timestamp is used instead 2. **Past timestamps**: If a timestamp is older than **15 minutes**, it's marked as `isTimestampFromThePast: true` ```typescript // Timestamp validation logic const ONE_MINUTE_MS = 60 * 1000; const FIFTEEN_MINUTES_MS = 15 * ONE_MINUTE_MS; // Future check: more than 1 minute ahead if (clientTimestampNumber > safeTimestamp + ONE_MINUTE_MS) { return { timestamp: safeTimestamp, isTimestampFromThePast: false }; } // Past check: older than 15 minutes const isTimestampFromThePast = clientTimestampNumber < safeTimestamp - FIFTEEN_MINUTES_MS; ``` Timestamp Impact on Sessions [#timestamp-impact-on-sessions] **Important**: Events with timestamps older than 15 minutes (`isTimestampFromThePast: true`) will **not create new sessions**. This prevents historical data imports from creating artificial sessions in your analytics. ```typescript // Events from the past don't create sessions if (uaInfo.isServer || isTimestampFromThePast) { // Attach to existing session or track without session } ``` This ensures that: * Real-time tracking creates proper sessions * Historical data imports don't interfere with session analytics * Backdated events are still tracked but don't affect session metrics --- ## MCP Server URL: https://pieowl.com/docs/mcp Pieowl runs an [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server. It lets AI assistants like Claude, Cursor and Windsurf query your analytics data right in the conversation. This is how you ask questions about your data: there is no chat inside Pieowl. The server is hosted, so there is nothing to install, and it shows up in your client as **Pieowl**. Endpoint [#endpoint] ``` https://api.pieowl.com/mcp ``` Authentication [#authentication] Pass the token in an `Authorization` header: ``` Authorization: Bearer YOUR_TOKEN ``` If your client can't set headers, pass it as a query parameter instead: ``` https://api.pieowl.com/mcp?token=YOUR_TOKEN ``` Both work. Prefer the header: query strings end up in browser history, shell history and proxy logs, so a token in the URL is easier to leak by accident. Token format [#token-format] The token is your client ID and client secret, joined by a colon and **base64-encoded**: ``` base64(clientId:clientSecret) ``` From the dashboard [#from-the-dashboard] The easiest way to get your MCP token is from the dashboard, no terminal needed: * **AI assistants** — in **Settings → Developers → AI assistants**, pick your client (Claude Desktop, Claude Code, Cursor, Windsurf, VS Code or Raycast) and click **Create token**. Pieowl makes a read-data key for the site and puts the token straight into a ready-made config. Copy it right away: it is shown only once. * **New key** — when you create a read-data key in **Settings → Developers → Keys**, the success screen shows a **Token for AI assistants** field ready to copy. Use **Download** to save your key ID, secret and token as `credentials.txt`. From the terminal [#from-the-terminal] ```bash echo -n "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" | base64 ``` Then send it with every request: ``` Authorization: Bearer ``` Or, for clients that can't set headers, add it to the MCP URL: ``` https://api.pieowl.com/mcp?token= ``` Required client type [#required-client-type] Only `read` and `root` clients can use MCP. Write-only clients are rejected. | Client type | Access | | ----------- | ---------------------------------------- | | `read` | One site (the one the client belongs to) | | `root` | Any site in your team | Use a `read` client to limit the AI assistant to one site. Use a `root` client if you need access across sites or want to list all sites. Create keys in **Settings → Developers** in your dashboard. See the [Authentication guide](/docs/api/authentication) for details. Connecting to Claude Desktop [#connecting-to-claude-desktop] Add this to your `claude_desktop_config.json`: ```json { "mcpServers": { "pieowl": { "type": "streamable-http", "url": "https://api.pieowl.com/mcp", "headers": { "Authorization": "Bearer YOUR_TOKEN" } } } } ``` If your client doesn't read `headers`, drop that block and put the token in the URL instead: `"url": "https://api.pieowl.com/mcp?token=YOUR_TOKEN"`. Connecting with Claude Code CLI [#connecting-with-claude-code-cli] Use the `--header` flag to pass the token as `Authorization: Bearer`: ```bash claude mcp add --transport http pieowl https://api.pieowl.com/mcp \ --header "Authorization: Bearer YOUR_TOKEN" ``` Or with the token in the URL: ```bash claude mcp add --transport http pieowl "https://api.pieowl.com/mcp?token=YOUR_TOKEN" ``` Connecting to Cursor [#connecting-to-cursor] Add this to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (one project): ```json { "mcpServers": { "pieowl": { "url": "https://api.pieowl.com/mcp", "transport": "streamable-http", "headers": { "Authorization": "Bearer YOUR_TOKEN" } } } } ``` For other clients, copy the config from **Settings → Developers → AI assistants** in your dashboard. Available tools [#available-tools] Site access [#site-access] | Tool | Description | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `list_projects` | List all sites your credentials can access, with their site IDs (`projectId`). Root clients see every site in the team; read clients see only their own. | | `get_dashboard_urls` | Get dashboard links for the current site — overview, events, profiles, sessions, and deep links to specific items. | Dashboards & reports [#dashboards--reports] | Tool | Description | | ----------------- | --------------------------------------------------------------------------- | | `list_dashboards` | List all dashboards for a site. | | `get_dashboard` | Get a dashboard with its full report settings and saved layouts. | | `list_reports` | List all reports in a dashboard with their chart types and tracked events. | | `get_report_data` | Run a saved report and return its data (time series, funnel, metric, etc.). | Dashboard management (root clients) [#dashboard-management-root-clients] | Tool | Description | | ------------------------ | ----------------------------------------------------------------------------- | | `create_dashboard` | Create a dashboard for a site. | | `update_dashboard` | Rename a dashboard. | | `delete_dashboard` | Delete an empty dashboard, or delete it with its reports using `forceDelete`. | | `create_report` | Add a saved chart to a dashboard. | | `update_report` | Replace a saved chart's settings. | | `delete_report` | Delete a saved chart. | | `duplicate_report` | Duplicate a saved chart in its dashboard. | | `update_report_layout` | Set a saved chart's grid position and size on the dashboard. | | `reset_dashboard_layout` | Remove all saved chart layouts from a dashboard. | Discovery [#discovery] | Tool | Description | | --------------------------- | -------------------------------------------------------------------------------------------------------- | | `list_event_names` | Get the 50 most common event names on the site. Call this first if you don't know the exact event names. | | `list_event_properties` | List all property keys tracked for an event (or across all events). | | `get_event_property_values` | Get all distinct values for one event property. | Events & sessions [#events--sessions] | Tool | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `query_events` | Query raw events with optional filters. Returns single records with path, device, country, referrer and custom properties. | | `query_sessions` | Query sessions with optional filters. Each session includes duration, entry and exit pages, bounce status and attribution data. | Profiles (users) [#profiles-users] | Tool | Description | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `find_profiles` | Search and filter user profiles by name, email, location, inactivity, session count, or whether they did a specific event. | | `get_profile` | Get one user profile with their most recent events. | | `get_profile_sessions` | Get all sessions for a user profile, newest first. | | `get_profile_metrics` | Get lifetime metrics for a user: sessions, page views, bounce rate, revenue and more. | Groups (B2B) [#groups-b2b] | Tool | Description | | ------------------ | -------------------------------------------------------------------------------------------- | | `list_group_types` | List all group types on the site (e.g. `company`, `team`). Call this before querying groups. | | `find_groups` | Search for groups by name, ID or type. | | `get_group` | Get one group with its properties and member profiles. | Aggregated metrics [#aggregated-metrics] | Tool | Description | | -------------------------- | ------------------------------------------------------------------------------------------------------- | | `get_analytics_overview` | Key metrics for a date range: visitors, page views, sessions, bounce rate and average session duration. | | `get_rolling_active_users` | Active users over time with a rolling window: daily (DAU), weekly (WAU) or monthly (MAU). | | `get_top_pages` | Most visited pages, ranked by page views. | | `get_page_performance` | Bounce rate, average session duration, sessions and page views per page. | | `get_page_conversions` | Pages ranked by how many visitors went on to convert after viewing them. | | `get_entry_exit_pages` | Most common entry pages (session start) or exit pages (session end). | | `get_top_referrers` | Top traffic sources by referrer name and type. | | `get_country_breakdown` | Visitor counts by country, region or city. | | `get_device_breakdown` | Visitor counts by device type, browser or OS. | User behavior [#user-behavior] | Tool | Description | | --------------------------------- | ------------------------------------------------------------------------------------------------------ | | `get_funnel` | Analyze a conversion funnel between 2 or more events — sign-up flows, checkout, onboarding. | | `get_retention_cohort` | Weekly retention cohort table that shows how well users stick around. | | `get_weekly_retention_series` | Week-over-week retention as a time series. | | `get_user_last_seen_distribution` | How recently users were last active, grouped into fresh, stale and churned. Useful for churn analysis. | | `get_user_flow` | User navigation flows as a Sankey diagram (before, after or between events). | Google Search Console [#google-search-console] These tools need Google Search Console connected for the site. Connect it in the dashboard on the **Google search** page. | Tool | Description | | ----------------------------- | -------------------------------------------------------------------------------- | | `gsc_get_overview` | Search performance over time: clicks, impressions, CTR and average position. | | `gsc_get_top_pages` | Top pages from Search Console, ranked by clicks. | | `gsc_get_page_details` | Search performance for one page, including all queries that bring traffic to it. | | `gsc_get_top_queries` | Top search queries, ranked by clicks. | | `gsc_get_query_opportunities` | Quick SEO wins: queries ranking 4–20 with meaningful search volume. | | `gsc_get_query_details` | Data for one search query, with every page that ranks for it. | | `gsc_get_cannibalization` | Queries where several of your pages compete with each other in Google. | Rate limiting [#rate-limiting] 60 requests per minute, counted per IP address. --- ## Revenue attribution URL: https://pieowl.com/docs/revenue-attribution [Revenue attribution](/features/revenue-attribution) shows which sources, campaigns, pages and countries make you money, not just which bring visits. You send each purchase to Pieowl, and Pieowl credits it to the visit where it happened. This page covers how to send purchases, how they are credited, and where to find revenue in the dashboard. For step-by-step checkout flows with Stripe and other payment providers, see [Revenue tracking](/docs/revenue-tracking). Send purchases [#send-purchases] **Settings → Site → Revenue** in your dashboard has everything in one place: your currency, ready code to copy for your site or your server, and the last payment Pieowl received, so you can see it works. Call `purchase()` when your site learns the payment went through. The SDKs send it as an event named `revenue`. ```js pieowl.purchase({ id: 'A-1001', amount: 49.99, currency: 'EUR', plan: 'pro' }); ``` * **`id`** is your order number. See [Order numbers](#order-numbers). * **`amount`** is in normal units, like `49.99`, not cents. It must be zero or more. * **`currency`** is an ISO 4217 code, like `EUR`. Leave it out and Pieowl uses your [site currency](#currency). * Add any other properties you want to break revenue down by, like `plan` or `product`. From your payment provider, no code [#from-your-payment-provider-no-code] Stripe, Paddle, Lemon Squeezy and Polar can send your payments to Pieowl by themselves. In Pieowl, open **Settings → Site → Revenue → Your payment provider** and pick yours. You get your own webhook address and the steps for that provider: | Provider | Where to add the webhook | Events | Secret | | ------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | | Stripe | **Workbench → Webhooks → Create an event destination**, payload **Snapshot** | `checkout.session.completed`, `checkout.session.async_payment_succeeded`, `invoice.paid`, `charge.refunded` | Copy the **Signing secret** (`whsec_…`) into Pieowl | | Paddle | **Events → Notifications → New destination**, type **URL** | `transaction.completed`, `adjustment.created`, `adjustment.updated` | Copy the destination's **Secret key** into Pieowl | | Lemon Squeezy | **Settings → Webhooks → +** | `order_created`, `order_refunded`, `subscription_created`, `subscription_payment_success`, `subscription_payment_refunded` | Paste the secret Pieowl shows into Lemon Squeezy | | Polar | **Settings → Webhooks → Add Endpoint**, format **Raw** | `order.paid`, `refund.created`, `refund.updated` | Copy the endpoint's secret (`whsec_…`) into Pieowl | From then on: * One-off payments, subscription payments and renewals are counted. Each event counts once, even when the provider sends it again. * Refunds are subtracted from the visit the order was paid in. * Test-mode payments show that the connection works, but they aren't counted. * The card shows when the provider last sent an event, and what to fix if something is wrong (for example, a wrong secret). **Who paid.** The Pieowl script adds the visitor to checkout links by itself: Stripe payment links and buy buttons, Lemon Squeezy and Polar checkout links, and Paddle.js buttons. If your own code creates the checkout, pass the visitor yourself: * Stripe: `client_reference_id: pieowl.getDeviceId()` (or `metadata.pieowl_device_id`) * Paddle: `customData: { pieowl_device_id: pieowl.getDeviceId() }` * Lemon Squeezy: `checkout_data.custom.pieowl_device_id` * Polar: `metadata.pieowl_device_id` Without it the payment counts in your total, without a source. If you also send `purchase()` from your site for a Stripe payment, use the PaymentIntent id (`pi_…`) as `id`, so the payment counts once. From the browser [#from-the-browser] Call `purchase()` with the [Web SDK](/docs/sdks/web) or the [script tag](/docs/sdks/script). The purchase is credited to the visit that is going on in that browser. ```js pieowl.purchase({ id: 'A-1001', amount: 49.99, currency: 'EUR', plan: 'pro' }); // Script tag window.pieowl('purchase', { id: 'A-1001', amount: 49.99, currency: 'EUR', plan: 'pro' }); ``` Payment in a modal or an embedded card form [#payment-in-a-modal-or-an-embedded-card-form] If people pay without leaving the page, there is no success page. Send the purchase where your code learns that the payment went through. For example, with Stripe's Payment Element in a modal, `confirmPayment` with `redirect: 'if_required'` returns the paid PaymentIntent for card payments: ```js const { paymentIntent } = await stripe.confirmPayment({ elements, redirect: 'if_required', }); if (paymentIntent?.status === 'succeeded') { window.pieowl('purchase', { id: paymentIntent.id, amount: paymentIntent.amount / 100, // Stripe sends cents currency: paymentIntent.currency.toUpperCase(), }); } ``` Some currencies, like JPY, have no decimals. Stripe sends `1500` for ¥1,500, so don't divide those by 100. If your payment provider gives you the amount with decimals, send that. The visitor is still on the same visit, so the purchase gets that visit's source, pages and country. Payment methods that redirect to a bank come back to your `return_url`: send the purchase there, as on a success page. Payment on another site [#payment-on-another-site] If the visitor pays on another site, like a hosted checkout page, the simplest way is your payment provider's webhook: [no code](#from-your-payment-provider-no-code) or [from your server](#from-your-server). Pieowl stores nothing in the browser to carry the purchase across the redirect. To send it from the browser instead, call `purchase()` on your success page. If the page doesn't know the order, look it up on your server, for example with the checkout session ID your payment provider adds to the success address: ```js // Success address: /thanks?session_id={CHECKOUT_SESSION_ID} const sessionId = new URLSearchParams(location.search).get('session_id'); const order = await fetch(`/api/orders/${sessionId}`).then((res) => res.json()); pieowl.purchase({ id: order.id, amount: order.total, currency: order.currency }); ``` Pieowl rejects purchases and refunds sent from the browser by default, because anyone can send browser events. Turn on **Accept revenue sent from the browser** in Settings → Site → Revenue to accept them. Sending from your server is more reliable. From your server [#from-your-server] Send the purchase when your server learns the payment went through, for example in your payment provider's webhook. Use the [JavaScript SDK](/docs/sdks/javascript) with your client secret, or the [HTTP API](/docs/api/track) from any language. ```js import { Pieowl } from '@pieowl/sdk'; const pieowl = new Pieowl({ clientId: 'YOUR_CLIENT_ID', clientSecret: 'YOUR_CLIENT_SECRET', }); // When the payment went through await pieowl.purchase({ id: order.number, amount: order.total, // 49.99 currency: order.currency, // 'EUR' deviceId: order.deviceId, // from pieowl.getDeviceId() in the browser plan: 'pro', }); ``` ```bash curl -X POST 'https://api.pieowl.com/track' \ -H 'Content-Type: application/json' \ -H 'pieowl-client-id: YOUR_CLIENT_ID' \ -H 'pieowl-client-secret: YOUR_CLIENT_SECRET' \ -d '{ "type": "track", "payload": { "name": "revenue", "properties": { "__amount": 49.99, "__currency": "EUR", "__orderId": "A-1001", "__deviceId": "DEVICE_ID", "plan": "pro" } } }' ``` A server has no visit of its own, so Pieowl needs to know who paid. Send one of these, or both (the `deviceId` is tried first): * **`deviceId`**, when you can. In the browser, read it with `pieowl.getDeviceId()` before checkout and keep it with the order, for example in your payment provider's metadata. Send it as `deviceId` with the SDK, or as the `__deviceId` property over HTTP. * **`profileId`**. In the browser, call [`identify()`](/docs/get-started/identify-users) with the visitor's `profileId` before they pay, for example when they log in or start checkout. Send the same `profileId` with the purchase. Pieowl credits the purchase to that visitor's current visit. If the visit has already ended (a visit ends after 30 minutes without activity), for example because the payment webhook came hours later, the purchase goes to the visitor's most recent visit that started up to 7 days before the purchase. A purchase without a `deviceId` or `profileId`, or with no visit in those 7 days, is still saved as a `revenue` event, but it isn't credited to a visit, so it doesn't show up in the revenue of sources, pages or countries. Order numbers [#order-numbers] Send your order number as `id`. Pieowl counts each order once, so the same order sent twice adds nothing the second time. This happens more than you'd think: a visitor reloads the thank-you page, or your payment provider retries a webhook. Use an ID that is unique on your site, like your order number or your payment provider's payment ID. For subscription renewals, use the ID of each invoice, not of the subscription. Refunds [#refunds] Call `refund()` with the order number when you give money back. Pieowl subtracts the order from your revenue. ```js // The whole order pieowl.refund({ id: 'A-1001' }); // Part of it pieowl.refund({ id: 'A-1001', amount: 10, currency: 'EUR' }); // Script tag window.pieowl('refund', { id: 'A-1001' }); ``` * Without `amount`, Pieowl refunds what was paid for that order. The order must have been sent with `purchase()`. * Each refund counts once. For several partial refunds of one order, give each a `refundId` (refunds without one are told apart by amount). * A refund lowers the revenue of the visit the order was paid in, so its source, pages and country lose it too. Over HTTP, send the same `revenue` event with `"__refund": true` and the `__orderId`. `__amount` and `__currency` are optional. Currency [#currency] Pieowl shows all revenue in your site currency. Set it in Settings → Site → **Revenue** → **Your currency**. It's USD until you change it. * You can send purchases in any of these currencies: USD, EUR, GBP, PLN, CHF, CZK, DKK, HUF, NOK, RON, SEK, ISK, TRY, AUD, BRL, CAD, CNY, HKD, IDR, ILS, INR, JPY, KRW, MXN, MYR, NZD, PHP, SGD, THB and ZAR. Any other code is rejected with an error, so a wrong currency never gets counted. * A purchase in another currency is converted at the European Central Bank reference rate of the payment's day. On weekends and holidays, Pieowl uses the last published rate. * Pieowl keeps the original amount and currency of each purchase. * If you change your site currency, past purchases are recalculated in the background. It takes a few minutes. The older revenue() call [#the-older-revenue-call] Before `purchase()`, the SDKs had `pieowl.revenue(4900)`. It still works. It takes a whole number in the smallest unit of your site currency, like cents. It has no order number, so a resent purchase counts twice. Use `purchase()` for new code. How a purchase is credited [#how-a-purchase-is-credited] Every purchase is credited to **the visit where it happened**. That visit's details become the purchase's details: * Source and referrer * UTM campaign, source and medium * Entry page (the first page of the visit) * Country * Device, browser and OS Other visits by the same visitor get nothing, even if they came first. Example [#example] A visitor comes to your site twice: | Visit | Came from | Entry page | Bought | | -------- | ------------- | -------------------- | ------- | | Monday | Google search | `/blog/pricing-tips` | Nothing | | Thursday | Instagram | `/products` | $49 | The $49 is credited to **Instagram** and to the entry page **`/products`**. Google and `/blog/pricing-tips` get no revenue. Both visits still count as visits for their own source and page. If the visitor had come back on Thursday by typing your address, the $49 would go to **Direct**. Where to see revenue [#where-to-see-revenue] In the overview, switch the metric to **Revenue** in any of these places: * **Sources**: referrers, UTM sources, mediums and campaigns * **Entry pages**: revenue by the page each visit started on. Each purchase belongs to one entry page, so the shares add up to 100%. * **Viewed pages**: revenue of the visits that viewed each page. A visit that viewed three pages counts for all three, so the shares overlap and add up to more than 100%. Use it to see which pages are part of buying visits. * **Countries and the map** * **Devices, browsers and OS** * **Events**: the revenue of visits that had each event. For example, revenue of visits with `signup_clicked`. Revenue also shows up in: * **People**: each person's lifetime revenue, from all of their visits * **Groups** and **cohorts**: revenue for each group and each cohort Metrics [#metrics] | Metric | What it means | | ----------------------- | -------------------------------------------------------------- | | **Revenue** | The sum of purchases credited to visits in the selected period | | **Paying visits** | Visits with at least one purchase | | **Conversion** | Paying visits divided by all visits, as a percentage | | **Revenue per visitor** | Revenue divided by unique visitors | Revenue is shown in your [site currency](#currency). Filters [#filters] Add the **Bought** filter to the overview: * **Bought: yes** shows only visits with a purchase. Use it to see where buyers come from and what they view. * **Bought: no** shows only visits without a purchase. All other filters work with revenue too. Filter by a UTM campaign and every revenue number on the page is for that campaign. Reports [#reports] In [reports](/features/data-visualization), revenue works with any event: * **Revenue (visits with this event)**: pick it as the metric for any event to get the revenue of the visits that had that event. * **Breakdowns by visit**: break revenue down by entry page, referrer or UTM fields. * **Funnels and conversion**: see the revenue of the visits that completed. * **Formulas**: combine revenue with other series. Example: revenue per visitor over time [#example-revenue-per-visitor-over-time] 1. Series **A**: pick `screen_view` with the metric **Revenue (visits with this event)**. 2. Series **B**: pick `screen_view` with the metric **Unique users**. 3. Click **Add formula** and enter `A / B`. The formula line is revenue per visitor for each day, week or month. Privacy [#privacy] You don't need personal data for revenue attribution. Send the order number and the amount and, from a server, a `deviceId` or `profileId`. The `profileId` can be your internal user ID, it doesn't need to be an email address. Don't put names, emails or card details in the purchase properties. Pieowl sets no cookies, stores nothing on the visitor's device and doesn't store IP addresses. See [How it works](/docs/how-it-works). Revenue by source is conversion measurement. In some countries that needs your visitors' consent, and France's exemption for audience measurement doesn't cover it. Sending purchases from your server or with webhooks keeps payment data out of the browser. See [Do you need a cookie banner?](/docs/consent-and-cookie-banners). FAQ [#faq] The currency the customer paid in. Pieowl converts it to your site currency at the European Central Bank rate of the payment's day, and keeps the original. See [Currency](#currency). Yes, from the moment you connect it: see [From your payment provider, no code](#from-your-payment-provider-no-code). Stripe, Paddle, Lemon Squeezy and Polar then send every payment, renewal and refund by themselves. Payments made before the connection aren't imported. Call `refund()` with the order number. Pass `amount` for a partial refund. See [Refunds](#refunds). Yes, send a purchase for each renewal, with the invoice ID as `id`. A renewal goes to the customer's most recent visit if it started up to 7 days before the renewal. Otherwise there is no visit to credit it to: it's still saved as a `revenue` event, but it doesn't add to the revenue of sources or pages. Pieowl credits each purchase to the visit where it happened. It doesn't split a purchase across earlier visits. Without cookies, Pieowl can't reliably link an anonymous visitor's visits on different days, so crediting the visit where the purchase happened gives numbers you can trust. Check that you send the visitor's `deviceId` (from `pieowl.getDeviceId()`) or a `profileId` that the browser also passed to `identify()`, and that the visitor had a visit that started up to 7 days before the purchase. --- ## Revenue tracking URL: https://pieowl.com/docs/revenue-tracking [Revenue attribution](/features/revenue-attribution) shows you which sources, campaigns, pages and countries bring in money. This page walks through checkout flows step by step. For how purchases are credited to visits and where to see revenue in the dashboard, see [Revenue attribution](/docs/revenue-attribution). First, a few basics about how Pieowl and your payment provider work, and how to link a payment to a visitor. Payment providers [#payment-providers] Usually, you create your checkout from your backend, which then returns a payment link that your visitor will be redirected to. When creating the checkout link, you usually add additional fields such as metadata, customer information, or order details. We'll add the device ID information in this metadata field to be able to link your payment to a visitor. Pieowl [#pieowl] Pieowl doesn't use cookies. It identifies visitors with a `device_id`. To link a payment to a visitor, you need to capture their `device_id` before they complete checkout. This `device_id` will be stored in your payment provider's metadata, and when the payment webhook arrives, you'll use it to associate the revenue with the correct visitor. Some typical flows [#some-typical-flows] * [Revenue tracking from your backend (not identified)](#revenue-tracking-from-your-backend-webhook) * [Revenue tracking from your backend (identified)](#revenue-tracking-from-your-backend-webhook---identified-users) * [Revenue tracking from your frontend](#revenue-tracking-from-your-frontend) * [Revenue tracking without linking it to a identity or device](#revenue-tracking-without-linking-it-to-an-identity-or-device) In the backend examples, `pieowl` is a server-side instance created with your client secret (see the [JavaScript SDK](/docs/sdks/javascript)). In the browser examples, it's your [Web SDK](/docs/sdks/web) instance or `window.pieowl`. Revenue tracking from your backend (webhook) [#revenue-tracking-from-your-backend-webhook] This is the most common flow and most secure one. Your backend receives webhooks from your payment provider, and here is the best opportunity to do revenue tracking. Pieowl credits the purchase to the visit of that `deviceId`: the visit going on when the webhook arrives or, if it has already ended, the visitor's most recent visit that started up to 7 days before the purchase. If there is no such visit, the purchase is still saved as a `revenue` event, but it doesn't show up in the revenue of sources, pages or countries. When you create the checkout, first call `pieowl.getDeviceId()`. It returns the visitor's current `deviceId`. Pass it to your checkout endpoint. ```javascript fetch('https://domain.com/api/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ deviceId: pieowl.getDeviceId(), // keep the visitor's deviceId with the payment // ... other checkout data }), }) .then(response => response.json()) .then(data => { // Handle checkout response, e.g., redirect to payment link window.location.href = data.paymentUrl; }) ``` ```javascript import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export async function POST(req: Request) { const { deviceId, amount, currency } = await req.json(); const session = await stripe.checkout.sessions.create({ payment_method_types: ['card'], line_items: [ { price_data: { currency: currency, product_data: { name: 'Product Name' }, unit_amount: amount * 100, // Convert to cents }, quantity: 1, }, ], mode: 'payment', metadata: { deviceId: deviceId, // keep the deviceId with the payment }, success_url: 'https://domain.com/success', cancel_url: 'https://domain.com/cancel', }); return Response.json({ paymentUrl: session.url, }); } ``` ```javascript export async function POST(req: Request) { const event = await req.json(); // Stripe sends events with type and data.object structure if (event.type === 'checkout.session.completed') { const session = event.data.object; pieowl.purchase({ id: session.id, // a resent webhook counts once amount: session.amount_total / 100, // Stripe sends cents currency: session.currency.toUpperCase(), deviceId: session.metadata.deviceId, // credited to this visitor's visit }); } return Response.json({ received: true }); } ``` Stripe sends amounts in the smallest currency unit. For currencies without decimals, like JPY, send `session.amount_total` as it is. If your payment provider gives you the amount with decimals, send that. *** Revenue tracking from your backend (webhook) - Identified users [#revenue-tracking-from-your-backend-webhook---identified-users] If your visitors are identified (meaning you have called `identify` with a `profileId`), this process gets a bit easier. You don't need to pass the `deviceId` when creating your checkout, and you only need to provide the `profileId` (in backend) to the `purchase()` call. Pieowl credits the purchase to the visitor's current visit, so its source, entry page and country get the revenue. If the visit has already ended (a visit ends after 30 minutes without activity), the purchase goes to the visitor's most recent visit that started up to 7 days before it. If you also have the visitor's `deviceId`, send it too: Pieowl tries it first. See [Revenue attribution](/docs/revenue-attribution#from-your-server). When a visitor logs in or is identified, call `pieowl.identify()` with their unique `profileId`. ```javascript pieowl.identify({ profileId: 'user-123', // Unique identifier for this user email: 'user@example.com', firstName: 'John', lastName: 'Doe', }); ``` Since the visitor is already identified, you don't need to fetch or pass the `deviceId`. Just send the checkout data. ```javascript fetch('https://domain.com/api/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ // No deviceId needed - user is already identified // ... other checkout data }), }) .then(response => response.json()) .then(data => { // Handle checkout response, e.g., redirect to payment link window.location.href = data.paymentUrl; }) ``` Since the user is authenticated, you can get their `profileId` from the session and store it in metadata for easy retrieval in the webhook. ```javascript import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export async function POST(req: Request) { const { amount, currency } = await req.json(); // Get profileId from authenticated session const profileId = req.session.userId; // or however you get the user ID const session = await stripe.checkout.sessions.create({ payment_method_types: ['card'], line_items: [ { price_data: { currency: currency, product_data: { name: 'Product Name' }, unit_amount: amount * 100, // Convert to cents }, quantity: 1, }, ], mode: 'payment', metadata: { profileId: profileId, // Store profileId instead of deviceId }, success_url: 'https://domain.com/success', cancel_url: 'https://domain.com/cancel', }); return Response.json({ paymentUrl: session.url, }); } ``` In the webhook handler, retrieve the `profileId` from the session metadata. ```javascript export async function POST(req: Request) { const event = await req.json(); // Stripe sends events with type and data.object structure if (event.type === 'checkout.session.completed') { const session = event.data.object; pieowl.purchase({ id: session.id, amount: session.amount_total / 100, // Stripe sends cents currency: session.currency.toUpperCase(), profileId: session.metadata.profileId, // Use profileId instead of deviceId }); } return Response.json({ received: true }); } ``` *** Revenue tracking from your frontend [#revenue-tracking-from-your-frontend] This flow sends the purchase from your frontend. Call `pieowl.purchase()` on your success page, or where your code learns that the payment went through. If your success page doesn't know the order, look it up on your server, for example with the checkout session ID your payment provider adds to the success address. Pieowl stores nothing in the browser between checkout and the success page. Create the checkout with a success address that tells your page which order was paid. With Stripe Checkout, add `session_id={CHECKOUT_SESSION_ID}` to `success_url`. Look up the paid order and send it with `purchase()`. The same order sent twice counts once. ```javascript const sessionId = new URLSearchParams(location.search).get('session_id'); const order = await fetch(`/api/orders/${sessionId}`).then((res) => res.json()); pieowl.purchase({ id: order.id, amount: order.total, // in normal units, like 49.99 currency: order.currency, }); ``` Pros: [#pros] * Quick way to get going * No webhook required Cons: [#cons] * Missed if the visitor closes the tab before the success page loads * Less "secure": anyone could post revenue data * Needs **Accept revenue sent from the browser** turned on in Settings → Site → Revenue * Revenue sent from the browser is conversion measurement. In some countries it needs your visitors' consent. See [Do you need a cookie banner?](/docs/consent-and-cookie-banners) *** Revenue tracking without linking it to an identity or device [#revenue-tracking-without-linking-it-to-an-identity-or-device] If you simply want to track revenue totals without linking payments to specific visitors or devices, you can call `pieowl.purchase()` directly from your backend without providing a `deviceId` or `profileId`. This is the simplest approach and works well when you only need aggregate revenue data. Call `pieowl.purchase()` with the order number and amount. No `deviceId` or `profileId` is needed. ```javascript export async function POST(req: Request) { const event = await req.json(); // Stripe sends events with type and data.object structure if (event.type === 'checkout.session.completed') { const session = event.data.object; // Simple revenue tracking without linking to a visitor pieowl.purchase({ id: session.id, amount: session.amount_total / 100, currency: session.currency.toUpperCase(), }); } return Response.json({ received: true }); } ``` Pros: [#pros-1] * Simplest implementation * No need to capture or pass device IDs * Works well for aggregate revenue tracking Cons: [#cons-1] * **You can't dive deeper into where this revenue came from.** For instance, you won't be able to see which source generates the best revenue, which campaigns are most profitable, or which visitors are your highest-value customers. * Revenue events won't be linked to specific user journeys or sessions * The overview's revenue only counts purchases credited to a visit, so these purchases don't show up there. You can still chart the `revenue` event in reports. Available methods [#available-methods] Purchase [#purchase] Sends a paid order. For security, Pieowl rejects purchases that aren't sent with your client secret. To send them from the browser, turn on **Accept revenue sent from the browser** in Settings → Site → Revenue. * `id`: your order number. The same order sent twice counts once. * `amount`: in normal units, like `49.99`, not cents. * `currency`: an ISO 4217 code, like `EUR`. Optional, your site currency by default. See [Currency](/docs/revenue-attribution#currency). * `deviceId`: from a server, the visitor's device ID from `pieowl.getDeviceId()`. * Any other properties, like `plan`. ```javascript pieowl.purchase({ id: string, amount: number, currency?: string, deviceId?: string, ...properties }): Promise ``` Refund [#refund] Subtracts an order from your revenue. Without `amount`, the whole order is refunded. Pass `amount` (and `currency`) for a partial refund, and a `refundId` to count several partial refunds of one order. The visit the order was paid in loses the amount. Browser refunds need the same setting as purchases. ```javascript pieowl.refund({ id: string, amount?: number, currency?: string, deviceId?: string }): Promise ``` Revenue (older) [#revenue-older] The older way to send a purchase. The amount is a whole number in the smallest unit of your site currency, like cents (`4900` for 49.00). It has no order number, so a resent purchase counts twice. Use `purchase()` for new code. ```javascript pieowl.revenue(amount: number, properties?: Record): Promise ``` Fetch your current users device id [#fetch-your-current-users-device-id] ```javascript pieowl.getDeviceId(): string ``` --- ## SDKs Overview URL: https://pieowl.com/docs/sdks Pieowl has SDKs for the web, Node.js and React Native. For every other language, you send events with a plain HTTP request to the [Track API](/docs/api/track). Quick start [#quick-start] For most web projects, start with one of these: * **[Script Tag](/docs/sdks/script)**: The fastest way to start, no build step * **[Web SDK](/docs/sdks/web)**: For TypeScript and more control * **[Next.js](/docs/sdks/nextjs)**: The script tag via `next/script` | [Setup guide](/guides/nextjs-analytics) Web and browser [#web-and-browser] Simple integration [#simple-integration] * **[Script Tag](/docs/sdks/script)**: Add analytics with one ` ``` What the script collects by itself (page views, outside link clicks, `data-track` clicks) is set in **Settings → Site → What Pieowl counts**, not in code. For options that need code, see [Options in code](/docs/sdks/script#options-in-code). Tracking events [#tracking-events] Call `window.pieowl`, or add `data-track` attributes. `data-track` needs **Clicks you marked in your code** to be on in Settings → Site (it is by default). ```astro ``` ```astro ``` Identifying users [#identifying-users] When you know the user on the server, render the call into the page: ```astro --- const user = Astro.locals.user; --- {user && ( )} ``` This call runs before `pieowl.js` has loaded, so add the [queue line](/docs/sdks/script#calling-pieowl-before-the-script-loads) before the script tag in your layout. `setGlobalProperties`, `increment`, `decrement` and `clear` work the same way. See the [Script Tag](/docs/sdks/script#usage) docs. Track AI crawlers and bots [#track-ai-crawlers-and-bots] On server-rendered sites (an Astro adapter with `output: 'server'` or on-demand pages), report the bots the script can't see: ```bash npm install @pieowl/bots ``` ```ts title="src/middleware.ts" import { trackBotRequest } from '@pieowl/bots'; import { defineMiddleware } from 'astro:middleware'; export const onRequest = defineMiddleware((context, next) => { trackBotRequest(context.request, { clientId: import.meta.env.PIEOWL_CLIENT_ID, clientSecret: import.meta.env.PIEOWL_CLIENT_SECRET, }); return next(); }); ``` Static sites have no server to see the bots. Read more in [AI crawlers and bots](/docs/ai-bots). Avoid ad blockers [#avoid-ad-blockers] On a server-rendered site, serve the script and send events through your own domain with an endpoint: ```ts title="src/pages/api/pieowl/[...path].ts" import { createRouteHandler } from '@pieowl/bots/route-handler'; const handler = createRouteHandler(); export const prerender = false; export const ALL = ({ request }: { request: Request }) => handler(request); ``` Then switch to the [`init` form](/docs/sdks/script#options-in-code), set `apiUrl: '/api/pieowl'` in `init`, and load the script from `/api/pieowl/pieowl.js`. See [Avoid ad blockers](/docs/adblockers). Server-side events [#server-side-events] Use the [JavaScript SDK](/docs/sdks/javascript) (`@pieowl/sdk`) with your client secret in endpoints and server-rendered pages. --- ## Express URL: https://pieowl.com/docs/sdks/express Looking for a step-by-step tutorial? Read the [Express analytics guide](/guides/express-analytics). Good to know [#good-to-know] An Express app needs up to three pieces. Use the ones you need: * **Pages and visitors**: the [HTML script](/docs/sdks/script) in the pages Express serves. * **AI crawlers and bots**: `@pieowl/bots`, which reports the bots the script can't see. * **Server-side events** (sign-ups, payments): `@pieowl/sdk`. Track pages [#track-pages] Add the script to your HTML template (EJS, Pug, Handlebars or static files) and replace `YOUR_CLIENT_ID` with your client ID. ```html title="views/layout.html" ``` For events, identifying users and all options, see the [Script Tag](/docs/sdks/script) docs. Track AI crawlers and bots [#track-ai-crawlers-and-bots] ```bash npm install @pieowl/bots ``` ```ts import { trackBots } from '@pieowl/bots/express'; app.use(trackBots({ clientId: process.env.PIEOWL_CLIENT_ID, clientSecret: process.env.PIEOWL_CLIENT_SECRET, })); ``` Requests from AI and search bots are reported after the response is sent, with the status code. Visitors cause no extra work. Behind a proxy or load balancer, set Express's [`trust proxy`](https://expressjs.com/en/guide/behind-proxies.html) so `req.ip` is the bot's IP. Read more in [AI crawlers and bots](/docs/ai-bots). Avoid ad blockers [#avoid-ad-blockers] Serve the script and send events through your own domain: ```ts import { createRouteHandler } from '@pieowl/bots/express'; app.use('/api/pieowl', createRouteHandler()); ``` Then switch to the [`init` form](/docs/sdks/script#options-in-code), set `apiUrl: '/api/pieowl'` in `init`, and load the script from `/api/pieowl/pieowl.js`. See [Avoid ad blockers](/docs/adblockers). Server-side events [#server-side-events] Use the [JavaScript SDK](/docs/sdks/javascript) with your client secret. Keep the secret on the server. ```bash npm install @pieowl/sdk ``` ```ts title="pieowl.ts" import { Pieowl } from '@pieowl/sdk'; export const pieowl = new Pieowl({ clientId: process.env.PIEOWL_CLIENT_ID, clientSecret: process.env.PIEOWL_CLIENT_SECRET, }); ``` ```ts import { pieowl } from './pieowl'; app.post('/sign-up', async (req, res) => { const user = await createUser(req.body); // Pass profileId on each call: one instance serves every visitor. pieowl.track('sign_up', { profileId: user.id, plan: req.body.plan }); res.json({ ok: true }); }); ``` Server events get their location from the request IP, which is your server's. To use the visitor's location, create an instance for the request and forward the IP and user agent: ```ts import { Pieowl } from '@pieowl/sdk'; function pieowlFor(req) { const pieowl = new Pieowl({ clientId: process.env.PIEOWL_CLIENT_ID, clientSecret: process.env.PIEOWL_CLIENT_SECRET, }); if (req.ip) pieowl.api.addHeader('pieowl-client-ip', req.ip); pieowl.api.addHeader('user-agent', req.get('user-agent') ?? ''); return pieowl; } ``` Working with groups [#working-with-groups] Groups let you track analytics at the account or company level. See the [Groups guide](/docs/get-started/groups) for the full walkthrough. ```ts app.post('/login', async (req, res) => { const user = await loginUser(req.body); const pieowl = pieowlFor(req); pieowl.identify({ profileId: user.id, email: user.email }); pieowl.upsertGroup({ id: user.organizationId, type: 'company', name: user.organizationName, properties: { plan: user.plan }, }); pieowl.setGroup(user.organizationId); res.json({ ok: true }); }); ``` For every method, see the [JavaScript SDK](/docs/sdks/javascript#usage). --- ## Java URL: https://pieowl.com/docs/sdks/java Pieowl doesn't ship a Java SDK. You send events from Java with a plain HTTP request to the [Track API](/docs/api/track). The example below uses `java.net.http.HttpClient`, which is built into Java 11 and later. It works the same in Spring Boot, Quarkus, Micronaut or plain Java. Before you start [#before-you-start] You need a client ID and a client secret. Find them in the dashboard under **Settings → Developers → Keys**. Server requests must include the secret, so keep it on your server and load it from an environment variable. Send an event [#send-an-event] Every request is a `POST` to `https://api.pieowl.com/track` with a JSON body of `{ "type": ..., "payload": ... }`. ```java title="Pieowl.java" import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; import java.util.concurrent.CompletableFuture; public final class Pieowl { private static final URI TRACK_URL = URI.create("https://api.pieowl.com/track"); private static final HttpClient CLIENT = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .build(); // json is the full body, for example {"type":"track","payload":{...}} public static CompletableFuture> send(String json) { HttpRequest request = HttpRequest.newBuilder(TRACK_URL) .timeout(Duration.ofSeconds(5)) .header("Content-Type", "application/json") .header("pieowl-client-id", System.getenv("PIEOWL_CLIENT_ID")) .header("pieowl-client-secret", System.getenv("PIEOWL_CLIENT_SECRET")) // Optional: forward the visitor's IP for location data // .header("pieowl-client-ip", visitorIp) .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); return CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString()); } } ``` `sendAsync` doesn't block your request thread. Call `.join()` on the result if you need to wait. The examples below write the JSON by hand to stay short. In a real app, build it with the JSON library you already use, like Jackson or Gson, so values are escaped correctly. Usage [#usage] Tracking events [#tracking-events] ```java Pieowl.send(""" {"type":"track","payload":{"name":"signup","profileId":"user_123","properties":{"plan":"pro"}}} """); ``` Identifying users [#identifying-users] ```java Pieowl.send(""" {"type":"identify","payload":{"profileId":"user_123","firstName":"Joe","lastName":"Doe","email":"joe@doe.com","properties":{"tier":"premium"}}} """); ``` Incrementing and decrementing properties [#incrementing-and-decrementing-properties] `value` is optional and defaults to 1. ```java Pieowl.send(""" {"type":"increment","payload":{"profileId":"user_123","property":"visits","value":1}} """); Pieowl.send(""" {"type":"decrement","payload":{"profileId":"user_123","property":"credits","value":1}} """); ``` Tracking revenue [#tracking-revenue] A purchase is a `revenue` event with these properties: * `__amount`: in normal units, like `49.99`, not cents. * `__currency`: an ISO 4217 code, like `EUR`. Optional, your site currency by default. * `__orderId`: your order number. The same order sent twice counts once. * `__deviceId`: the visitor's device ID, from `pieowl.getDeviceId()` in the browser. The purchase is credited to their visit. For a refund, send the same event with `__refund` set to true. Leave out `__amount` to refund the whole order. See [Revenue attribution](/docs/revenue-attribution). ```java Pieowl.send(""" {"type":"track","payload":{"name":"revenue","properties":{"__amount":49.99,"__currency":"EUR","__orderId":"A-1001","__deviceId":"DEVICE_ID"}}} """); // Refund the whole order Pieowl.send(""" {"type":"track","payload":{"name":"revenue","properties":{"__orderId":"A-1001","__refund":true}}} """); ``` The text blocks (`"""`) need Java 15 or later. On Java 11, use normal strings. Good to know [#good-to-know] * **Errors:** Pieowl answers with a `2xx` status when it accepts a request. Check `response.statusCode()` and log anything else, for example a bad client secret or an invalid payload. * **Kotlin:** The same code works from Kotlin on the JVM. See the [Kotlin page](/docs/sdks/kotlin) for an Android example. * **More event types:** Groups and the full request schema are in the [Track API reference](/docs/api/track). --- ## JavaScript (Node / Generic) URL: https://pieowl.com/docs/sdks/javascript `@pieowl/sdk` is the core SDK. Use it on the server, or in any JavaScript runtime that isn't a browser. For websites, use the [Web SDK](/docs/sdks/web) instead. Installation [#installation] Step 1: Install [#step-1-install] npm pnpm yarn bun ```bash npm install @pieowl/sdk ``` ```bash pnpm add @pieowl/sdk ``` ```bash yarn add @pieowl/sdk ``` ```bash bun add @pieowl/sdk ``` Step 2: Initialize [#step-2-initialize] ```js title="pieowl.ts" import { Pieowl } from '@pieowl/sdk'; export const pieowl = new Pieowl({ clientId: 'YOUR_CLIENT_ID', clientSecret: 'YOUR_CLIENT_SECRET', }); ``` Keep the client secret on the server. Never ship it to the browser. Options [#options] Step 3: Usage [#step-3-usage] ```js title="main.ts" import { pieowl } from './pieowl'; pieowl.track('my_event', { foo: 'bar' }); ``` Usage [#usage] Tracking events [#tracking-events] Call `pieowl.track()` with an event name and optional properties. ```ts title="index.ts" import { pieowl } from './pieowl'; pieowl.track('my_event', { foo: 'bar' }); ``` To track an event for a specific user, pass `profileId`: ```ts title="index.ts" pieowl.track('invoice_paid', { profileId: 'user_123', plan: 'pro' }); ``` Identifying users [#identifying-users] To identify a user, call `pieowl.identify()` with a unique ID. ```js title="index.js" import { pieowl } from './pieowl'; pieowl.identify({ profileId: '123', // Required firstName: 'Joe', lastName: 'Doe', email: 'joe@doe.com', properties: { tier: 'premium', }, }); ``` Setting global properties [#setting-global-properties] To set properties that are sent with every event: ```js title="index.js" import { pieowl } from './pieowl'; pieowl.setGlobalProperties({ app_version: '1.0.2', environment: 'production', }); ``` Incrementing properties [#incrementing-properties] To increase a numeric property on a user profile. * `value` is the amount to add. If you leave it out, the property goes up by 1. ```js title="index.js" import { pieowl } from './pieowl'; pieowl.increment({ profileId: '1', property: 'visits', value: 1 // optional }); ``` Decrementing properties [#decrementing-properties] To decrease a numeric property on a user profile. * `value` is the amount to subtract. If you leave it out, the property goes down by 1. ```js title="index.js" import { pieowl } from './pieowl'; pieowl.decrement({ profileId: '1', property: 'visits', value: 1 // optional }); ``` Tracking revenue [#tracking-revenue] Call `purchase()` when your server learns the payment went through. Send your order number as `id` and the amount in normal units, like `49.99`. `currency` is optional and defaults to your site currency. The same order sent twice counts once. Send the visitor's `deviceId` (from `pieowl.getDeviceId()` in the browser), and the purchase is credited to their visit. See [Revenue attribution](/docs/revenue-attribution) for how purchases are credited, and [Revenue tracking](/docs/revenue-tracking) for checkout flows. ```js title="index.js" import { pieowl } from './pieowl'; pieowl.purchase({ id: 'A-1001', amount: 49.99, currency: 'EUR', deviceId }); // Refund the whole order, or pass amount for part of it pieowl.refund({ id: 'A-1001', deviceId }); ``` Working with groups [#working-with-groups] Groups let you track analytics at the account or company level. See the [Groups guide](/docs/get-started/groups) for the full walkthrough. **Create or update a group:** ```js title="index.js" import { pieowl } from './pieowl'; pieowl.upsertGroup({ id: 'org_acme', type: 'company', name: 'Acme Inc', properties: { plan: 'enterprise' }, }); ``` **Assign the current user to a group** (call after `identify`): ```js title="index.js" import { pieowl } from './pieowl'; pieowl.setGroup('org_acme'); // or multiple groups: pieowl.setGroups(['org_acme', 'team_eng']); ``` Once set, every later `track()` call includes the group IDs. Clearing user data [#clearing-user-data] To clear the current user's data (including groups): ```js title="index.js" import { pieowl } from './pieowl'; pieowl.clear() ``` Server-side tips [#server-side-tips] * **Location:** Pieowl looks up the country and city from the request IP. On a server, that is your server's IP. To use the visitor's IP, forward it in the `pieowl-client-ip` header: `pieowl.api.addHeader('pieowl-client-ip', visitorIp)`. Headers are set on the instance, so on a server that handles many visitors, create one instance per request for this (see [Express](/docs/sdks/express#server-side-events)). * **Shared instances:** `identify()` and `setGroup()` store the user on the instance. On a server that handles many users, pass `profileId` on each `track()` call instead. --- ## Kotlin / Android URL: https://pieowl.com/docs/sdks/kotlin Pieowl doesn't ship a Kotlin or Android SDK. You send events from Kotlin with a plain HTTP request to the [Track API](/docs/api/track). The example below uses `HttpURLConnection` and `org.json`, which are built into Android, so there is nothing to add to Gradle. Looking for a step-by-step tutorial? Read the [Kotlin analytics guide](/guides/kotlin-analytics). Before you start [#before-you-start] You need a client ID and a client secret. Find them in the dashboard under **Settings → Developers → Keys**. Apps can't send the browser headers Pieowl uses to check where a request comes from, so app requests include the secret. Add the internet permission to your `AndroidManifest.xml` if your app doesn't have it yet: ```xml ``` Send an event [#send-an-event] Every request is a `POST` to `https://api.pieowl.com/track` with a JSON body of `{ "type": ..., "payload": ... }`. Android doesn't allow network calls on the main thread, so the example uses a coroutine on `Dispatchers.IO`. ```kotlin title="Pieowl.kt" import java.net.HttpURLConnection import java.net.URL import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONObject object Pieowl { private const val TRACK_URL = "https://api.pieowl.com/track" private const val CLIENT_ID = "YOUR_CLIENT_ID" private const val CLIENT_SECRET = "YOUR_CLIENT_SECRET" private const val TIMEOUT_MS = 5_000 suspend fun send(type: String, payload: JSONObject): Int = withContext(Dispatchers.IO) { val body = JSONObject().put("type", type).put("payload", payload).toString() val connection = URL(TRACK_URL).openConnection() as HttpURLConnection try { connection.requestMethod = "POST" connection.connectTimeout = TIMEOUT_MS connection.readTimeout = TIMEOUT_MS connection.doOutput = true connection.setRequestProperty("Content-Type", "application/json") connection.setRequestProperty("pieowl-client-id", CLIENT_ID) connection.setRequestProperty("pieowl-client-secret", CLIENT_SECRET) connection.outputStream.use { it.write(body.toByteArray()) } connection.responseCode } finally { connection.disconnect() } } } ``` Usage [#usage] Call `send` from a coroutine, for example `lifecycleScope.launch { ... }` in an activity. Tracking events [#tracking-events] ```kotlin Pieowl.send("track", JSONObject() .put("name", "signup") .put("profileId", "user_123") // optional .put("properties", JSONObject().put("plan", "pro"))) ``` Tracking screen views [#tracking-screen-views] To fill the pages report, send a `screen_view` event with the screen name in `__path`: ```kotlin Pieowl.send("track", JSONObject() .put("name", "screen_view") .put("properties", JSONObject().put("__path", "/settings"))) ``` Identifying users [#identifying-users] ```kotlin Pieowl.send("identify", JSONObject() .put("profileId", "user_123") // required .put("firstName", "Joe") .put("lastName", "Doe") .put("email", "joe@doe.com") .put("properties", JSONObject().put("tier", "premium"))) ``` Incrementing and decrementing properties [#incrementing-and-decrementing-properties] `value` is optional and defaults to 1. ```kotlin Pieowl.send("increment", JSONObject() .put("profileId", "user_123").put("property", "visits").put("value", 1)) Pieowl.send("decrement", JSONObject() .put("profileId", "user_123").put("property", "credits").put("value", 1)) ``` Good to know [#good-to-know] * **Errors:** `send` returns the HTTP status. A `2xx` status means Pieowl accepted the request. Network errors throw an `IOException`, so catch it if analytics must never crash your app. * **Offline:** Requests fail while the device is offline. If you need offline support, store events and send them later. * **More event types:** Revenue, groups and the full request schema are in the [Track API reference](/docs/api/track). --- ## Next.js URL: https://pieowl.com/docs/sdks/nextjs Looking for a step-by-step tutorial? Read the [Next.js analytics guide](/guides/nextjs-analytics). Good to know [#good-to-know] A Next.js app needs up to three pieces. Use the ones you need: * **Pages and visitors**: the [HTML script](/docs/sdks/script), loaded with `next/script`. It tracks client-side route changes in the App Router and the Pages Router. * **AI crawlers and bots**: `@pieowl/bots` in `proxy.ts`, which reports the bots the script can't see. * **Server-side events** (route handlers, server actions): `@pieowl/sdk`. Track pages [#track-pages] Add the script to your root layout and replace `YOUR_CLIENT_ID` with your client ID. The first script creates a small queue, so calls made before `pieowl.js` loads (for example in `useEffect`) are sent once it's ready. ```tsx title="app/layout.tsx" import Script from 'next/script'; export default function RootLayout({ children }) { return ( {children} ``` Identifying users [#identifying-users] ```ts window.pieowl('identify', { profileId: user.id, // Required email: user.email, properties: { tier: 'premium' }, }); ``` `setGlobalProperties`, `increment`, `decrement` and `clear` work the same way. See the [Script Tag](/docs/sdks/script#usage) docs. Track AI crawlers and bots [#track-ai-crawlers-and-bots] ```bash npm install @pieowl/bots ``` ```typescript title="server/middleware/pieowl-bots.ts" import { trackBotRequest } from '@pieowl/bots'; export default defineEventHandler((event) => { trackBotRequest({ url: getRequestURL(event).href, headers: getRequestHeaders(event) }, { clientId: process.env.PIEOWL_CLIENT_ID!, clientSecret: process.env.PIEOWL_CLIENT_SECRET!, }); }); ``` It reports only requests that look like a bot and never blocks the page. Read more in [AI crawlers and bots](/docs/ai-bots). Avoid ad blockers [#avoid-ad-blockers] Serve the script and send events through your own domain: ```typescript title="server/routes/api/pieowl/[...path].ts" import { createRouteHandler } from '@pieowl/bots/route-handler'; export default fromWebHandler(createRouteHandler()); ``` Then switch to the [`init` form](/docs/sdks/script#options-in-code), set `apiUrl: '/api/pieowl'` in `init`, and load the script from `/api/pieowl/pieowl.js`. See [Avoid ad blockers](/docs/adblockers). Server-side events [#server-side-events] To track events from your server, use the [JavaScript SDK](/docs/sdks/javascript). Server events must include your client secret. Browsers prove where they come from with CORS headers, but servers can't, so the secret authenticates the request. Use the same client ID as in the script, add its client secret, and keep it on the server. ```typescript title="server/api/signup.post.ts" import { Pieowl } from '@pieowl/sdk'; const pieowlServer = new Pieowl({ clientId: process.env.PIEOWL_CLIENT_ID!, clientSecret: process.env.PIEOWL_CLIENT_SECRET!, }); export default defineEventHandler(async (event) => { const user = await createUser(await readBody(event)); // In serverless functions, await so the event is sent before the function stops. await pieowlServer.track('sign_up', { profileId: user.id }); return { ok: true }; }); ``` --- ## Python URL: https://pieowl.com/docs/sdks/python Pieowl doesn't ship a Python SDK. You send events from Python with a plain HTTP request to the [Track API](/docs/api/track). The example below uses only the standard library, so there is nothing to install. Looking for a step-by-step tutorial? Read the [Python analytics guide](/guides/python-analytics). Before you start [#before-you-start] You need a client ID and a client secret. Find them in the dashboard under **Settings → Developers → Keys**. Server requests must include the secret, so keep it on your server and load it from an environment variable. Send an event [#send-an-event] Every request is a `POST` to `https://api.pieowl.com/track` with a JSON body of `{ "type": ..., "payload": ... }`. ```python title="pieowl.py" import json import os import urllib.request PIEOWL_URL = "https://api.pieowl.com/track" def pieowl_send(event_type, payload, client_ip=None, user_agent=None): headers = { "Content-Type": "application/json", "pieowl-client-id": os.environ["PIEOWL_CLIENT_ID"], "pieowl-client-secret": os.environ["PIEOWL_CLIENT_SECRET"], } # Optional: forward the visitor's IP and user agent for location and device data if client_ip: headers["pieowl-client-ip"] = client_ip if user_agent: headers["user-agent"] = user_agent body = json.dumps({"type": event_type, "payload": payload}).encode() request = urllib.request.Request(PIEOWL_URL, data=body, headers=headers, method="POST") with urllib.request.urlopen(request, timeout=5) as response: return response.status ``` Usage [#usage] Tracking events [#tracking-events] ```python pieowl_send("track", { "name": "signup", "profileId": "user_123", # optional "properties": {"plan": "pro"}, }) ``` Identifying users [#identifying-users] ```python pieowl_send("identify", { "profileId": "user_123", # required "firstName": "Joe", "lastName": "Doe", "email": "joe@doe.com", "properties": {"tier": "premium"}, }) ``` Incrementing and decrementing properties [#incrementing-and-decrementing-properties] `value` is optional and defaults to 1. ```python pieowl_send("increment", {"profileId": "user_123", "property": "visits", "value": 1}) pieowl_send("decrement", {"profileId": "user_123", "property": "credits", "value": 1}) ``` Tracking revenue [#tracking-revenue] A purchase is a `revenue` event with these properties: * `__amount`: in normal units, like `49.99`, not cents. * `__currency`: an ISO 4217 code, like `EUR`. Optional, your site currency by default. * `__orderId`: your order number. The same order sent twice counts once. * `__deviceId`: the visitor's device ID, from `pieowl.getDeviceId()` in the browser. The purchase is credited to their visit. For a refund, send the same event with `__refund` set to true. Leave out `__amount` to refund the whole order. See [Revenue attribution](/docs/revenue-attribution). ```python pieowl_send("track", { "name": "revenue", "properties": { "__amount": 49.99, "__currency": "EUR", "__orderId": "A-1001", "__deviceId": device_id, }, }) # Refund the whole order pieowl_send("track", { "name": "revenue", "properties": {"__orderId": "A-1001", "__refund": True}, }) ``` Good to know [#good-to-know] * **Errors:** `urlopen` raises `urllib.error.HTTPError` when Pieowl rejects a request, for example a bad client secret or an invalid payload. Catch it so analytics never breaks a user request. * **Don't block requests:** In a web app, send events from a background thread or task queue. * **More event types:** Groups and the full request schema are in the [Track API reference](/docs/api/track). --- ## React URL: https://pieowl.com/docs/sdks/react Good to know [#good-to-know] All tracking on this page happens in the browser. For React single-page apps, use `@pieowl/web` directly. You don't need a separate React SDK: create one `Pieowl` instance and import it where you need it. Installation [#installation] Step 1: Install [#step-1-install] ```bash npm install @pieowl/web ``` Step 2: Initialize [#step-2-initialize] Create a shared Pieowl instance in your project: ```ts title="src/pieowl.ts" import { Pieowl } from '@pieowl/web'; export const pieowl = new Pieowl({ clientId: 'YOUR_CLIENT_ID', }); ``` `clientId` is enough. What the SDK collects by itself (page views, outside link clicks, `data-track` clicks) is set in **Settings → Site → What Pieowl counts**, not in code. Options [#options] Step 3: Usage [#step-3-usage] Import the instance in your components: ```tsx import { pieowl } from '@/pieowl'; function MyComponent() { const handleClick = () => { pieowl.track('button_click', { button: 'signup' }); }; return ; } ``` Usage [#usage] Tracking events [#tracking-events] Call `pieowl.track()` directly, or add `data-track` attributes to your buttons and links. `data-track` needs **Clicks you marked in your code** to be on in Settings → Site (it is by default). ```tsx import { pieowl } from '@/pieowl'; function MyComponent() { useEffect(() => { pieowl.track('my_event', { foo: 'bar' }); }, []); return
My Component
; } ``` Identifying users [#identifying-users] To identify a user, call `pieowl.identify()` with a unique ID. ```tsx import { pieowl } from '@/pieowl'; function LoginComponent() { const handleLogin = (user: User) => { pieowl.identify({ profileId: user.id, // Required firstName: user.firstName, lastName: user.lastName, email: user.email, properties: { tier: 'premium', }, }); }; return ; } ``` Setting global properties [#setting-global-properties] To set properties that are sent with every event: ```tsx import { pieowl } from '@/pieowl'; function App() { useEffect(() => { pieowl.setGlobalProperties({ app_version: '1.0.2', environment: 'production', }); }, []); return
App
; } ``` Incrementing properties [#incrementing-properties] To increase a numeric property on a user profile. * `value` is the amount to add. If you leave it out, the property goes up by 1. ```tsx import { pieowl } from '@/pieowl'; function MyComponent() { const handleAction = () => { pieowl.increment({ profileId: '1', property: 'visits', value: 1, // optional }); }; return ; } ``` Decrementing properties [#decrementing-properties] To decrease a numeric property on a user profile. * `value` is the amount to subtract. If you leave it out, the property goes down by 1. ```tsx import { pieowl } from '@/pieowl'; function MyComponent() { const handleAction = () => { pieowl.decrement({ profileId: '1', property: 'visits', value: 1, // optional }); }; return ; } ``` Working with groups [#working-with-groups] Groups let you track analytics at the account or company level. See the [Groups guide](/docs/get-started/groups) for the full walkthrough. ```tsx import { pieowl } from '@/pieowl'; function LoginComponent() { const handleLogin = async (user: User) => { // 1. Identify the user pieowl.identify({ profileId: user.id, email: user.email }); // 2. Create/update the group entity (only when data changes) pieowl.upsertGroup({ id: user.organizationId, type: 'company', name: user.organizationName, properties: { plan: user.plan }, }); // 3. Link the user to their group — tags all future events pieowl.setGroup(user.organizationId); }; return ; } ``` Clearing user data [#clearing-user-data] To clear the current user's data (including groups): ```tsx import { pieowl } from '@/pieowl'; function LogoutComponent() { const handleLogout = () => { pieowl.clear(); // ... logout logic }; return ; } ``` Revenue tracking [#revenue-tracking] Call `purchase()` with your order number and the amount in normal units, like `49.99`. `currency` is optional and defaults to your site currency. The same order sent twice counts once. Purchases and refunds from the browser only work after you turn on **Accept revenue sent from the browser** in Settings → Site → Revenue. Each purchase is credited to the current visit. See [Revenue attribution](/docs/revenue-attribution). ```tsx import { pieowl } from '@/pieowl'; function CheckoutComponent() { const handlePurchase = async () => { // Track a purchase right away await pieowl.purchase({ id: 'A-1001', amount: 29.99, currency: 'EUR' }); // Refund it later: the whole order, or pass amount for part of it await pieowl.refund({ id: 'A-1001' }); }; return ; } ``` Optional: create a hook [#optional-create-a-hook] If you prefer a React hook, write a small wrapper: ```ts title="src/hooks/usePieowl.ts" import { pieowl } from '@/pieowl'; export function usePieowl() { return pieowl; } ``` Then use it in your components: ```tsx import { usePieowl } from '@/hooks/usePieowl'; function MyComponent() { const pieowl = usePieowl(); useEffect(() => { pieowl.track('my_event', { foo: 'bar' }); }, []); return
My Component
; } ``` --- ## React Native URL: https://pieowl.com/docs/sdks/react-native Looking for a step-by-step tutorial? Read the [React Native analytics guide](/guides/react-native-analytics). Installation [#installation] Install dependencies [#install-dependencies] The SDK uses `expo-application` for the app version and build number (and the install referrer on Android), and `expo-constants` to get the user agent. npm pnpm yarn bun ```bash npm install @pieowl/react-native npx expo install expo-application expo-constants ``` ```bash npm install @pieowl/react-native pnpm dlx expo install expo-application expo-constants ``` ```bash npm install @pieowl/react-native yarn dlx expo install expo-application expo-constants ``` ```bash npm install @pieowl/react-native bun x expo install expo-application expo-constants ``` Initialize [#initialize] Native apps can't send CORS headers, so the SDK authenticates with your client secret. ```typescript import { Pieowl } from '@pieowl/react-native'; const pieowl = new Pieowl({ clientId: '{YOUR_CLIENT_ID}', clientSecret: '{YOUR_CLIENT_SECRET}', }); ``` Options [#options] Usage [#usage] Track event [#track-event] ```typescript pieowl.track('my_event', { foo: 'bar' }); ``` Navigation and screen views [#navigation-and-screen-views] ```typescript import { usePathname, useSegments } from 'expo-router'; const pieowl = new Pieowl({ /* ... */ }) function RootLayout() { // ... const pathname = usePathname() // Segments is optional but can be nice to have if you // want to group routes together // pathname = /posts/123 // segments = ['posts', '[id]'] const segments = useSegments() useEffect(() => { // Simple pieowl.screenView(pathname) // With extra data pieowl.screenView(pathname, { // segments is optional but nice to have segments: segments.join('/'), // other optional data you want to send with the screen view }) }, [pathname,segments]) // ... } ``` ```tsx import { createNavigationContainerRef } from '@react-navigation/native' import { Pieowl } from '@pieowl/react-native' const pieowl = new Pieowl({ /* ... */ }) const navigationRef = createNavigationContainerRef() export function NavigationRoot() { const handleNavigationStateChange = () => { const current = navigationRef.getCurrentRoute() if (current) { pieowl.screenView(current.name, { params: current.params, }) } } return ( ) } ``` For all other methods, see the [JavaScript SDK](/docs/sdks/javascript#usage). Offline support [#offline-support] The SDK can hold events while the device is offline and send them when it's back online. Each queued event gets a timestamp when it fires, so it's recorded at the right time even if it arrives later. Two optional peer dependencies enable this feature: npm pnpm yarn bun ```bash npm install @react-native-async-storage/async-storage @react-native-community/netinfo ``` ```bash pnpm add @react-native-async-storage/async-storage @react-native-community/netinfo ``` ```bash yarn add @react-native-async-storage/async-storage @react-native-community/netinfo ``` ```bash bun add @react-native-async-storage/async-storage @react-native-community/netinfo ``` Pass them to the constructor: ```typescript import { Pieowl } from '@pieowl/react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; import NetInfo from '@react-native-community/netinfo'; const pieowl = new Pieowl({ clientId: '{YOUR_CLIENT_ID}', clientSecret: '{YOUR_CLIENT_SECRET}', // Persist the event queue across app restarts storage: AsyncStorage, // Automatically flush the queue when the device comes back online networkInfo: NetInfo, }); ``` What each option does: * **`networkInfo`** — tells the SDK when the device goes offline, so events are queued instead of sent, and sends the queue as soon as the device is back online. * **`storage`** — saves the queue to disk, so queued events survive an app restart. The SDK also tries to send the queue each time the app becomes active. --- ## Remix URL: https://pieowl.com/docs/sdks/remix Pieowl doesn't have a Remix-specific SDK. Use the [script tag](/docs/sdks/script) or the [Web SDK](/docs/sdks/web) (`@pieowl/web`). Both work in Remix apps. For a full walkthrough, read the [Remix analytics guide](/guides/remix-analytics). --- ## Ruby URL: https://pieowl.com/docs/sdks/ruby Pieowl doesn't ship a Ruby gem. You send events from Ruby with a plain HTTP request to the [Track API](/docs/api/track). The example below uses `Net::HTTP` from the standard library, so there is nothing to install. Before you start [#before-you-start] You need a client ID and a client secret. Find them in the dashboard under **Settings → Developers → Keys**. Server requests must include the secret, so keep it on your server and load it from an environment variable. Send an event [#send-an-event] Every request is a `POST` to `https://api.pieowl.com/track` with a JSON body of `{ "type": ..., "payload": ... }`. ```ruby title="pieowl.rb" require "json" require "net/http" module Pieowl URL = URI("https://api.pieowl.com/track") def self.send_event(type, payload, client_ip: nil, user_agent: nil) headers = { "Content-Type" => "application/json", "pieowl-client-id" => ENV.fetch("PIEOWL_CLIENT_ID"), "pieowl-client-secret" => ENV.fetch("PIEOWL_CLIENT_SECRET") } # Optional: forward the visitor's IP and user agent for location and device data headers["pieowl-client-ip"] = client_ip if client_ip headers["user-agent"] = user_agent if user_agent response = Net::HTTP.post(URL, { type: type, payload: payload }.to_json, headers) response.is_a?(Net::HTTPSuccess) end end ``` Usage [#usage] Tracking events [#tracking-events] ```ruby Pieowl.send_event("track", { name: "signup", profileId: "user_123", # optional properties: { plan: "pro" } }) ``` Identifying users [#identifying-users] ```ruby Pieowl.send_event("identify", { profileId: "user_123", # required firstName: "Joe", lastName: "Doe", email: "joe@doe.com", properties: { tier: "premium" } }) ``` Incrementing and decrementing properties [#incrementing-and-decrementing-properties] `value` is optional and defaults to 1. ```ruby Pieowl.send_event("increment", { profileId: "user_123", property: "visits", value: 1 }) Pieowl.send_event("decrement", { profileId: "user_123", property: "credits", value: 1 }) ``` Tracking revenue [#tracking-revenue] A purchase is a `revenue` event with these properties: * `__amount`: in normal units, like `49.99`, not cents. * `__currency`: an ISO 4217 code, like `EUR`. Optional, your site currency by default. * `__orderId`: your order number. The same order sent twice counts once. * `__deviceId`: the visitor's device ID, from `pieowl.getDeviceId()` in the browser. The purchase is credited to their visit. For a refund, send the same event with `__refund` set to true. Leave out `__amount` to refund the whole order. See [Revenue attribution](/docs/revenue-attribution). ```ruby Pieowl.send_event("track", { name: "revenue", properties: { __amount: 49.99, __currency: "EUR", __orderId: "A-1001", __deviceId: device_id } }) # Refund the whole order Pieowl.send_event("track", { name: "revenue", properties: { __orderId: "A-1001", __refund: true } }) ``` Rails [#rails] In a controller, pass the visitor's IP and user agent so Pieowl can show their location and device: ```ruby class SignupsController < ApplicationController def create user = User.create!(user_params) Pieowl.send_event( "track", { name: "signup", profileId: user.id.to_s }, client_ip: request.remote_ip, user_agent: request.user_agent ) redirect_to dashboard_path end end ``` For busy apps, send events from a background job (for example Active Job) so a slow request never holds up the response. Good to know [#good-to-know] * **Errors:** `send_event` returns `false` when Pieowl rejects a request, for example a bad client secret or an invalid payload. Network errors raise, so rescue them if analytics must never break a request. * **More event types:** Groups and the full request schema are in the [Track API reference](/docs/api/track). --- ## Rust URL: https://pieowl.com/docs/sdks/rust Pieowl doesn't ship a Rust crate. You send events from Rust with a plain HTTP request to the [Track API](/docs/api/track). Rust has no HTTP client in its standard library, so the example below uses the common `reqwest` and `serde_json` crates. Before you start [#before-you-start] You need a client ID and a client secret. Find them in the dashboard under **Settings → Developers → Keys**. Server requests must include the secret, so keep it on your server and load it from an environment variable. Add the crates to your `Cargo.toml`: ```toml title="Cargo.toml" [dependencies] reqwest = { version = "0.12", features = ["json"] } serde_json = "1" tokio = { version = "1", features = ["full"] } ``` Send an event [#send-an-event] Every request is a `POST` to `https://api.pieowl.com/track` with a JSON body of `{ "type": ..., "payload": ... }`. ```rust title="src/pieowl.rs" use serde_json::{json, Value}; use std::time::Duration; const TRACK_URL: &str = "https://api.pieowl.com/track"; pub async fn send( client: &reqwest::Client, event_type: &str, payload: Value, ) -> Result { let response = client .post(TRACK_URL) .timeout(Duration::from_secs(5)) .header("pieowl-client-id", std::env::var("PIEOWL_CLIENT_ID").unwrap_or_default()) .header("pieowl-client-secret", std::env::var("PIEOWL_CLIENT_SECRET").unwrap_or_default()) // Optional: forward the visitor's IP for location data // .header("pieowl-client-ip", visitor_ip) .json(&json!({ "type": event_type, "payload": payload })) .send() .await?; Ok(response.status()) } ``` Create one `reqwest::Client` and reuse it for every call. Usage [#usage] Tracking events [#tracking-events] ```rust let client = reqwest::Client::new(); send(&client, "track", json!({ "name": "signup", "profileId": "user_123", // optional "properties": { "plan": "pro" } })).await?; ``` Identifying users [#identifying-users] ```rust send(&client, "identify", json!({ "profileId": "user_123", // required "firstName": "Joe", "lastName": "Doe", "email": "joe@doe.com", "properties": { "tier": "premium" } })).await?; ``` Incrementing and decrementing properties [#incrementing-and-decrementing-properties] `value` is optional and defaults to 1. ```rust send(&client, "increment", json!({ "profileId": "user_123", "property": "visits", "value": 1 })).await?; send(&client, "decrement", json!({ "profileId": "user_123", "property": "credits", "value": 1 })).await?; ``` Tracking revenue [#tracking-revenue] A purchase is a `revenue` event with these properties: * `__amount`: in normal units, like `49.99`, not cents. * `__currency`: an ISO 4217 code, like `EUR`. Optional, your site currency by default. * `__orderId`: your order number. The same order sent twice counts once. * `__deviceId`: the visitor's device ID, from `pieowl.getDeviceId()` in the browser. The purchase is credited to their visit. For a refund, send the same event with `__refund` set to true. Leave out `__amount` to refund the whole order. See [Revenue attribution](/docs/revenue-attribution). ```rust send(&client, "track", json!({ "name": "revenue", "properties": { "__amount": 49.99, "__currency": "EUR", "__orderId": "A-1001", "__deviceId": device_id } })).await?; // Refund the whole order send(&client, "track", json!({ "name": "revenue", "properties": { "__orderId": "A-1001", "__refund": true } })).await?; ``` Good to know [#good-to-know] * **Errors:** `send` returns the HTTP status. A `2xx` status means Pieowl accepted the request. Network errors come back as `reqwest::Error`. * **Don't block requests:** In a web server, send events with `tokio::spawn` so a slow call never holds up the response. * **More event types:** Groups and the full request schema are in the [Track API reference](/docs/api/track). --- ## Script Tag URL: https://pieowl.com/docs/sdks/script Installation [#installation] Paste this tag into the `` of your page and replace `YOUR_CLIENT_ID` with your client ID. ```html title="index.html" /YOUR_CLIENT_ID/ ``` What gets collected [#what-gets-collected] You choose this in the dashboard, in **Settings → Site → What Pieowl counts**: * **Page views**: every page someone opens. On by default. * **Clicks on links to other sites**: which outside links people click. On by default. * **Clicks you marked in your code**: elements with `data-track="…"`. On by default. * **Address changes after #**: for one-page sites that change only the part after `#`. Off by default. * **Session replay**: records visits so you can watch them later. Off by default. See [Session replay](/docs/session-replay). The script loads these settings on every page. A change reaches your site within about a minute, with no code to edit. Calling Pieowl before the script loads [#calling-pieowl-before-the-script-loads] `window.pieowl('track', ...)` works once `pieowl.js` has loaded. To call it earlier, put this line before the script tag. It queues your calls and sends them in order once the script is ready. Until then only the `window.pieowl('track', ...)` form works, not `window.pieowl.track(...)`. ```html title="index.html" ``` Options in code [#options-in-code] Most sites need no options. For options from the list below, such as `filter` or `disabled`, use the `init` form instead of `data-client-id`: the queue line, then `init` with your options, then the script tag without `data-client-id`. ```html title="index.html" ``` Options [#options] Usage [#usage] You can call every method in two ways: `window.pieowl('track', ...)` or `window.pieowl.track(...)`. The second form works only after `pieowl.js` has loaded. This page uses the first form. Tracking events [#tracking-events] Call `window.pieowl('track')` directly, or add `data-track` attributes to your buttons and links. `data-track` needs **Clicks you marked in your code** to be on in Settings → Site (it is by default). ```html title="index.html" ``` ```html title="index.html" ``` Identifying users [#identifying-users] To identify a user, call `window.pieowl('identify')` with a unique ID. ```js title="main.js" window.pieowl('identify', { profileId: '123', // Required firstName: 'Joe', lastName: 'Doe', email: 'joe@doe.com', properties: { tier: 'premium', }, }); ``` Setting global properties [#setting-global-properties] To set properties that are sent with every event: ```js title="main.js" window.pieowl('setGlobalProperties', { app_version: '1.0.2', environment: 'production', }); ``` Incrementing properties [#incrementing-properties] To increase a numeric property on a user profile. * `value` is the amount to add. If you leave it out, the property goes up by 1. ```js title="main.js" window.pieowl('increment', { profileId: '1', property: 'visits', value: 1 // optional }); ``` Decrementing properties [#decrementing-properties] To decrease a numeric property on a user profile. * `value` is the amount to subtract. If you leave it out, the property goes down by 1. ```js title="main.js" window.pieowl('decrement', { profileId: '1', property: 'visits', value: 1 // optional }); ``` Tracking revenue [#tracking-revenue] Send the order number as `id` and the amount in normal units, like `49.99`. `currency` is optional and defaults to your site currency. The same order sent twice counts once. Purchases and refunds sent from the browser are rejected unless you turn on **Accept revenue sent from the browser** in Settings → Site → Revenue. Each purchase is credited to the current visit. See [Revenue attribution](/docs/revenue-attribution) for how purchases are credited, and [Revenue tracking](/docs/revenue-tracking) for checkout flows. ```js title="main.js" window.pieowl('purchase', { id: 'A-1001', amount: 49.99, currency: 'EUR' }); // Refund the whole order, or pass amount for part of it window.pieowl('refund', { id: 'A-1001' }); ``` Clearing user data [#clearing-user-data] To clear the current user's data, for example on logout: ```js title="main.js" window.pieowl('clear'); ``` Advanced usage [#advanced-usage] Filtering events [#filtering-events] Add a `filter` function to the `init` options (see [Options in code](#options-in-code)). If it returns `false`, the event is not sent. This example turns tracking off for users who have a `disable_tracking` item in local storage. ```js title="main.js" window.pieowl('init', { clientId: 'YOUR_CLIENT_ID', filter: () => localStorage.getItem('disable_tracking') === null, }); ``` Using the Web SDK with npm [#using-the-web-sdk-with-npm] Step 1: Install the SDK [#step-1-install-the-sdk] npm pnpm yarn bun ```bash npm install @pieowl/web ``` ```bash pnpm add @pieowl/web ``` ```bash yarn add @pieowl/web ``` ```bash bun add @pieowl/web ``` Step 2: Initialize the SDK [#step-2-initialize-the-sdk] ```js title="pieowl.js" import { Pieowl } from '@pieowl/web'; export const pieowl = new Pieowl({ clientId: 'YOUR_CLIENT_ID', }); ``` Step 3: Use the SDK [#step-3-use-the-sdk] ```js title="main.js" import { pieowl } from './pieowl.js'; pieowl.track('my_event', { foo: 'bar' }); ``` TypeScript [#typescript] Getting TypeScript errors when you call `window.pieowl`? Add a type definition file to your project. Simple [#simple] Paste this into any of your `.d.ts` files. ```ts title="pieowl.d.ts" declare global { interface Window { pieowl: { q?: IArguments[]; (...args: [ 'init' | 'track' | 'identify' | 'setGlobalProperties' | 'increment' | 'decrement' | 'revenue' | 'clear', ...any[] ]): void; }; } } ``` Strict typing (from the SDK) [#strict-typing-from-the-sdk] Step 1: Install the SDK [#step-1-install-the-sdk-1] npm pnpm yarn bun ```bash npm install @pieowl/web ``` ```bash pnpm add @pieowl/web ``` ```bash yarn add @pieowl/web ``` ```bash bun add @pieowl/web ``` Step 2: Create a type definition file [#step-2-create-a-type-definition-file] Create a `pieowl.d.ts` file and paste this line: ```ts title="pieowl.d.ts" /// ``` --- ## Swift URL: https://pieowl.com/docs/sdks/swift Pieowl doesn't ship a Swift SDK. You send events from Swift with a plain HTTP request to the [Track API](/docs/api/track). The example below uses `URLSession` and `JSONSerialization` from Foundation, so there is no package to add. It works on iOS, macOS, tvOS and watchOS. Looking for a step-by-step tutorial? Read the [Swift analytics guide](/guides/swift-analytics). Before you start [#before-you-start] You need a client ID and a client secret. Find them in the dashboard under **Settings → Developers → Keys**. Apps can't send the browser headers Pieowl uses to check where a request comes from, so app requests include the secret. Send an event [#send-an-event] Every request is a `POST` to `https://api.pieowl.com/track` with a JSON body of `{ "type": ..., "payload": ... }`. ```swift title="Pieowl.swift" import Foundation enum Pieowl { private static let trackURL = URL(string: "https://api.pieowl.com/track")! private static let clientId = "YOUR_CLIENT_ID" private static let clientSecret = "YOUR_CLIENT_SECRET" @discardableResult static func send(_ type: String, _ payload: [String: Any]) async throws -> Int { var request = URLRequest(url: trackURL) request.httpMethod = "POST" request.timeoutInterval = 5 request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue(clientId, forHTTPHeaderField: "pieowl-client-id") request.setValue(clientSecret, forHTTPHeaderField: "pieowl-client-secret") request.httpBody = try JSONSerialization.data( withJSONObject: ["type": type, "payload": payload] ) let (_, response) = try await URLSession.shared.data(for: request) return (response as? HTTPURLResponse)?.statusCode ?? 0 } } ``` `async` `URLSession` calls need iOS 15, macOS 12, tvOS 15 or watchOS 8. Usage [#usage] Call `send` from a `Task`, for example `Task { try? await Pieowl.send(...) }`. Tracking events [#tracking-events] ```swift try await Pieowl.send("track", [ "name": "signup", "profileId": "user_123", // optional "properties": ["plan": "pro"], ]) ``` Tracking screen views [#tracking-screen-views] To fill the pages report, send a `screen_view` event with the screen name in `__path`: ```swift try await Pieowl.send("track", [ "name": "screen_view", "properties": ["__path": "/settings"], ]) ``` Identifying users [#identifying-users] ```swift try await Pieowl.send("identify", [ "profileId": "user_123", // required "firstName": "Joe", "lastName": "Doe", "email": "joe@doe.com", "properties": ["tier": "premium"], ]) ``` Incrementing and decrementing properties [#incrementing-and-decrementing-properties] `value` is optional and defaults to 1. ```swift try await Pieowl.send("increment", ["profileId": "user_123", "property": "visits", "value": 1]) try await Pieowl.send("decrement", ["profileId": "user_123", "property": "credits", "value": 1]) ``` Good to know [#good-to-know] * **Errors:** `send` returns the HTTP status. A `2xx` status means Pieowl accepted the request. Network errors throw, so use `try?` if analytics must never interrupt your app. * **Offline:** Requests fail while the device is offline. If you need offline support, store events and send them later. * **More event types:** Revenue, groups and the full request schema are in the [Track API reference](/docs/api/track). --- ## Vue URL: https://pieowl.com/docs/sdks/vue Looking for a step-by-step tutorial? Read the [Vue analytics guide](/guides/vue-analytics). Good to know [#good-to-know] All tracking on this page happens in the browser. For Vue single-page apps, use `@pieowl/web` directly. You don't need a separate Vue SDK: create one `Pieowl` instance and import it where you need it. For Nuxt, see [Nuxt](/docs/sdks/nuxt). Installation [#installation] Step 1: Install [#step-1-install] ```bash npm install @pieowl/web ``` Step 2: Initialize [#step-2-initialize] Create a shared Pieowl instance in your project: ```ts title="src/pieowl.ts" import { Pieowl } from '@pieowl/web'; export const pieowl = new Pieowl({ clientId: 'YOUR_CLIENT_ID', }); ``` `clientId` is enough. What the SDK collects by itself (page views, outside link clicks, `data-track` clicks) is set in **Settings → Site → What Pieowl counts**, not in code. Options [#options] Step 3: Usage [#step-3-usage] Import the instance in your Vue components: ```vue ``` Usage [#usage] Tracking events [#tracking-events] Call `pieowl.track()` directly, or add `data-track` attributes to your buttons and links. `data-track` needs **Clicks you marked in your code** to be on in Settings → Site (it is by default). ```vue ``` Identifying users [#identifying-users] To identify a user, call `pieowl.identify()` with a unique ID. ```vue ``` Setting global properties [#setting-global-properties] To set properties that are sent with every event: ```vue ``` Incrementing properties [#incrementing-properties] To increase a numeric property on a user profile. * `value` is the amount to add. If you leave it out, the property goes up by 1. ```vue ``` Decrementing properties [#decrementing-properties] To decrease a numeric property on a user profile. * `value` is the amount to subtract. If you leave it out, the property goes down by 1. ```vue ``` Clearing user data [#clearing-user-data] To clear the current user's data: ```vue ``` Revenue tracking [#revenue-tracking] Call `purchase()` with your order number and the amount in normal units, like `49.99`. `currency` is optional and defaults to your site currency. The same order sent twice counts once. Purchases and refunds from the browser only work after you turn on **Accept revenue sent from the browser** in Settings → Site → Revenue. Each purchase is credited to the current visit. See [Revenue attribution](/docs/revenue-attribution). ```vue ``` Optional: create a composable [#optional-create-a-composable] If you prefer a composable, write a small wrapper: ```ts title="src/composables/usePieowl.ts" import { pieowl } from '@/pieowl'; export function usePieowl() { return pieowl; } ``` Then use it in your components: ```vue ``` --- ## JavaScript (Web) URL: https://pieowl.com/docs/sdks/web Installation [#installation] Step 1: Install [#step-1-install] npm pnpm yarn bun ```bash npm install @pieowl/web ``` ```bash pnpm add @pieowl/web ``` ```bash yarn add @pieowl/web ``` ```bash bun add @pieowl/web ``` Step 2: Initialize [#step-2-initialize] ```js title="pieowl.ts" import { Pieowl } from '@pieowl/web'; export const pieowl = new Pieowl({ clientId: 'YOUR_CLIENT_ID', }); ``` `clientId` is enough. What the SDK collects by itself (page views, outside link clicks, `data-track` clicks, session replay) is set in **Settings → Site → What Pieowl counts**, not in code. Options [#options] Step 3: Usage [#step-3-usage] ```js title="main.ts" import { pieowl } from './pieowl'; pieowl.track('my_event', { foo: 'bar' }); ``` Usage [#usage] The Web SDK has the same methods as the [JavaScript SDK](/docs/sdks/javascript#usage). It also adds: * `screenView()` to track a page view by hand * Session replay, turned on per site in Settings → Site. See [Session replay](/docs/session-replay). * `allowReplay()` to start session replay once the visitor agrees, when **Record only after visitors agree** is on. See [Consent management](/docs/consent-management#session-replay-only-after-consent). --- ## Session Replay URL: https://pieowl.com/docs/session-replay Session replay records what users do on your website. You can replay any session to see what they clicked, how they filled in forms, and where they got stuck, without guessing. Session replay is **off by default**. You turn it on per site in the dashboard, with no code to edit. When it's off, the replay script is never downloaded, so your analytics script stays small. How it works [#how-it-works] Pieowl session replay is built on [rrweb](https://www.rrweb.io/), a widely used library for recording and replaying web sessions. It captures page changes, mouse movements, scroll positions and clicks as structured data, not video. The recorder loads **asynchronously** as a separate script (`pieowl-replay.js`). This means: * The main tracking script (`pieowl.js`) stays small, with or without replay * The recorder is only downloaded for sessions that are actually recorded * Page load speed isn't affected when replay is off Retention [#retention] Replays are kept for 30 days, then deleted automatically. Setup [#setup] 1. [Install Pieowl](/docs/sdks/script). The one-line script tag or the `@pieowl/web` package is enough. 2. In the dashboard, open **Settings → Site → What Pieowl counts** and turn on **Session replay**. 3. Keep **Record only after visitors agree** on (it's on by default), and call `allowReplay` when a visitor accepts in your consent banner. See [Consent](#consent). 4. Choose **Visits recorded**: every visit, 50%, 25% or 10% of visits. Fewer recordings keep the list short on busy sites. The script loads these settings on every page. A change reaches your site within about a minute. When replay is off for a site, Pieowl drops any recordings sent for it. With the npm package, your bundler splits the recorder into its own chunk. It's never part of your main bundle, and it's only loaded when session replay is on. Watching replays [#watching-replays] Go to **People → Visitors → Visits**. Visits with a recording have a **Replay** chip. Open a visit and click **Watch replay**. Options in code [#options-in-code] Most sites need none of these. To use them, add `sessionReplay` to the [`init` form](/docs/sdks/script#options-in-code) or to `new Pieowl({...})`. They don't turn replay on; that's done in the dashboard. | Option | Type | Default | Description | | ------------------- | -------- | ---------------------------- | ------------------------------------------------------------ | | `blockSelector` | `string` | `[data-pieowl-replay-block]` | CSS selector for elements to replace with a placeholder | | `blockClass` | `string` | — | Class name that blocks elements from being recorded | | `ignoreSelector` | `string` | — | CSS selector for elements whose interactions aren't recorded | | `flushIntervalMs` | `number` | `10000` | How often (ms) recorded data is sent to the server | | `maxEventsPerChunk` | `number` | `200` | Maximum number of recorded events per chunk | | `maxPayloadBytes` | `number` | `1048576` | Maximum chunk size in bytes (1 MB) | | `scriptUrl` | `string` | — | Custom URL for the replay script (script tag only) | Privacy controls [#privacy-controls] All text and form fields are always hidden. Each character is replaced with `*` in the browser, before anything is sent. Page text, labels and input values show up as `***` in replays, while the layout stays visible. This can't be turned off, in the dashboard or in code. So replays can't capture names, emails or other personal data shown as text on the page. Images still show. To hide one, such as a photo of a face, block it. Blocking elements [#blocking-elements] Elements that match `blockSelector` or `blockClass` show up as a grey placeholder of the same size. The element and everything inside it are never recorded. ```html
This section won't appear in replays at all
``` Or use your own selector: ```ts sessionReplay: { blockSelector: '.payment-form, .user-avatar', blockClass: 'no-replay', } ``` Setting `blockSelector` replaces the default, so add `[data-pieowl-replay-block]` to your selector if you still want the attribute to work. Ignoring interactions [#ignoring-interactions] Use `ignoreSelector` to skip interactions on some elements. The element still shows in the replay, but clicks and typing on it aren't recorded. ```ts sessionReplay: { ignoreSelector: '.debug-panel', } ``` Consent [#consent] Recording visits needs your visitors' consent. Ask for it in your own consent banner, and name Pieowl session replay in your privacy policy. With **Record only after visitors agree** on, Pieowl records nothing until your site calls `allowReplay` after the visitor accepts: ```js // Script tag window.pieowl('allowReplay'); // npm package pieowl.allowReplay(); ``` Pieowl stores nothing in the browser, so call it on every page where the visitor has agreed. If they decline, don't call it. See [Consent management](/docs/consent-management#session-replay-only-after-consent) and [Do you need a cookie banner?](/docs/consent-and-cookie-banners). Custom script URL [#custom-script-url] The script tag loads the recorder from next to `pieowl.js`: it swaps `.js` for `-replay.js` in the script URL. If you serve `pieowl.js` from your own domain, put `pieowl-replay.js` next to it. You can also set the URL yourself: ```ts sessionReplay: { scriptUrl: 'https://your-cdn.example.com/pieowl-replay.js', } ``` Related [#related] * [Session tracking](/features/session-tracking) — understand sessions without full replay * [Session replay feature overview](/features/session-replay) — what you get with session replay * [Web SDK](/docs/sdks/web) — full Web SDK reference * [Script tag](/docs/sdks/script) — using Pieowl with a script tag