# MetricRaven documentation > Every MetricRaven docs page in one file, in reading order. The index with agent guidance is https://metricraven.com/llms.txt; each page is also at its own `.md` URL. --- # Quickstart > Create a site, install the script and see your first pageview in a few minutes. Source: https://metricraven.com/docs/quickstart Markdown: https://metricraven.com/docs/quickstart.md Section: Get started Install the tracker first. Revenue and search data are optional extras; you don't need them to see visitors. 1. **Create your site** [Sign in](https://metricraven.com/signup) with Google or a six-digit email code. Choose **Add a site** and enter a name and domain, such as `example.com`. 2. **Save the secret key** The secret key is shown once, right after you create the site. Store it in your backend's secret manager as `METRICRAVEN_SECRET_KEY`. It must never go in browser code. 3. **Add the script to your website** Copy the script from your site's **Settings → Install** card and paste it into your website's shared layout. The public site ID inside it is safe to publish. Follow [Install & verify](https://metricraven.com/docs/install) or pick your platform under [Installation guides](https://metricraven.com/docs/install-html). 4. **Open your website and check Traffic** Visit your website in a normal browser, then open the site's **Traffic** tab. You should see a pageview and, while the page is open, a live visitor. > **Verify it works** > > Your first pageview confirms the public tracker works. Connecting a payment or search provider is a separate step. ## What to set up next | You want | Do this | | --- | --- | | Website traffic | Install the script. | | Signups and other conversions | Add [goals](https://metricraven.com/docs/goals-funnels) or send verified [server events](https://metricraven.com/docs/server-events). | | Stripe revenue | [Connect a restricted key](https://metricraven.com/docs/stripe), then add checkout attribution. | | RevenueCat revenue | [Configure a webhook](https://metricraven.com/docs/revenuecat), then link app users to visits. | | Other payment providers | Add a signed webhook for [Lemon Squeezy](https://metricraven.com/docs/lemonsqueezy), [Polar](https://metricraven.com/docs/polar), [Paddle](https://metricraven.com/docs/paddle), [Dodo Payments](https://metricraven.com/docs/dodo-payments), [WooCommerce](https://metricraven.com/docs/woocommerce) or [Shopify](https://metricraven.com/docs/shopify), then pass the visitor ID at checkout. | | Google keywords | [Authorize Search Console](https://metricraven.com/docs/search-console) and choose a property. | ## See who is online now Click **Online** in Traffic overview, or **Open realtime globe** in the live card. You get globe and map views, country, device and source filters, and an accessible visitor list. See [Realtime](https://metricraven.com/docs/realtime) for replay and the value of visitors online. ## Details - The live window covers the last five minutes and refreshes every 15 seconds, independently of report dates and filters. - Locations are approximate IP locations rounded to 0.1°. Visitors without coordinates still count toward the total. - On busier sites, map and list filters use the 500 most recently active visitors; the total includes everyone. - Recent goals and payments show site-wide activity from the last 30 minutes. - Live reports are private and unavailable in public share and embed links. --- # Install & verify > Add the MetricRaven script to every page, confirm data is arriving and configure optional attributes. Source: https://metricraven.com/docs/install Markdown: https://metricraven.com/docs/install.md Section: Get started One script tag records traffic on every page that loads it. 1. **Paste the script before \** Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```html title="Your website's shared layout" ``` Framework apps should put it in their shared layout so every route loads it. See the [installation guides](https://metricraven.com/docs/install-guides) for platform-specific steps. Prefer npm? Use the [NPM package](https://metricraven.com/docs/npm-package). For mobile apps, see [React Native & Expo](https://metricraven.com/docs/react-native). 2. **Deploy and open your website** Publish the change, then open the live site in your browser. 3. **Check the Network panel** Find `script.js`, then a request to `/api/v1/collect`. A successful collection response contains `ok: true`. 4. **Refresh Traffic** Open **Traffic** and choose **Refresh**. The reporting window must include your visit. > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). ## What the script records Pageviews, single-page-app navigation, time on page, sources and campaign tags, devices, available location data and outbound clicks. Not tracked by default: - Automated browsers (for example headless or test automation). - Pages loaded inside an iframe, unless you add `data-debug`. - `localhost` and other local development hostnames, unless you add `data-allow-localhost`. - Browsers you excluded (see [Exclude your visits](https://metricraven.com/docs/exclude-visits)). A visit can also be excluded by your site settings or blocked by a browser extension. ## Optional attributes Add these to the same script tag. Don't install a second tracker. | Attribute | Effect | | --- | --- | | data-domain="example.com" | Share a persistent visitor cookie across this domain and its subdomains after storage consent. | | data-api="/a/event" | Send events through your own server. See [Use your own domain](https://metricraven.com/docs/proxy). | | data-auto-pageviews="false" | Disable automatic pageviews and call `analytics.pageview(path)` yourself. Other event tracking stays on. See [SPA & virtual pages](https://metricraven.com/docs/spa-virtual-pages). | | data-track-hash="true" | Opt in to #/... and #!/... router paths. Hash query parameters and ordinary anchors or OAuth fragments are omitted. | | data-track-hash="all" | Also count plain anchors such as `#pricing` as pages. Token-like fragments are never recorded. See [SPA & virtual pages](https://metricraven.com/docs/spa-virtual-pages#anchors). | | data-allowed-hostnames="app.example.io" | Keep the same visitor when they follow a link to another of your domains (comma-separated). See [Cross-domain tracking](https://metricraven.com/docs/cross-domain). | | data-debug | Log to the browser console what is sent and why anything is skipped, and also track inside iframes. Add `?metricraven_debug=1` to a page URL for the logging alone. See [Debug mode](https://metricraven.com/docs/troubleshooting#debug-mode). | | data-strip-query="true" | Omit URL query strings from transport. Recognized hash routes still work when enabled; UTM and referral fields remain separate. | | data-allow-localhost | Opt in to tracking local development. Add the local hostname to this site's allowed domains too. | ## Details - The script reads its settings from its own ` ``` 2. **Call setConsent from your banner** Once the tracker is loaded, wire up the banner's accept and withdraw handlers: ```javascript // Call these from your cookie banner's accept / withdraw handlers. window.analytics?.setConsent(true); window.analytics?.setConsent(false); ``` > **Verify it works** > > After accepting, `window.analytics.getVisitorId()` keeps returning the same ID across days in that browser. After withdrawing, the stored ID is removed. ## Details - These calls only control MetricRaven's visitor storage. They don't configure your cookie banner or other scripts. - If the tracker isn't loaded yet, `setConsent` isn't available; set `window.analyticsConsent = true` before loading it instead. - With consent, the identifier is kept in a first-party cookie and `localStorage`. Add `data-domain` to share the cookie across subdomains. - Linking separate domains with `data-allowed-hostnames` follows the same rules: before consent nothing is stored on either side and the join lasts until the end of the UTC day; with consent on the receiving domain, it continues with the visitor's ID. See [Cross-domain tracking](https://metricraven.com/docs/cross-domain#privacy). --- # Change or cancel your plan > Upgrade right away with a prorated charge, downgrade at the end of your billing period, cancel and resume, switch between monthly and yearly, and find your invoices. Source: https://metricraven.com/docs/change-plan Markdown: https://metricraven.com/docs/change-plan.md Section: Get started Your plan lives in **Settings → Billing & usage**. Only the account owner sees it: the person whose plan pays for the websites. Upgrades apply right away; downgrades and cancellations wait until the period you already paid for ends, so you never lose time you paid for. ## At a glance | Change | When it applies | What you pay | | --- | --- | --- | | Higher plan (Starter → Growth) | Right away | The prorated difference for the rest of this period, charged now | | Higher traffic tier | Right away | The prorated difference, charged now | | Monthly → yearly | Right away | The yearly price for the rest of your billing year, minus the unused part of this month | | Lower plan (Growth → Starter) | At the end of this period | Nothing now; the lower price from the next period | | Lower traffic tier | At the end of this period | Nothing now; the lower price from the next period | | Yearly → monthly | At the end of your yearly period | Nothing now; monthly from then | | Cancel | At the end of this period | Nothing more | A change that lowers anything counts as a downgrade, even if it raises something else. For example, Growth with 100k events to Starter with 1M events lowers the plan, so it waits for the period end. To get the bigger tier now, upgrade the tier first and change the plan later. ## Upgrade 1. **Choose the new plan** In Settings → Billing & usage, pick the plan, traffic tier and billing interval, then select **Review change** or **Switch to** on the plan card. 2. **Check the amount** You see the new price and the exact amount due now: the difference between the two plans for the days left in this period. Coupons on your subscription are applied to it. 3. **Confirm** The card on file is charged straight away. The new limits apply as soon as the payment goes through. If the payment is declined or needs extra authentication, nothing changes: you stay on your current plan and nothing is charged. Update your card under **Invoices and payment** and try again. > **Note:** A preview is valid for 15 minutes. If your plan changes in the meantime, for example from another browser tab, the confirm is refused and you're asked to review the change again. Confirming twice never charges twice. ## Downgrade Choose the lower plan or tier the same way. Instead of an amount, you see the date it takes effect: the end of your current billing period. Nothing is charged or credited now. Until that date you keep your current plan and all of its limits; from then on you pay the new price and get the new limits. While a downgrade is scheduled, the Plan card shows **Changes to Starter (100k) on** the date, with **Cancel scheduled change** to stay on your current plan. | If you then… | What happens | | --- | --- | | Choose another downgrade | It replaces the scheduled one. Only one change is scheduled at a time. | | Upgrade | The scheduled downgrade is dropped and the upgrade applies right away, with its prorated charge. | | Cancel your plan | The scheduled downgrade is dropped; the plan ends at the period end. | | Select Cancel scheduled change | You stay on your current plan and it renews as usual. | ## Monthly and yearly Yearly costs ten monthly payments. Switching from monthly to yearly is an upgrade and applies right away. Your billing year runs from the day your subscription started, so you pay the yearly price for the rest of that year, minus the unused part of your current month. Switching from yearly to monthly is a downgrade: your yearly period runs to its end, then monthly billing starts. On either interval, your event allowance resets every month on the same day and time, so a yearly plan includes the monthly allowance each month rather than twelve months to use at once. Switching interval doesn't move your reset date. ## Cancel and resume Select **Cancel plan** on the Plan card and confirm, or cancel from **Invoices and payment** (Stripe's billing page). Either way the plan is cancelled at the end of the period you paid for: the Plan card shows **Cancels on** the date, and nothing more is charged. Until that date you can select **Resume plan** and it renews as if nothing happened. While a cancellation is scheduled you can't change plans; resume first. After the date, choose a plan again to start a new subscription. ## What happens to your data and limits | Situation | Tracking and limits | | --- | --- | | Downgrade scheduled | Your current limits stay until the change date. | | After a downgrade | The new event allowance applies from the next usage cycle. Websites and team members you already have are kept; you can't add more than the new plan allows. | | Cancellation scheduled | Everything keeps working until the cancel date. | | After the plan ends | Tracking pauses. Your data, reports, share links and embeds stay available, and choosing a plan resumes tracking. | | A renewal payment fails | Stripe retries the card. Your limits stay for up to 14 days from the renewal; after that tracking pauses until the invoice is paid. Paying it restores your plan right away. | Nothing is deleted when you downgrade or cancel. See [pricing](https://metricraven.com/pricing) for what each plan includes. ## Invoices and payment method Select **Invoices and payment** on the Plan card to open Stripe's billing page. There you can download every invoice and receipt, including the prorated invoices from upgrades, and change the card you pay with. ## Details - The free trial is granted once, when you create your account, and doesn't need a card. Cancelling a plan during the trial leaves you with what is left of that trial; subscribing again never starts a new one. - Prorations are calculated by Stripe to the second, from the moment you confirm. - Downgrades never create a credit: you keep what you paid for until the period ends instead. - The 10M+ tier isn't sold online. Contact us to move to it or away from it. - Only the account owner can change, cancel or resume a plan. Admins and viewers see usage on each website but not billing. --- # Installation guides > Step-by-step guides for adding MetricRaven to your framework, CMS, website builder or AI app builder. Source: https://metricraven.com/docs/install-guides Markdown: https://metricraven.com/docs/install-guides.md Section: Installation guides Every guide installs the same script tag, once per page, in the site-wide ``. Pick your platform for where to paste it. ```html title="The tag every guide uses" ``` ## Your own code - [Plain HTML and static site generators](https://metricraven.com/docs/install-html) - [Next.js](https://metricraven.com/docs/install-nextjs) - [React (Vite)](https://metricraven.com/docs/install-react) - [React Router](https://metricraven.com/docs/install-react-router) - [Vue and Nuxt](https://metricraven.com/docs/install-vue-nuxt) - [Astro](https://metricraven.com/docs/install-astro) - [Django](https://metricraven.com/docs/install-django) - [Laravel](https://metricraven.com/docs/install-laravel) ## Website builders and CMS - [WordPress](https://metricraven.com/docs/install-wordpress) - [Ghost](https://metricraven.com/docs/install-ghost) - [Shopify](https://metricraven.com/docs/install-shopify) - [Webflow](https://metricraven.com/docs/install-webflow) - [Framer](https://metricraven.com/docs/install-framer) - [Wix](https://metricraven.com/docs/install-wix) - [Squarespace](https://metricraven.com/docs/install-squarespace) - [Bubble](https://metricraven.com/docs/install-bubble) ## Course and creator platforms - [Kajabi](https://metricraven.com/docs/install-kajabi) - [Podia](https://metricraven.com/docs/install-podia) ## AI app builders - [Lovable](https://metricraven.com/docs/install-lovable) - [Bolt](https://metricraven.com/docs/install-bolt) - [v0](https://metricraven.com/docs/install-v0) - [Replit](https://metricraven.com/docs/install-replit) ## Tag managers - [Google Tag Manager](https://metricraven.com/docs/install-google-tag-manager) ## Packages and mobile apps - [NPM package (@metricraven/web)](https://metricraven.com/docs/npm-package) - [React Native and Expo](https://metricraven.com/docs/react-native) - [Node.js and server SDK (@metricraven/node)](https://metricraven.com/docs/node-sdk) ## Revenue on course and membership platforms These platforms charge through your Stripe account but can't pass the visitor ID to checkout. After installing, follow their revenue guide to credit sales to visits by the buyer's email: - [Attribute revenue with Podia](https://metricraven.com/docs/attribution-guide-with-podia) - [Attribute revenue with Kajabi](https://metricraven.com/docs/attribution-guide-with-kajabi) - [Attribute revenue with Ghost](https://metricraven.com/docs/attribution-guide-with-ghost) ## Something else? Any platform that lets you add code to the head of every page works: paste the tag there. See [Install & verify](https://metricraven.com/docs/install) for how to check it and for optional attributes. To serve the script from your own domain, see [Use your own domain](https://metricraven.com/docs/proxy). --- # Install on a plain HTML site > Add MetricRaven to hand-written HTML pages or a static site generator's base layout. Source: https://metricraven.com/docs/install-html Markdown: https://metricraven.com/docs/install-html.md Section: Installation guides Paste one script tag into the head of every page. 1. **Copy your snippet** In MetricRaven, open your site's **Settings → Install** card and copy the script tag. It already contains your site ID. 2. **Paste it before \** Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```html title="index.html" ``` Add it to every HTML file you want tracked. If your pages share a template, include or partial (for example a static site generator's base layout), paste it there once instead. 3. **Upload and open the page** Deploy the updated files and open a page on your live domain. > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). ## Notes - Pages opened from disk (`file://`) or on `localhost` are not tracked unless you add `data-allow-localhost`; test on your real domain. - The page's hostname must match one of the domains in your site's **Settings → General → Site**, or a subdomain of one. Otherwise the collection request is refused with `domain_not_allowed`. Optional script attributes are listed in [Install & verify](https://metricraven.com/docs/install). To exclude your own visits, see [Exclude your visits](https://metricraven.com/docs/exclude-visits). --- # Install on Next.js > Load the MetricRaven script with next/script in the App Router or Pages Router. Source: https://metricraven.com/docs/install-nextjs Markdown: https://metricraven.com/docs/install-nextjs.md Section: Installation guides Load the tracker once from your root layout with `next/script`. It follows client-side navigation on its own. ## App Router 1. **Add the script to app/layout.tsx** Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```typescript title="app/layout.tsx" // app/layout.tsx import Script from "next/script"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ``` `index.html` sits in your project root. Vite copies the tag into the production build unchanged. 3. **Build, deploy and open your app** The Vite dev server runs on `localhost`, which is not tracked by default; check the deployed app. > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). ## Notes - React Router and similar routers change pages with the history API. The tracker records those navigations automatically. - Avoid adding the script from a `useEffect` or component: a single tag in `index.html` loads it exactly once. - Using a hash router (`/#/pricing`)? Add `data-track-hash="true"`. See [SPA & virtual pages](https://metricraven.com/docs/spa-virtual-pages). - Track events from components with `window.analytics.track()`; see [Goals & funnels](https://metricraven.com/docs/goals-funnels). Optional script attributes are listed in [Install & verify](https://metricraven.com/docs/install). To exclude your own visits, see [Exclude your visits](https://metricraven.com/docs/exclude-visits). --- # Install on React Router > Add MetricRaven to a React Router v7 app in framework mode (server or SPA) or library mode. Source: https://metricraven.com/docs/install-react-router Markdown: https://metricraven.com/docs/install-react-router.md Section: Installation guides In framework mode, the tag goes in the `Layout` export of `app/root.tsx`. That works with server rendering, prerendering and SPA mode (`ssr: false`). ## Framework mode 1. **Copy your snippet** In MetricRaven, open your site's **Settings → Install** card and copy the script tag. It already contains your site ID. 2. **Add the script to the Layout head** Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```typescript title="app/root.tsx" // app/root.tsx import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router"; export function Layout({ children }: { children: React.ReactNode }) { return ( ``` 2. **Build, deploy and open your app** Check the deployed app; `localhost` is not tracked by default. ## Nuxt 1. **Add the script to app.head in nuxt.config.ts** Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```typescript title="nuxt.config.ts" // nuxt.config.ts export default defineNuxtConfig({ app: { head: { script: [ { src: "https://metricraven.com/js/script.js", defer: true, "data-site-id": "YOUR_SITE_ID", }, ], }, }, }); ``` 2. **Deploy and open your site** Navigate between pages to confirm each route is recorded. > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). ## Notes - Vue Router's history mode uses the browser history API, which the tracker records automatically. - Hash mode (`createWebHashHistory`, URLs like `/#/pricing`) needs `data-track-hash="true"` on the script. See [SPA & virtual pages](https://metricraven.com/docs/spa-virtual-pages). - Add the script globally only, not from individual pages or components. Optional script attributes are listed in [Install & verify](https://metricraven.com/docs/install). To exclude your own visits, see [Exclude your visits](https://metricraven.com/docs/exclude-visits). --- # Install on Astro > Add MetricRaven to the shared layout of an Astro site. Source: https://metricraven.com/docs/install-astro Markdown: https://metricraven.com/docs/install-astro.md Section: Installation guides Put the tag in the layout every page uses. Astro copies it into each page's ``, on static and server-rendered sites alike. 1. **Copy your snippet** In MetricRaven, open your site's **Settings → Install** card and copy the script tag. It already contains your site ID. 2. **Add it to your base layout** Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```xml title="src/layouts/Layout.astro" --- // src/layouts/Layout.astro const { title } = Astro.props; --- {title} ``` `is:inline` tells Astro to output the tag as written instead of bundling it, which keeps the `data-*` attributes the tracker reads. 3. **Build, deploy and open your site** `astro dev` runs on `localhost`, which is not tracked by default; check the deployed site. > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). ## Notes - Have more than one layout? Add the tag to each, or to a shared head component they all include. - With view transitions (``), navigations use the history API, which the tracker records. - Click between a few pages and check the Network panel shows one `/api/v1/collect` pageview per navigation. Two per click means the tag is loaded twice. - To serve the script from your own domain, see the [Astro proxy](https://metricraven.com/docs/proxy-astro). Optional script attributes are listed in [Install & verify](https://metricraven.com/docs/install). To exclude your own visits, see [Exclude your visits](https://metricraven.com/docs/exclude-visits). --- # Install on Django > Add MetricRaven to the base template your Django pages extend. Source: https://metricraven.com/docs/install-django Markdown: https://metricraven.com/docs/install-django.md Section: Installation guides Add the tag once to the base template that your other templates extend with `{% extends "base.html" %}`. 1. **Copy your snippet** In MetricRaven, open your site's **Settings → Install** card and copy the script tag. It already contains your site ID. 2. **Paste it into the head of your base template** Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```django title="templates/base.html" {# templates/base.html #} {% block title %}My site{% endblock %} {% block extra_head %}{% endblock %} {% block content %}{% endblock %} ``` 3. **Deploy and open your site** `runserver` on `localhost` is not tracked by default, so check your deployed site. > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). ## Optional: read the site ID from settings Useful if staging and production report to different MetricRaven sites. The site ID is public, so this is about convenience, not secrecy. ```python title="settings.py and context_processors.py" # settings.py · optional: keep the site ID out of templates METRICRAVEN_SITE_ID = os.environ.get("METRICRAVEN_SITE_ID", "") # context_processors.py in one of your apps from django.conf import settings def metricraven(request): return {"METRICRAVEN_SITE_ID": settings.METRICRAVEN_SITE_ID} # Then add "yourapp.context_processors.metricraven" to # TEMPLATES[0]["OPTIONS"]["context_processors"] and in base.html use: # data-site-id="{{ METRICRAVEN_SITE_ID }}" ``` ## Notes - Templates that don't extend the base template (for example a standalone landing page) need the tag too. - If you set a Content Security Policy (for example with django-csp), allow `https://metricraven.com` in `script-src` and `connect-src`. - Django admin pages use their own templates and are not tracked, which is usually what you want. - To serve the script from your own domain, see the proxy recipes for [Nginx](https://metricraven.com/docs/proxy-nginx) or [Caddy](https://metricraven.com/docs/proxy-caddy), which usually sit in front of Django. Optional script attributes are listed in [Install & verify](https://metricraven.com/docs/install). To exclude your own visits, see [Exclude your visits](https://metricraven.com/docs/exclude-visits). --- # Install on Laravel > Add MetricRaven to your Laravel Blade layout, including Inertia and Livewire apps. Source: https://metricraven.com/docs/install-laravel Markdown: https://metricraven.com/docs/install-laravel.md Section: Installation guides Add the tag to the Blade layout that renders your pages' ``. 1. **Copy your snippet** In MetricRaven, open your site's **Settings → Install** card and copy the script tag. It already contains your site ID. 2. **Paste it into your layout's head** Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```php title="resources/views/layouts/app.blade.php" {{-- resources/views/layouts/app.blade.php --}} {{ config('app.name') }} @vite(['resources/css/app.css', 'resources/js/app.js']) {{-- ...your existing body... --}} ``` Which file that is depends on your setup: - **Inertia (React, Vue or Svelte):** the root template, usually `resources/views/app.blade.php`. - **Livewire or Blade:** the layout your pages use, such as `resources/views/layouts/app.blade.php` or a `components/layouts` file. Add it to guest layouts too if you track signed-out pages. 3. **Deploy and open your site** `php artisan serve` and `*.test` hosts from Valet or Herd are local and not tracked by default; check the deployed site. > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). ## Optional: read the site ID from config Handy when staging and production report to different MetricRaven sites. ```php title="config/services.php" // config/services.php · optional 'metricraven' => [ 'site_id' => env('METRICRAVEN_SITE_ID'), ], {{-- In the layout --}} data-site-id="{{ config('services.metricraven.site_id') }}" ``` ## Notes - Inertia links and Livewire's `wire:navigate` change pages with the history API, which the tracker records. - Click between a few pages and check the Network panel shows one `/api/v1/collect` pageview per navigation. Two per click means the tag is loaded twice. - To serve the script from your own domain, see the [Laravel proxy](https://metricraven.com/docs/proxy-laravel). Optional script attributes are listed in [Install & verify](https://metricraven.com/docs/install). To exclude your own visits, see [Exclude your visits](https://metricraven.com/docs/exclude-visits). --- # Install on WordPress > Install the MetricRaven WordPress plugin: paste your site ID, exclude admins, connect your consent banner and attribute WooCommerce orders. Source: https://metricraven.com/docs/install-wordpress Markdown: https://metricraven.com/docs/install-wordpress.md Section: Installation guides The MetricRaven plugin adds the tracker to every page, leaves out your own logged-in visits and saves the visitor ID on WooCommerce orders. No theme edits. ## Install the plugin > **Note:** The plugin is waiting for its WordPress.org listing. Until it appears in the plugin directory, upload the zip as shown in step 1. 1. **Upload the plugin** Download [metricraven-wordpress.zip](https://metricraven.com/downloads/metricraven-wordpress.zip). In WordPress, open **Plugins → Add New → Upload Plugin**, choose the zip, select **Install Now** and then **Activate**. Once it is listed, you can instead search for **MetricRaven** under **Plugins → Add New**. 2. **Paste your site ID** Open **Settings → MetricRaven**, paste the site ID from your site's **Settings → Tracking & install** card in MetricRaven (it starts with `ana_pub_`) and save. 3. **Purge caches** If you use a page-cache plugin or host-level cache, purge it so visitors get the updated HTML. > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). ## Plugin settings | Setting | What it does | | --- | --- | | Site ID | Your public site ID. Nothing is loaded until it is set. | | Root domain | Optional. Shares the visitor ID across subdomains (sets `data-domain`), e.g. `example.com` for a shop on `shop.example.com`. | | Load the script from | **MetricRaven** (default); **This WordPress site**, a built-in proxy that serves the script and events from your own domain at `/?mrp=script` and `/?mrp=event` so fewer visits are lost to blockers; or **a custom or managed proxy host**, with your script URL. For the [managed proxy](https://metricraven.com/docs/managed-proxy), leave the `data-api` endpoint empty: events go to the script's own host. Fill it in only for a self-hosted proxy. | | Do not track | Logged-in roles to leave out. Administrators are excluded by default. | | Consent | **Cookieless until consent** (default): nothing is stored in the browser. **Follow the WP Consent API**: banners that support it (Complianz, CookieYes and others) turn on a persistent visitor ID when the visitor allows **statistics**. **Always keep a visitor ID**: only where analytics cookies need no consent. | | Hash routes | Counts `#/` router paths as pages (`data-track-hash`). | | Revenue attribution | With WooCommerce active, saves the visitor ID on each new order as hidden `_metricraven_visitor_id` meta. | ## WooCommerce revenue Keep **Revenue attribution** on, then add the MetricRaven webhook as described in [Connect WooCommerce](https://metricraven.com/docs/woocommerce). The plugin passes the visitor ID to the order through a hidden checkout field and the WooCommerce session the cart already uses, so it works on the classic and block checkouts without an extra cookie. You no longer need the must-use attribution snippet on that page; the plugin also reads the cookie the snippet set, so orders keep attributing while you switch. ## Content-Security-Policy If your site sends a CSP header with a script nonce, pass it to the plugin. Nonces that security plugins add through WordPress's `wp_inline_script_attributes` filter are kept automatically. ```php title="functions.php" ` on all pages, paste the snippet into its header field and save. ```html title="Header code" ``` 3. **Or use the wp_head hook** A child theme (or a small site-specific plugin) keeps the code when the parent theme updates. ```php title="functions.php" Add MetricRaven to a Ghost site with Code injection, or in your theme. Source: https://metricraven.com/docs/install-ghost Markdown: https://metricraven.com/docs/install-ghost.md Section: Installation guides Ghost's site-wide Code injection adds the tag to the head of every page and post. 1. **Open Settings → Advanced → Code injection** In Ghost Admin, choose the gear icon, then find **Code injection** under **Advanced**. 2. **Paste the snippet into Site header** Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```html title="Site header" ``` Use **Site header**, not **Site footer** or the code injection on a single post. 3. **Save and open your site** Open a page on your public domain in a private window. > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). ## Alternative: add it to your theme On a self-hosted Ghost, or a plan that allows custom themes, you can put the tag in your theme's `default.hbs` instead, before `{{ghost_head}}`. Re-add it when you switch or update the theme. ```xml title="default.hbs" {{!-- default.hbs in your theme --}} ... {{ghost_head}} ``` ## Notes - Only hostnames listed in your site's **Settings → General → Site** domains (and their subdomains) are accepted. To also count visits on a platform address such as `your-site.ghost.io`, add it there; otherwise those requests are refused with `domain_not_allowed`. - Your own visits to the public site count like anyone else's. To leave them out, see [Exclude your visits](https://metricraven.com/docs/exclude-visits). - Pair it with `data-goal` attributes on your signup buttons to count member signups as goals. - If you use a cookie banner, connect it with [Consent & visitor IDs](https://metricraven.com/docs/consent). ## Attribute sales To credit Ghost sales to the visits that brought the buyer, connect Stripe and follow [Attribute revenue with Ghost](https://metricraven.com/docs/attribution-guide-with-ghost). Optional script attributes are listed in [Install & verify](https://metricraven.com/docs/install). To exclude your own visits, see [Exclude your visits](https://metricraven.com/docs/exclude-visits). --- # Install on Shopify > Add MetricRaven to your Shopify theme's theme.liquid layout. Source: https://metricraven.com/docs/install-shopify Markdown: https://metricraven.com/docs/install-shopify.md Section: Installation guides Paste the script into your theme layout so every storefront page loads it. 1. **Copy your snippet** In MetricRaven, open your site's **Settings → Install** card and copy the script tag. It already contains your site ID. 2. **Open your theme code** In Shopify admin, open **Online Store → Themes**, choose the menu next to your live theme and select **Edit code**. 3. **Paste it into layout/theme.liquid before \** Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```html title="layout/theme.liquid" ``` 4. **Save and open your store** Open a storefront page on your store's domain. > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). ## Notes - The snippet runs where your theme's layout is used. Checkout and other pages that don't render `theme.liquid` are not covered by it. - The code lives in that theme. If you publish a different theme, add the script there too. - Only hostnames listed in your site's **Settings → General → Site** domains (and their subdomains) are accepted. To also count visits on a platform address such as `your-store.myshopify.com`, add it there; otherwise those requests are refused with `domain_not_allowed`. - Shopify orders are not imported automatically. You can send payments from your own backend with the [Payments API](https://metricraven.com/docs/payments-api). - If you use a cookie banner, connect it with [Consent & visitor IDs](https://metricraven.com/docs/consent). Optional script attributes are listed in [Install & verify](https://metricraven.com/docs/install). To exclude your own visits, see [Exclude your visits](https://metricraven.com/docs/exclude-visits). --- # NPM package (@metricraven/web) > Install MetricRaven from npm in Next.js, React, Vue or Svelte, with typed track, identify, pageview and consent calls. Source: https://metricraven.com/docs/npm-package Markdown: https://metricraven.com/docs/npm-package.md Section: Installation guides `@metricraven/web` loads the MetricRaven tracker for you and gives you typed functions to call it. It has no dependencies and is safe to import during server rendering. The package loads the same hosted script as the HTML tag, with the options you pass. So consent rules, single-page app routing and the data it sends are the same either way. Use the package or the tag, not both. If the tag is already on the page, the package uses it instead of loading a second copy. ## Install 1. **Add the package** ```bash npm install @metricraven/web ``` 2. **Call init once, in the browser** Call `init` from client code that runs once, such as a root layout effect. Calling it again does nothing. On the server, every function does nothing. ```typescript title="Next.js App Router" // app/analytics.tsx "use client"; import { useEffect } from "react"; import { init } from "@metricraven/web"; export function Analytics() { useEffect(() => { init({ siteId: process.env.NEXT_PUBLIC_METRICRAVEN_SITE_ID! }); }, []); return null; } // app/layout.tsx: render once inside . ``` 3. **Track events and identify users** You can import these anywhere, even before `init`. Calls made before the script loads are queued and sent in order. ```typescript import { track, identify, pageview, setConsent, reset, getVisitorId } from "@metricraven/web"; track("checkout_started", { plan: "pro" }); identify(user.id, { name: user.name, email: user.email, plan: "pro" }); pageview("/checkout/payment"); // a virtual screen; with autoPageviews: false, every view setConsent(true); // from your cookie banner reset(); // on logout const visitorId = getVisitorId(); // null until the first response ``` > **Verify it works** > > Deploy and open your site. Within a few seconds, the visit appears in **Realtime**. On `localhost`, pass `allowLocalhost: true` to test locally. ## Options ```typescript init({ siteId: "YOUR_SITE_ID", apiHost: "https://metricraven.com", // where MetricRaven is served scriptUrl: "/a/script", // or: a same-origin proxy path for the script… endpoint: "/a/event", // …and for events autoPageviews: true, // false: call pageview() yourself hashRouting: false, // true: count #/route paths; "all": also #anchors stripQuery: false, consent: false, // true when your banner already has consent domain: "example.com", // share the consented cookie across subdomains allowedHostnames: ["app.example.io"], // your other domains: keep the same visitor across them allowLocalhost: false, debug: false, // true: log what is sent to the console }); ``` | Option | What it does | | --- | --- | | siteId | Required. Your site ID from Settings. | | apiHost | Where MetricRaven is served. Defaults to https://metricraven.com. | | scriptUrl / endpoint | Same-origin proxy paths for the script and for events. See the proxy guide. | | autoPageviews | Defaults to true: records the first page and history navigation. Set false to call pageview() yourself. | | hashRouting | true counts #/route and #!/route as paths; "all" also counts plain #anchors such as #pricing. | | stripQuery | Drops query strings from recorded URLs. | | consent | Set true when your cookie banner already has stored consent. Otherwise nothing is stored in the browser. | | domain | Shares the consented visitor cookie across a domain and its subdomains. | | allowLocalhost | Also tracks on localhost and \*.test. | | nonce | CSP nonce for the injected script tag. | | allowedHostnames | Your site's other domains, e.g. ["app.example.io"]. Links and forms to them keep the same visitor (cross-domain tracking). | | debug | Logs to the console what is sent and why anything is skipped. | ## Functions | Function | Notes | | --- | --- | | init(options) | Loads the tracker once and returns the client. Does nothing on the server. | | track(name, props?) | A custom goal. Names start with a letter. Up to 10 properties. | | identify(userId, traits?) | Links this visitor to your user ID, with optional name, email, image (https profile picture) and custom traits. | | payment({ email }) | On the page after a successful payment: this visitor paid with this email. A provider payment with that customer email, within an hour and linked to nothing else, is credited to this visit. See Attribute by email. | | pageview(path?) | A manual view: the current URL, or a same-origin virtual path such as /checkout/payment. | | setConsent(granted) | Grants or withdraws analytics storage consent from your cookie banner. | | reset() | On logout. Forgets identify state and, with consent, starts a new visitor ID. | | getVisitorId() | The current visitor ID. Returns null until the first response, and on the server. | | linkUrl(url) | The URL with a short-lived cross-domain token added when it leads to one of allowedHostnames; otherwise unchanged. For navigation in code. | | linkParams() | The cross-domain parameters ({ mr_link }) to append yourself, or {} without a fresh token. | | onVisitorId(listener) | Called when the visitor ID changes. Returns an unsubscribe function. | | ready() | Resolves once the script has loaded, or failed to load (for example, blocked). | ## Other frameworks ```typescript title="Vue / Nuxt" // main.ts (Vue / Nuxt client plugin) import { init } from "@metricraven/web"; init({ siteId: import.meta.env.VITE_METRICRAVEN_SITE_ID }); ``` ```html title="SvelteKit" ``` - React (Vite): call `init` in `main.tsx` before rendering. - Routers that use the history API are tracked automatically. For hash routers, pass `hashRouting: true`. See [SPA & virtual pages](https://metricraven.com/docs/spa-virtual-pages). - Landing site and app on different domains? Pass `allowedHostnames` and use `linkUrl` for navigation in code. See [Cross-domain tracking](https://metricraven.com/docs/cross-domain). ## Details - The package is ESM and CommonJS, with TypeScript types. Nothing runs when you import it, so it's tree-shakeable and safe in server components and route handlers. Tracking itself only happens in the browser. - If an ad blocker or CSP blocks the script, calls keep queueing quietly and never throw. To avoid blockers, see [Proxy](https://metricraven.com/docs/proxy). - For identify rules and limits, see [Link users to visits](https://metricraven.com/docs/identify-users). For consent, see [Consent & visitor IDs](https://metricraven.com/docs/consent). --- # Install on Webflow > Add MetricRaven through Webflow's site-wide custom code head setting. Source: https://metricraven.com/docs/install-webflow Markdown: https://metricraven.com/docs/install-webflow.md Section: Installation guides Use the site-wide head code field, then publish. 1. **Copy your snippet** In MetricRaven, open your site's **Settings → Install** card and copy the script tag. It already contains your site ID. 2. **Open Site settings → Custom code** Find the **Head code** field. Webflow may require a paid site plan for custom code. 3. **Paste the snippet into Head code and save** Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```html title="Head code" ``` 4. **Publish your site** Custom code runs on the published site, not in the Designer canvas. > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). ## Notes - Use the site-wide head code setting, not an embed or HTML widget: widgets that place code inside an iframe are ignored by the tracker. - Only hostnames listed in your site's **Settings → General → Site** domains (and their subdomains) are accepted. To also count visits on a platform address such as `your-site.webflow.io`, add it there; otherwise those requests are refused with `domain_not_allowed`. - If you use a cookie banner, connect it with [Consent & visitor IDs](https://metricraven.com/docs/consent). Optional script attributes are listed in [Install & verify](https://metricraven.com/docs/install). To exclude your own visits, see [Exclude your visits](https://metricraven.com/docs/exclude-visits). --- # React Native & Expo > Track screens, events and signed-in users in React Native and Expo apps, with an offline queue and consent. Source: https://metricraven.com/docs/react-native Markdown: https://metricraven.com/docs/react-native.md Section: Installation guides `@metricraven/react-native` records screen views, goals and identified users from your app. Events wait in a queue while the device is offline and are sent when it reconnects. ## Set up 1. **Allow your app in Settings** In your site's **Settings**, add your bundle ID (iOS) or package name (Android) under **Mobile apps**, for example `com.example.app`. App events from IDs that aren't listed are rejected. 2. **Install the SDK** AsyncStorage is optional, but without it queued events are lost if the app is closed while offline. ```bash npx expo install @metricraven/react-native @react-native-async-storage/async-storage ``` 3. **Create the client once** ```typescript title="analytics.ts" // analytics.ts import AsyncStorage from "@react-native-async-storage/async-storage"; import * as Application from "expo-application"; import { createMetricRaven } from "@metricraven/react-native"; export const analytics = createMetricRaven({ siteId: process.env.EXPO_PUBLIC_METRICRAVEN_SITE_ID!, appId: Application.applicationId ?? "com.example.app", // listed under Settings → Mobile apps appVersion: Application.nativeApplicationVersion ?? undefined, appBuild: Application.nativeBuildVersion ?? undefined, storage: AsyncStorage, // keeps the offline queue (and, with consent, the visitor ID) disabled: __DEV__, }); ``` 4. **Track screens** With Expo Router, track the route pattern from your root layout: ```typescript title="app/_layout.tsx" // app/_layout.tsx import { Slot, useSegments } from "expo-router"; import { MetricRavenProvider, useExpoRouterTracking } from "@metricraven/react-native"; import { analytics } from "../analytics"; function ScreenTracking() { useExpoRouterTracking(useSegments()); // "/users/[id]", never the real ID return null; } export default function RootLayout() { return ( ); } ``` With React Navigation, pass the helpers to your container. It records route names, not params: ```typescript title="App.tsx" // App.tsx import { NavigationContainer, useNavigationContainerRef } from "@react-navigation/native"; import { MetricRavenProvider, trackNavigation } from "@metricraven/react-native"; import { analytics } from "./analytics"; export default function App() { const navigationRef = useNavigationContainerRef(); return ( {/* your navigators */} ); } ``` 5. **Send events and identify users** ```typescript analytics.track("cta_click", { source: "home" }); analytics.screen("Paywall"); // a screen view outside your navigator analytics.identify(user.id, { name: user.name, email: user.email, plan: "pro" }); analytics.reset(); // on logout analytics.setConsent(true); // from your consent prompt analytics.openedFromUrl(url); // utm_* from a deep link, for the next screen view // In components: const analytics = useMetricRaven(); useScreen("Settings"); ``` > **Verify it works** > > Open the app on a device or simulator with `disabled` off. The screen appears in **Realtime**. Screens are listed under **Pages** as paths like `/Home` or `/users/[id]`, and your app ID appears under **Hostnames**. ## Client | Call | Notes | | --- | --- | | screen(name, props?) | A screen view. Names like "Settings" become the path /Settings. | | track(name, props?) | A custom goal, attributed to the current screen. Up to 10 properties. | | identify(userId, traits?) | Links this device's visitor to your user ID, with optional name, email and up to 10 custom traits. | | reset() | On logout. Forgets identify state and, with consent, starts a new visitor ID. | | setConsent(granted) | Grants or withdraws storage consent. Withdrawing also removes the ID from queued events. | | openedFromUrl(url) | Reads utm_\* parameters from a deep link and attaches them to the next screen view. | | flush() | Sends the queue now. Runs automatically when the app returns to the foreground or goes to the background. | | getVisitorId() / onVisitorId() | The current visitor ID, and a listener for changes. | | Option | What it does | | --- | --- | | siteId, appId | Required. appId must be listed under Mobile apps. | | appVersion, appBuild | Shown on events and visitor profiles. Read them from expo-application or react-native-device-info. | | storage | AsyncStorage. Persists the queue and, with consent, the visitor ID. | | consent | Set true when the user has already allowed analytics storage. | | apiHost / endpoint | Where events go. Defaults to https://metricraven.com/api/v1/collect. | | maxQueueSize, maxEventAgeMs | Defaults to 500 events and 7 days. Older or extra events are dropped. | | disabled | Turns tracking off, for example in development. | ## Consent and visitor IDs The app follows the same rules as the website tracker. Until you call `setConsent(true)` (or pass `consent: true`), nothing that identifies the device is stored. The server derives a daily visitor ID from the network address and the app's user agent. Raw IP addresses are not kept. With consent, a random ID is kept in `storage`, so sessions on different days connect. Granting consent also relabels today's events from that device. > **Note:** Mobile networks change address often, so without consent one person can appear as several visitors in a day. Ask for consent if returning-user numbers matter to you. ## Offline queue and retries Events are sent one at a time, in order. Network errors, timeouts, rate limits and server errors are retried with growing waits, up to 5 minutes, and `Retry-After` is respected. Rejected events are dropped instead of retried. That covers invalid data, an app ID that isn't listed, an unknown site, or paused tracking. Events replayed more than 5 minutes after they happened are recorded at the time they arrive. ## Details - App traffic is identified by your app ID because native requests have no web page or browser origin. A browser can't use this to send data as your app: browser requests always carry an origin, and those are checked against your domains. - Like the website tag, the site ID and app ID aren't secrets. The allowlist keeps out misconfigured and casual traffic, not a determined forger. For verified events, use [server events](https://metricraven.com/docs/server-events) or [server identify](https://metricraven.com/docs/identify-users). - Expo Router: use `useSegments()` rather than `usePathname()`, so IDs in URLs (like `/users/42`) don't show up in reports. - Purchases inside the app: see [RevenueCat](https://metricraven.com/docs/revenuecat) and send the same user ID you pass to `identify`. - `@metricraven/react-native/core` exports the client without React or react-native imports, for tests and other JavaScript runtimes. --- # Install on Framer > Add MetricRaven through Framer's site settings custom code, then publish. Source: https://metricraven.com/docs/install-framer Markdown: https://metricraven.com/docs/install-framer.md Section: Installation guides Paste the script into the site-wide head code, then publish. 1. **Copy your snippet** In MetricRaven, open your site's **Settings → Install** card and copy the script tag. It already contains your site ID. 2. **Open your site settings and find Custom Code** Framer may limit custom code to paid site plans. 3. **Paste it into the end of \ field** Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```html title="End of " ``` 4. **Publish your site** Custom code runs on the published site, not in the editor preview. > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). ## Notes - If your site switches pages without a full reload, the tracker still records each page through the browser history API. - Use the site-wide head code setting, not an embed or HTML widget: widgets that place code inside an iframe are ignored by the tracker. - Only hostnames listed in your site's **Settings → General → Site** domains (and their subdomains) are accepted. To also count visits on a platform address such as `your-site.framer.website`, add it there; otherwise those requests are refused with `domain_not_allowed`. - If you use a cookie banner, connect it with [Consent & visitor IDs](https://metricraven.com/docs/consent). Optional script attributes are listed in [Install & verify](https://metricraven.com/docs/install). To exclude your own visits, see [Exclude your visits](https://metricraven.com/docs/exclude-visits). --- # Node.js and server SDK (@metricraven/node) > Send goals, identify, payments, refunds and crawler hits from Node, Bun, Deno, Cloudflare Workers or Vercel Edge with typed calls. Source: https://metricraven.com/docs/node-sdk Markdown: https://metricraven.com/docs/node-sdk.md Section: Installation guides `@metricraven/node` sends what only your backend knows: confirmed sign-ups, verified identities, payments and refunds. It runs anywhere `fetch` does, including Node 18+, Bun, Deno, Cloudflare Workers and Vercel Edge, and it has no dependencies. It wraps the same secret-key endpoints as [server events](https://metricraven.com/docs/server-events), [identify](https://metricraven.com/docs/identify-users) and the [Payments API](https://metricraven.com/docs/payments-api), with types, timeouts and retries. Calls never throw into your request handler: each one returns a result. ## Install 1. **Add the package** ```bash npm install @metricraven/node ``` 2. **Set your secret key on the server** Copy the site secret key (`ana_sec_…`) from Settings and store it as `METRICRAVEN_SECRET_KEY`. Never ship it to the browser or a mobile app. 3. **Send an event after the action succeeds** ```typescript // lib/metricraven.ts — server only. Reads METRICRAVEN_SECRET_KEY. import { createMetricRaven, getVisitorId } from "@metricraven/node"; export const metricraven = createMetricRaven(); // In your sign-up route handler, after the account exists: await metricraven.track({ name: "signup", userId: user.id, visitorId: getVisitorId(request), // the tracker cookie, or null props: { plan: "free" }, }); ``` > **Verify it works** > > The event appears in the **Goal** tab at the bottom of Traffic after refreshing. If it doesn't, log `result.error` and check it against the table under **Results and errors** below. ## Calls | Call | What it sends | | --- | --- | | track({ name, userId?, visitorId?, props?, timestamp? }) | A goal (server event). Needs userId or visitorId. Up to 10 props. | | identify({ visitorId, userId, email?, name?, traits? }) | A verified link from a visitor to your user. It replaces any identity the browser claimed. | | payment({ amount, currency, transactionId, visitorId?, userId?, plan?, customerId?, renewal?, isFreeTrial?, email? }) | A payment in major currency units. Repeats of a transactionId are ignored. Without visitorId or userId, email matches a visitor identified with it or a browser that called payment({ email }); see Attribute by email. | | refund({ amount, currency, transactionId }) | A refund, with the cumulative amount refunded so far. | | crawl({ url, userAgent, status?, ip? }) | One crawler request. Uses the bot token when one is set. For middleware, use @metricraven/ai-crawl. | ```typescript title="Identify" await metricraven.identify({ visitorId, userId: user.id, email: user.email, name: user.name, traits: { plan: "pro" }, }); ``` ```typescript title="Payments and refunds" // After the payment succeeds. amount is in major units: 29 means $29. await metricraven.payment({ amount: 29, currency: "USD", transactionId: order.id, visitorId, userId: user.id, plan: "pro", }); // Refunds send the cumulative amount refunded so far. await metricraven.refund({ amount: 15, currency: "USD", transactionId: order.id }); ``` `track`, `identify`, `payment`, `refund` and `crawl` are also exported as functions that use a shared client configured from the environment, or from `configure(options)`. ## Bot token for crawler reports If a server only reports crawlers, give it the site's ingest-only [bot token](https://metricraven.com/docs/bot-traffic-settings#bot-token) (`mrbot_…`) as `METRICRAVEN_BOT_TOKEN` or `botToken`, instead of the secret key. It can only send crawl events. | Configured | What happens | | --- | --- | | Bot token only | crawl() uses it. track, identify, payment and refund return bot_token_cannot_track (…_identify, …_payment, …_refund) without a request. | | Bot token and secret key | crawl() uses the bot token; every other call uses the secret key. | | siteId (METRICRAVEN_SITE_ID) | Optional. Crawl reports are refused (wrong_site) if the token belongs to another website. | ## Attribute to the visit `getVisitorId(request)` reads the visitor ID from a Fetch `Request`, a Node or Express request, `Headers` or a raw `Cookie` header. It checks the tracker's `ana_vid` cookie, then the `metricraven_visitor_id` cookie that the checkout recipes set. It returns `null` when there's neither. > **Note:** The tracker only stores its cookie after the visitor gives storage consent. Without consent, send `window.analytics.getVisitorId()` from the browser with your own request, or rely on `userId` after identify. ## Results and errors ```typescript const result = await metricraven.track({ name: "signup", userId: user.id }); if (!result.ok) console.warn("MetricRaven:", result.error, result.status); ``` | error | Meaning | | --- | --- | | invalid_input | Rejected before sending, for example a bad event name, visitor ID or currency. | | missing_secret_key / invalid_secret_key | No key, or not an ana_sec_ key. Nothing was sent. | | invalid_bot_token | botToken isn't an mrbot_ token. Nothing was sent. | | bot_token_cannot_track (…_identify, …_payment, …_refund) | Only a bot token is set, and it can only report crawlers. Nothing was sent. | | unauthorized | The key wasn't accepted, for example after a rotation's grace period ended. | | invalid_payload | The API rejected the body. | | domain_not_allowed | The crawl URL's host isn't one of the site's domains. | | tracking_paused | The plan's usage limit was reached. | | bot_tracking_paused | crawl: the bot request allowance is used up until the reset. | | bot_token_required | crawl: the website only accepts crawler reports sent with its bot token. | | wrong_site | crawl: the bot token belongs to a different website than siteId. | | timeout / network_error / http_5xx | No usable response after retries. | ## Options | Option | What it does | | --- | --- | | secretKey | Defaults to METRICRAVEN_SECRET_KEY. | | botToken | Defaults to METRICRAVEN_BOT_TOKEN. Used by crawl() only. | | siteId | Defaults to METRICRAVEN_SITE_ID. Optional check of the bot token's website. | | apiHost | Defaults to METRICRAVEN_API_BASE, then https://metricraven.com. | | timeoutMs | Per attempt. Defaults to 5000. | | retries | Extra attempts after 5xx or network errors. Defaults to 2. 4xx responses are never retried. | | retryDelayMs | First backoff wait. Defaults to 300, and doubles each time with jitter. | | onError | Called with each failure: code, status and endpoint. Never includes the key or token. | | throwOnError | Rejects instead of returning { ok: false }. | | disabled | Skips the network, for tests and local development. | | fetch | A custom fetch. | ## Edge and serverless Don't hold the response while a call runs. Hand the promise to `waitUntil` (Workers `ctx`, Next.js `after()` or Vercel's `waitUntil`): ```typescript // Cloudflare Workers: don't hold the response for analytics. import { createMetricRaven } from "@metricraven/node"; export default { async fetch(request, env, ctx) { const metricraven = createMetricRaven({ secretKey: env.METRICRAVEN_SECRET_KEY }); ctx.waitUntil(metricraven.track({ name: "api_call", userId: "user_42" })); return new Response("ok"); }, }; ``` ## Details - After a timeout, payments and identify are retried, because the server deduplicates or overwrites them. Events and crawls aren't retried after a timeout, because they have no deduplication key. - The unscoped `metricraven` package exposes the same API at `metricraven/node`. - ESM and CommonJS, with TypeScript types. It uses only `fetch`, with no Node built-ins. --- # Install on Wix > Add MetricRaven to every page of a Wix site with Custom Code. Source: https://metricraven.com/docs/install-wix Markdown: https://metricraven.com/docs/install-wix.md Section: Installation guides Wix's Custom Code setting adds the tag to the head of every page. It works on a published site with a connected domain, which needs a Wix premium plan. 1. **Open Settings → Custom Code** In your site dashboard, open **Settings** and choose **Custom Code** (under Development & integrations). 2. **Add Custom Code** Choose **+ Add Custom Code** and paste the snippet. Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```html title="Custom Code" ``` 3. **Choose where it loads** Name it MetricRaven, apply it to **All pages**, choose **Load code once**, and set **Place Code in** to **Head**. The tracker records later page changes itself. Then choose **Apply**. 4. **Open your site on its connected domain** Check a page in a private window, not the editor preview. > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). ## Notes - Custom code belongs to your site's domain. If you change the primary domain, check the snippet is still listed. - Use the site-wide head code setting, not an embed or HTML widget: widgets that place code inside an iframe are ignored by the tracker. - Only hostnames listed in your site's **Settings → General → Site** domains (and their subdomains) are accepted. To also count visits on a platform address such as `your-site.wixsite.com`, add it there; otherwise those requests are refused with `domain_not_allowed`. - The editor and its previews load pages inside a frame, which the tracker ignores. - If you use a cookie banner, connect it with [Consent & visitor IDs](https://metricraven.com/docs/consent). Optional script attributes are listed in [Install & verify](https://metricraven.com/docs/install). To exclude your own visits, see [Exclude your visits](https://metricraven.com/docs/exclude-visits). --- # Install on Squarespace > Add MetricRaven to every page of a Squarespace site with Code Injection. Source: https://metricraven.com/docs/install-squarespace Markdown: https://metricraven.com/docs/install-squarespace.md Section: Installation guides Squarespace's Code Injection adds the tag to the head of every page. It's available on the Core, Plus and Advanced plans and some legacy plans. 1. **Open Code Injection** In your site's editor, search for **Code Injection** in the settings (it sits with the custom code and developer tools). 2. **Paste the snippet into Header and save** Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```html title="Header" ``` Use the site-wide **Header** field, not a single page's header code injection. 3. **Open your live site** Check a page on your public domain in a private window, logged out of Squarespace. > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). ## Notes - Squarespace runs injected code in the editor preview too, so your own editing can be counted. Exclude your browser; see [Exclude your visits](https://metricraven.com/docs/exclude-visits). - Squarespace checkout pages don't run custom code, so checkout steps aren't recorded. - Use the site-wide head code setting, not an embed or HTML widget: widgets that place code inside an iframe are ignored by the tracker. - Only hostnames listed in your site's **Settings → General → Site** domains (and their subdomains) are accepted. To also count visits on a platform address such as `your-site.squarespace.com`, add it there; otherwise those requests are refused with `domain_not_allowed`. - If you use a cookie banner, connect it with [Consent & visitor IDs](https://metricraven.com/docs/consent). Optional script attributes are listed in [Install & verify](https://metricraven.com/docs/install). To exclude your own visits, see [Exclude your visits](https://metricraven.com/docs/exclude-visits). --- # Install on Bubble > Add MetricRaven to every page of a Bubble app from the SEO / metatags settings. Source: https://metricraven.com/docs/install-bubble Markdown: https://metricraven.com/docs/install-bubble.md Section: Installation guides Bubble has an app-wide field for header scripts. Visits are tracked on your live app, which needs a paid Bubble plan. 1. **Open Settings → SEO / metatags** In the Bubble editor, open **Settings** in the left sidebar, then the **SEO / metatags** tab. 2. **Paste the snippet into the header scripts field** Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```html title="Script/meta tags in header" ``` Use the app-wide header field, not a page's own header or an HTML element on the page. 3. **Deploy to Live and open your app** Deploy your development version to live, then open the live app on your domain in a private window. > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). ## Notes - Bubble's Free plan has only a development version, without a live app or custom domain. Starter and higher plans can publish one. - Only hostnames listed in your site's **Settings → General → Site** domains (and their subdomains) are accepted. To also count visits on a platform address such as `your-app.bubbleapps.io`, add it there; otherwise those requests are refused with `domain_not_allowed`. - The development version (`/version-test` URLs) loads the same header code. Exclude your own browser while you build; see [Exclude your visits](https://metricraven.com/docs/exclude-visits). - Track workflows such as signups by running JavaScript (with a plugin like Toolbox) that calls `window.analytics.track("signup")`; see [Goals & funnels](https://metricraven.com/docs/goals-funnels). - Use the site-wide head code setting, not an embed or HTML widget: widgets that place code inside an iframe are ignored by the tracker. - If you use a cookie banner, connect it with [Consent & visitor IDs](https://metricraven.com/docs/consent). Optional script attributes are listed in [Install & verify](https://metricraven.com/docs/install). To exclude your own visits, see [Exclude your visits](https://metricraven.com/docs/exclude-visits). --- # Install on Kajabi > Add MetricRaven to Kajabi site pages with Page scripts and to checkout with the checkout tracking code. Source: https://metricraven.com/docs/install-kajabi Markdown: https://metricraven.com/docs/install-kajabi.md Section: Installation guides Kajabi has two places for scripts: one for your site pages, and a separate one for checkout pages. Add the tag to both to follow visitors through to purchase. ## Site pages 1. **Open Settings → Site details** In your Kajabi dashboard, open **Settings**, then **Site details** under the site settings. 2. **Paste the snippet into Header Page Scripts** Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```html title="Page scripts → Header Page Scripts" ``` Save. It loads in the head of your website, product, lesson and landing pages. ## Checkout pages 1. **Open Settings → Payment settings → Checkout** Header Page Scripts don't run on checkout, so add the tag here as well. 2. **Paste the same snippet into the header tracking code** ```html title="Checkout Tracking Code → Header tracking code" ``` Save. Each offer can switch the header tracking code off, so check that it is on for the offers you sell. 3. **Open your site in a private window** Browse a page, then open an offer's checkout. > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). ## Notes - Only hostnames listed in your site's **Settings → General → Site** domains (and their subdomains) are accepted. To also count visits on a platform address such as `your-site.mykajabi.com`, add it there; otherwise those requests are refused with `domain_not_allowed`. - Sales show in MetricRaven only if they're paid into a Stripe account you connect. See [Other Stripe methods](https://metricraven.com/docs/stripe-other-methods) for what can be linked to a visit. - If you use a cookie banner, connect it with [Consent & visitor IDs](https://metricraven.com/docs/consent). ## Attribute sales To credit Kajabi sales to the visits that brought the buyer, connect Stripe and follow [Attribute revenue with Kajabi](https://metricraven.com/docs/attribution-guide-with-kajabi). Optional script attributes are listed in [Install & verify](https://metricraven.com/docs/install). To exclude your own visits, see [Exclude your visits](https://metricraven.com/docs/exclude-visits). --- # Install on Podia > Add MetricRaven to your Podia site and checkout with the website tracking code setting. Source: https://metricraven.com/docs/install-podia Markdown: https://metricraven.com/docs/install-podia.md Section: Installation guides Podia's third-party code setting adds the tag to your public pages, including checkout. It needs a paid Podia plan. 1. **Open Admin → Settings → Analytics** From your Podia home, open your site's **Admin**, then **Settings** and **Analytics**. 2. **Edit Third-party code and paste the snippet** Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```html title="Website tracking code" ``` Use **Website tracking code**, not the conversion tracking code, and save. 3. **Log out, then open your site** Podia hides tracking code while you're logged in, so test in a private window. > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). > **Note:** Podia places tracking code at the end of the page body rather than in the head. That's fine for MetricRaven: the tag is deferred either way. ## Notes - If the option is greyed out on a paid plan, verify your account email first. It isn't available during a trial. - Podia doesn't load tracking code in the customer area, product viewer or community, so those visits aren't recorded. - Add every hostname your site opens on (your custom domain and any Podia address) to your site's **Settings → General → Site** domains. - If you use a cookie banner, connect it with [Consent & visitor IDs](https://metricraven.com/docs/consent). ## Attribute sales To credit Podia sales to the visits that brought the buyer, connect Stripe and follow [Attribute revenue with Podia](https://metricraven.com/docs/attribution-guide-with-podia). Optional script attributes are listed in [Install & verify](https://metricraven.com/docs/install). To exclude your own visits, see [Exclude your visits](https://metricraven.com/docs/exclude-visits). --- # Install on Lovable > Add MetricRaven to a Lovable app by asking Lovable, or by editing the code yourself. Source: https://metricraven.com/docs/install-lovable Markdown: https://metricraven.com/docs/install-lovable.md Section: Installation guides The quickest way is to ask Lovable to add the tag. You can also edit the file yourself in Code mode or through GitHub. ## Ask Lovable 1. **Send this prompt in the chat** Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```text title="Prompt" Add this analytics script to every page of the app. Load it once for the whole app, in index.html or the root layout (with next/script in a Next.js app), never from a single page or component. Keep the src and data-site-id attributes exactly as written: ``` 2. **Publish** Choose **Publish** (or **Update** for an app that is already live). The editor preview isn't your published app. ## Or edit the code Open the **Code** tab (editing needs a paid Lovable plan) or edit the repository you synced to GitHub. Where the tag goes depends on how the app was built: - **Vite + React** (most apps created before May 2026): paste the tag before `` in `index.html`. - **TanStack Start** (newer, server-rendered apps; `package.json` lists `@tanstack/react-start`): add it to the document head in the root route, `src/routes/__root.tsx`. ```html title="Your app's head" ``` > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). ## Notes - Only hostnames listed in your site's **Settings → General → Site** domains (and their subdomains) are accepted. To also count visits on a platform address such as `your-app.lovable.app`, add it there; otherwise those requests are refused with `domain_not_allowed`. - AI edits can drift. After each publish, open your live app, view the page source and check the tag is still in `` with its `data-site-id` unchanged. - Click between a few pages and check the Network panel shows one `/api/v1/collect` pageview per navigation. Two per click means the tag is loaded twice. - To record signups or other actions, ask Lovable to call `window.analytics.track("signup")` at that point; see [Goals & funnels](https://metricraven.com/docs/goals-funnels). Optional script attributes are listed in [Install & verify](https://metricraven.com/docs/install). To exclude your own visits, see [Exclude your visits](https://metricraven.com/docs/exclude-visits). --- # Install on Bolt > Add MetricRaven to a Bolt.new project by prompting Bolt or editing the code, then publish. Source: https://metricraven.com/docs/install-bolt Markdown: https://metricraven.com/docs/install-bolt.md Section: Installation guides Ask Bolt to add the tag, or paste it yourself in the code view, then publish the project. ## Ask Bolt 1. **Send this prompt in the chat** Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```text title="Prompt" Add this analytics script to every page of the app. Load it once for the whole app, in index.html or the root layout (with next/script in a Next.js app), never from a single page or component. Keep the src and data-site-id attributes exactly as written: ``` 2. **Publish** Publish the project, then open the published URL. The in-editor preview doesn't count. ## Or edit the code 1. **Open the code view** Choose the **<>** code icon at the top of the editor and pick the file from the file tree. Code editing isn't available on mobile, and is read-only in Safari. 2. **Paste the tag into the page head** For a Vite project, that's `index.html`, before ``. For a Next.js project, follow the [Next.js](https://metricraven.com/docs/install-nextjs) guide. ```html title="index.html" ``` 3. **Save and publish** Save the file and publish again. > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). ## Notes - Add the address your project is published on to your site's **Settings → General → Site** domains: a `*.bolt.host` address with Bolt hosting, a `*.netlify.app` address if you publish through Netlify, or your custom domain. Otherwise requests are refused with `domain_not_allowed`. - AI edits can drift. After each publish, open your live app, view the page source and check the tag is still in `` with its `data-site-id` unchanged. - Click between a few pages and check the Network panel shows one `/api/v1/collect` pageview per navigation. Two per click means the tag is loaded twice. Optional script attributes are listed in [Install & verify](https://metricraven.com/docs/install). To exclude your own visits, see [Exclude your visits](https://metricraven.com/docs/exclude-visits). --- # Install on v0 > Add MetricRaven to a v0 app by prompting v0 or editing app/layout.tsx, then publish to Vercel. Source: https://metricraven.com/docs/install-v0 Markdown: https://metricraven.com/docs/install-v0.md Section: Installation guides v0 builds Next.js apps, so the tag goes in the root layout. Ask v0 to add it or edit the file yourself, then publish. ## Ask v0 1. **Send this prompt in the chat** Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```text title="Prompt" Add this analytics script to every page of the app. Load it once for the whole app, in index.html or the root layout (with next/script in a Next.js app), never from a single page or component. Keep the src and data-site-id attributes exactly as written: ``` 2. **Publish** Choose **Publish** (or **Publish Changes** later). The chat preview isn't your production site. ## Or edit the code 1. **Open app/layout.tsx in the code editor** Add the `Script` import and element, keeping the rest of your layout. Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```typescript title="app/layout.tsx" // app/layout.tsx import Script from "next/script"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ``` 2. **Publish** Choose **Publish** at the top right of the workspace (or republish). The development preview is a private, temporary URL that isn't your live app. ## Or edit the file yourself Open the file that renders your page head and paste the tag before ``: - A Vite or plain HTML app: `index.html` (in full-stack apps it may sit in a `client` folder). - Next.js: follow the [Next.js](https://metricraven.com/docs/install-nextjs) guide. - Flask or Django: your base template; see [Django](https://metricraven.com/docs/install-django). ```html title="index.html" ``` > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). ## Notes - Only hostnames listed in your site's **Settings → General → Site** domains (and their subdomains) are accepted. To also count visits on a platform address such as `your-app.replit.app`, add it there; otherwise those requests are refused with `domain_not_allowed`. - On the Starter plan a published app expires after 30 days. When it does, tracking stops with it. - AI edits can drift. After each publish, open your live app, view the page source and check the tag is still in `` with its `data-site-id` unchanged. - Click between a few pages and check the Network panel shows one `/api/v1/collect` pageview per navigation. Two per click means the tag is loaded twice. Optional script attributes are listed in [Install & verify](https://metricraven.com/docs/install). To exclude your own visits, see [Exclude your visits](https://metricraven.com/docs/exclude-visits). --- # Install with Google Tag Manager > Load MetricRaven from a Custom HTML tag that fires on all pages. Source: https://metricraven.com/docs/install-google-tag-manager Markdown: https://metricraven.com/docs/install-google-tag-manager.md Section: Installation guides Create one Custom HTML tag that fires on every page, then publish the container. 1. **Create a Custom HTML tag** In your container, choose **Tags → New** and pick **Custom HTML** as the tag type. 2. **Paste the snippet** Replace `YOUR_SITE_ID` with the public site ID from your site's **Settings → Install** card. The site ID is public and safe in browser code; never put your secret key in a page. ```html title="Custom HTML" ``` 3. **Fire it on all pages** Use a page-load trigger for all pages, such as **All Pages**. Save the tag. 4. **Preview, then publish** Use GTM's Preview mode to confirm the tag fires once per page load, then **Submit** the container. > **Verify it works** > > Open your live website in a normal browser window (not an automated or embedded one). In the browser's Network panel, look for `script.js` and then a request to `/api/v1/collect` whose response contains `ok: true`. Open your site's **Traffic** tab and choose **Refresh**; the selected dates must include your visit. > > Nothing showing? See [Troubleshooting](https://metricraven.com/docs/troubleshooting). ## Notes - Don't add a history-change trigger for this tag. The tracker records single-page-app navigation itself; firing the tag again would load it twice. - The script reads `data-site-id` from its own element. If no collection request appears, inspect the injected ` ``` ## Example: a React welcome page If checkout redirects to `/welcome`, call it when the page loads: ```jsx title="app/welcome/page.jsx" "use client"; import { useEffect } from "react"; // The page your checkout redirects to, e.g. /welcome, with the signed-in buyer. export default function Welcome({ user }) { useEffect(() => { if (!user?.email) return; if (window.metricraven?.payment) { window.metricraven.payment({ email: user.email }); } else { window.metricraven = window.metricraven || { q: [] }; window.metricraven.q.push(["payment", { email: user.email }]); } }, [user?.email]); return

Welcome aboard

; } ``` With the [npm package](https://metricraven.com/docs/npm-package), the call is `payment({ email })` from `@metricraven/web`. ## When to use this - Checkouts where you can't set custom data, including third-party tools that sell through Lemon Squeezy. - Flows where the buyer's email is known on the page after payment. When you can pass the visitor ID at checkout, prefer that: it's exact and needs no email. ## Notes - Repeat calls are ignored, so reloading the page can't double anything. Payments themselves still come only from the webhook. - It only fills in payments that have no other link to a visit, and never replaces a visitor ID, user ID or server-side identify. - Matching rules, privacy and the security model: [Attribute by email](https://metricraven.com/docs/attribute-by-email). --- # Connect Polar > Receive Polar orders, renewals and refunds through a signed webhook and attribute them to visits. Source: https://metricraven.com/docs/polar Markdown: https://metricraven.com/docs/polar.md Section: Revenue Polar sends paid and refunded orders to a webhook signed with the endpoint's secret. Checkout metadata links each order to the visit that started it. ## Set up the webhook 1. **Copy the webhook URL** In MetricRaven, open **Settings → Integrations → Polar** and copy the webhook URL. 2. **Add an endpoint in Polar** In your Polar organization, open **Settings → Webhooks → Add Endpoint**. Paste the URL, choose the **Raw** format, and select `order.paid` and `order.refunded`. For [subscription goals](https://metricraven.com/docs/goals-funnels#payment-provider-goals), also select `subscription.created`, `subscription.active`, `subscription.updated`, `subscription.canceled`, `subscription.uncanceled` and `subscription.revoked`. 3. **Paste the secret in MetricRaven** Copy the endpoint's secret from Polar, paste it into the Polar card and choose **Connect Polar**. ## Pass the visitor ID at checkout Polar copies checkout `metadata` to the order. Use these keys, and set `externalCustomerId` to your user ID so it matches the Identify API: | Key | Value | | --- | --- | | metricraven_visitor_id | The visitor ID from `getVisitorId()` (24 or 32 hex characters). | | metricraven_user_id | Optional. Your signed-in user's ID, set on the server, matching the Identify API. | | metricraven_plan | Optional. A plan name for reports; otherwise the product from the order is used. | 1. **Read the visitor ID in the browser** The MetricRaven script exposes `window.analytics.getVisitorId()`. Read it when checkout starts and send it to your checkout code: ```javascript // Read this in the visitor's browser when they start checkout. const visitorId = window.analytics?.getVisitorId() ?? null; // Send visitorId to your own checkout endpoint alongside its usual request data. // Use the authenticated user ID from your backend; do not trust a browser-supplied user ID. ``` 2. **Add it to the checkout** ```javascript title="Checkout API" // Server: create a checkout with the Polar SDK (@polar-sh/sdk). const checkout = await polar.checkouts.create({ products: ["YOUR_PRODUCT_ID"], successUrl: "https://example.com/success", // Your authenticated user's ID; Polar returns it as customer.external_id. externalCustomerId: user?.id, metadata: visitorId ? { metricraven_visitor_id: visitorId } : {}, }); // Redirect the visitor to checkout.url ``` For a checkout link, pass the visitor ID as `reference_id`; Polar adds it to the order metadata: ```javascript title="Checkout link" // Browser: Polar copies ?reference_id= from a checkout link into the order metadata. const url = new URL("https://buy.polar.sh/YOUR_CHECKOUT_LINK_ID"); const visitorId = window.analytics?.getVisitorId(); if (visitorId) url.searchParams.set("reference_id", visitorId); window.location.href = url.toString(); ``` MetricRaven links each payment to a visit using the first of these that is available: 1. The `metricraven_visitor_id` passed through checkout. 2. The `metricraven_user_id` (or the provider's customer external ID), matched to a visitor you linked with the [Identify API](https://metricraven.com/docs/identify-users). 3. An earlier payment from the same provider customer, so renewals keep the original attribution. 4. The checkout email, matched to an email you sent to the Identify API. 5. The checkout email, matched to a browser that called [payment({ email })](https://metricraven.com/docs/attribute-by-email) within an hour of the payment. Can't set metadata on your checkout? See [Other Polar methods](https://metricraven.com/docs/polar-other-methods) to match by the buyer's email. > **Verify it works** > > After a delivery, the Polar card in **Settings → Integrations** shows **Connected**, the number of verified events and the last event type. A live payment appears in **Revenue** in its own currency; with a visitor ID it is credited to that visit's source in **Traffic**. > > If the card says a delivery was rejected, the signing secret doesn't match. Paste it again or generate a replacement. ## Details - Revenue is the order's `total_amount` (after discounts, including tax), converted from cents. Refunds include refunded tax. - Orders with `billing_reason: subscription_cycle` are renewals. A subscription cancellation is not a refund. - Signature: Polar uses Standard Webhooks headers `webhook-id`, `webhook-timestamp` and `webhook-signature`. Deliveries older than five minutes are rejected; MetricRaven accepts both Polar's original and current secret formats. - Polar's sandbox is a separate environment; connect it only to a test site. - Each delivery is verified with an HMAC signature over the exact request body, compared in constant time. Unsigned or altered requests are rejected with `401` and never change revenue. - Redelivered events are recognized and ignored, so provider retries never add a payment twice. Refunds reduce the original payment and never exceed it. - The signing secret is stored encrypted, bound to this site, and never shown in reports, share links or logs. Disconnecting deletes it; imported revenue stays in your reports. - Free trials and zero-amount orders are not revenue. A subscription cancellation is not a refund: revenue already paid stays. - Webhooks deliver new events only. Payments from before you connected are not imported, and live MRR is currently available for Stripe only. - Use one revenue path per purchase: don't also send the same orders through the Payments API. --- # Other Polar methods > Attribute Polar payments by the customer's email when the checkout can't carry the visitor ID. Source: https://metricraven.com/docs/polar-other-methods Markdown: https://metricraven.com/docs/polar-other-methods.md Section: Revenue If you create checkouts with the Polar API or checkout links, pass the visitor ID as in [Connect Polar](https://metricraven.com/docs/polar). For every other flow, the buyer's email links the payment to the visit. ## Send the buyer's email from the browser When you can't put the visitor ID in the checkout's custom data (a third-party storefront, a no-code tool, a mobile app or an older checkout), tell MetricRaven which email this visitor is paying with. A Polar payment with that customer email, made within an hour, is credited to the visit. 1. **Connect Polar** Follow [Connect Polar](https://metricraven.com/docs/polar) first: payments arrive through its webhook. 2. **Call payment() after a successful payment** On the page buyers land on after paying, pass the email they paid with: ```javascript // On the page buyers see after paying, with the email they paid with. window.metricraven.payment({ email: "customer@example.com" }); ``` 3. **Make the call safe before the script loads (recommended)** Add this to the page's ``, above other scripts. Calls made before the MetricRaven script loads are queued and sent as soon as it does. ```html title="Queue stub" ``` ## Example: a React welcome page If checkout redirects to `/welcome`, call it when the page loads: ```jsx title="app/welcome/page.jsx" "use client"; import { useEffect } from "react"; // The page your checkout redirects to, e.g. /welcome, with the signed-in buyer. export default function Welcome({ user }) { useEffect(() => { if (!user?.email) return; if (window.metricraven?.payment) { window.metricraven.payment({ email: user.email }); } else { window.metricraven = window.metricraven || { q: [] }; window.metricraven.q.push(["payment", { email: user.email }]); } }, [user?.email]); return

Welcome aboard

; } ``` With the [npm package](https://metricraven.com/docs/npm-package), the call is `payment({ email })` from `@metricraven/web`. ## When to use this - Checkouts where you can't set custom data, including third-party tools that sell through Polar. - Flows where the buyer's email is known on the page after payment. When you can pass the visitor ID at checkout, prefer that: it's exact and needs no email. ## Notes - Repeat calls are ignored, so reloading the page can't double anything. Payments themselves still come only from the webhook. - It only fills in payments that have no other link to a visit, and never replaces a visitor ID, user ID or server-side identify. - Matching rules, privacy and the security model: [Attribute by email](https://metricraven.com/docs/attribute-by-email). --- # Connect Paddle > Receive Paddle Billing transactions and refunds through a signed webhook and attribute them to visits. Source: https://metricraven.com/docs/paddle Markdown: https://metricraven.com/docs/paddle.md Section: Revenue Paddle Billing sends completed transactions and refund adjustments to a notification destination signed with its secret key. Checkout custom data links each transaction to a visit. > **Note:** This guide is for Paddle Billing. Paddle Classic webhooks are not supported. ## Set up the notification destination 1. **Copy the webhook URL** In MetricRaven, open **Settings → Integrations → Paddle** and copy the webhook URL. 2. **Create a destination in Paddle** In Paddle, open **Developer tools → Notifications → New destination**. Choose **Webhook**, paste the URL, and select `transaction.completed`, `adjustment.created` and `adjustment.updated`. Also select `customer.created` and `customer.updated` to match payments by the buyer's email, and the `subscription.*` events (created, activated, trialing, updated, past_due, paused, resumed, canceled) for [subscription goals](https://metricraven.com/docs/goals-funnels#payment-provider-goals). 3. **Paste the secret key in MetricRaven** Open the saved destination, copy its **secret key** (`pdl_ntfset_…`), paste it into the Paddle card and choose **Connect Paddle**. ## Pass the visitor ID at checkout Paddle keeps `custom_data` on the transaction and copies it to the subscription and its renewals. Use these keys: | Key | Value | | --- | --- | | metricraven_visitor_id | The visitor ID from `getVisitorId()` (24 or 32 hex characters). | | metricraven_user_id | Optional. Your signed-in user's ID, set on the server, matching the Identify API. | | metricraven_plan | Optional. A plan name for reports; otherwise the product from the transaction's first price is used. | 1. **Read the visitor ID in the browser** The MetricRaven script exposes `window.analytics.getVisitorId()`. Read it when checkout starts and send it to your checkout code: ```javascript // Read this in the visitor's browser when they start checkout. const visitorId = window.analytics?.getVisitorId() ?? null; // Send visitorId to your own checkout endpoint alongside its usual request data. // Use the authenticated user ID from your backend; do not trust a browser-supplied user ID. ``` 2. **Add it to the checkout** With Paddle.js: ```javascript title="Paddle.js" // Browser: Paddle.js overlay or inline checkout. Paddle.Checkout.open({ items: [{ priceId: "pri_YOUR_PRICE_ID", quantity: 1 }], customData: { metricraven_visitor_id: window.analytics?.getVisitorId() ?? "", }, }); ``` Or create the transaction on your server, which can also set your trusted user ID: ```javascript title="Transactions API" // Server: create a transaction with the Paddle API, then open it with Paddle.js. const response = await fetch("https://api.paddle.com/transactions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.PADDLE_API_KEY}`, }, body: JSON.stringify({ items: [{ price_id: "pri_YOUR_PRICE_ID", quantity: 1 }], custom_data: { ...(visitorId ? { metricraven_visitor_id: visitorId } : {}), ...(user ? { metricraven_user_id: user.id } : {}), }, }), }); const { data } = await response.json(); // In the browser: Paddle.Checkout.open({ transactionId: data.id }) ``` MetricRaven links each payment to a visit using the first of these that is available: 1. The `metricraven_visitor_id` passed through checkout. 2. The `metricraven_user_id` (or the provider's customer external ID), matched to a visitor you linked with the [Identify API](https://metricraven.com/docs/identify-users). 3. An earlier payment from the same provider customer, so renewals keep the original attribution. 4. The checkout email, matched to an email you sent to the Identify API. 5. The checkout email, matched to a browser that called [payment({ email })](https://metricraven.com/docs/attribute-by-email) within an hour of the payment. Can't set custom data on your checkout? See [Other Paddle methods](https://metricraven.com/docs/paddle-other-methods) to match by the buyer's email. > **Verify it works** > > After a delivery, the Paddle card in **Settings → Integrations** shows **Connected**, the number of verified events and the last event type. A live payment appears in **Revenue** in its own currency; with a visitor ID it is credited to that visit's source in **Traffic**. > > If the card says a delivery was rejected, the signing secret doesn't match. Paste it again or generate a replacement. ## Details - Revenue is the transaction's `grand_total` (including tax), converted from the lowest denomination. Trials and fully credited transactions are not revenue. - Transactions with `origin: subscription_recurring` are renewals. Approved refund and chargeback adjustments reduce the original transaction once, even when both `created` and `updated` events arrive. - Paddle transactions carry only a customer ID. With `customer.created` and `customer.updated` selected, MetricRaven keeps a keyed hash of each customer's email to match [payment({ email })](https://metricraven.com/docs/attribute-by-email) calls; otherwise pass `metricraven_user_id` or the visitor ID. - Signature: `Paddle-Signature` carries `ts` and one or more `h1` values, the hex HMAC-SHA256 of `ts:body`. Deliveries older than five minutes are rejected. - Paddle's sandbox is a separate account; connect it only to a test site. - Each delivery is verified with an HMAC signature over the exact request body, compared in constant time. Unsigned or altered requests are rejected with `401` and never change revenue. - Redelivered events are recognized and ignored, so provider retries never add a payment twice. Refunds reduce the original payment and never exceed it. - The signing secret is stored encrypted, bound to this site, and never shown in reports, share links or logs. Disconnecting deletes it; imported revenue stays in your reports. - Free trials and zero-amount orders are not revenue. A subscription cancellation is not a refund: revenue already paid stays. - Webhooks deliver new events only. Payments from before you connected are not imported, and live MRR is currently available for Stripe only. - Use one revenue path per purchase: don't also send the same orders through the Payments API. --- # Other Paddle methods > Attribute Paddle payments by the customer's email when the checkout can't carry the visitor ID. Source: https://metricraven.com/docs/paddle-other-methods Markdown: https://metricraven.com/docs/paddle-other-methods.md Section: Revenue If you open checkouts with Paddle.js or create transactions on your server, pass the visitor ID as in [Connect Paddle](https://metricraven.com/docs/paddle). For every other flow, the buyer's email links the payment to the visit. ## Send the buyer's email from the browser When you can't put the visitor ID in the checkout's custom data (a third-party storefront, a no-code tool, a mobile app or an older checkout), tell MetricRaven which email this visitor is paying with. A Paddle payment with that customer email, made within an hour, is credited to the visit. 1. **Connect Paddle** Follow [Connect Paddle](https://metricraven.com/docs/paddle) first: payments arrive through its webhook. 2. **Call payment() after a successful payment** On the page buyers land on after paying, pass the email they paid with: ```javascript // On the page buyers see after paying, with the email they paid with. window.metricraven.payment({ email: "customer@example.com" }); ``` 3. **Make the call safe before the script loads (recommended)** Add this to the page's ``, above other scripts. Calls made before the MetricRaven script loads are queued and sent as soon as it does. ```html title="Queue stub" ``` ## Example: a React welcome page If checkout redirects to `/welcome`, call it when the page loads: ```jsx title="app/welcome/page.jsx" "use client"; import { useEffect } from "react"; // The page your checkout redirects to, e.g. /welcome, with the signed-in buyer. export default function Welcome({ user }) { useEffect(() => { if (!user?.email) return; if (window.metricraven?.payment) { window.metricraven.payment({ email: user.email }); } else { window.metricraven = window.metricraven || { q: [] }; window.metricraven.q.push(["payment", { email: user.email }]); } }, [user?.email]); return

Welcome aboard

; } ``` With the [npm package](https://metricraven.com/docs/npm-package), the call is `payment({ email })` from `@metricraven/web`. ## When to use this - Checkouts where you can't set custom data, including third-party tools that sell through Paddle. - Flows where the buyer's email is known on the page after payment. When you can pass the visitor ID at checkout, prefer that: it's exact and needs no email. ## Notes - Repeat calls are ignored, so reloading the page can't double anything. Payments themselves still come only from the webhook. - It only fills in payments that have no other link to a visit, and never replaces a visitor ID, user ID or server-side identify. - Paddle transactions carry only a customer ID. In your notification destination, also select `customer.created` and `customer.updated` so the customer's email can be matched. Only a keyed hash of it is kept. - Matching rules, privacy and the security model: [Attribute by email](https://metricraven.com/docs/attribute-by-email). --- # Connect Dodo Payments > Receive Dodo Payments payments, renewals and refunds through a signed webhook and attribute them to visits. Source: https://metricraven.com/docs/dodo-payments Markdown: https://metricraven.com/docs/dodo-payments.md Section: Revenue Dodo Payments sends successful payments and refunds to a webhook signed with its secret. Checkout metadata links each payment to a visit. ## Set up the webhook 1. **Copy the webhook URL** In MetricRaven, open **Settings → Integrations → Dodo Payments** and copy the webhook URL. 2. **Add a webhook in Dodo Payments** In the Dodo Payments dashboard, open **Developer → Webhooks → Add Webhook**. Paste the URL and select `payment.succeeded` and `refund.succeeded`. For [subscription goals](https://metricraven.com/docs/goals-funnels#payment-provider-goals), also select the `subscription.*` events (active, on_hold, cancelled, expired, failed, plan_changed, updated). 3. **Paste the signing secret in MetricRaven** Copy the webhook's signing secret (`whsec_…`), paste it into the Dodo Payments card and choose **Connect Dodo Payments**. ## Pass the visitor ID at checkout Dodo stores checkout session `metadata` on the payment, and subscription renewals carry the subscription's metadata. Use these keys: | Key | Value | | --- | --- | | metricraven_visitor_id | The visitor ID from `getVisitorId()` (24 or 32 hex characters). | | metricraven_user_id | Optional. Your signed-in user's ID, set on the server, matching the Identify API. | | metricraven_plan | Optional. A plan name for reports; otherwise the product from the payment is used. | 1. **Read the visitor ID in the browser** The MetricRaven script exposes `window.analytics.getVisitorId()`. Read it when checkout starts and send it to your checkout code: ```javascript // Read this in the visitor's browser when they start checkout. const visitorId = window.analytics?.getVisitorId() ?? null; // Send visitorId to your own checkout endpoint alongside its usual request data. // Use the authenticated user ID from your backend; do not trust a browser-supplied user ID. ``` 2. **Add it to the checkout** ```javascript title="Checkout Sessions API" // Server: create a checkout session with the Dodo Payments SDK (dodopayments). const session = await dodo.checkoutSessions.create({ product_cart: [{ product_id: "pdt_YOUR_PRODUCT_ID", quantity: 1 }], customer: user ? { email: user.email, name: user.name } : undefined, return_url: "https://example.com/success", metadata: { ...(visitorId ? { metricraven_visitor_id: visitorId } : {}), ...(user ? { metricraven_user_id: user.id } : {}), }, }); // Redirect the visitor to session.checkout_url ``` For a static payment link, add `metadata_metricraven_visitor_id`: ```javascript title="Payment link" // Browser: payment links turn ?metadata_= into payment metadata. const url = new URL("https://checkout.dodopayments.com/buy/pdt_YOUR_PRODUCT_ID"); const visitorId = window.analytics?.getVisitorId(); if (visitorId) url.searchParams.set("metadata_metricraven_visitor_id", visitorId); window.location.href = url.toString(); ``` MetricRaven links each payment to a visit using the first of these that is available: 1. The `metricraven_visitor_id` passed through checkout. 2. The `metricraven_user_id` (or the provider's customer external ID), matched to a visitor you linked with the [Identify API](https://metricraven.com/docs/identify-users). 3. An earlier payment from the same provider customer, so renewals keep the original attribution. 4. The checkout email, matched to an email you sent to the Identify API. 5. The checkout email, matched to a browser that called [payment({ email })](https://metricraven.com/docs/attribute-by-email) within an hour of the payment. > **Verify it works** > > After a delivery, the Dodo Payments card in **Settings → Integrations** shows **Connected**, the number of verified events and the last event type. A live payment appears in **Revenue** in its own currency; with a visitor ID it is credited to that visit's source in **Traffic**. > > If the card says a delivery was rejected, the signing secret doesn't match. Paste it again or generate a replacement. ## Details - Revenue is the payment's `total_amount` (including tax), converted from the currency's smallest unit. Each `refund.succeeded` reduces its payment once. - Subscription payments without a checkout session are renewals. A cancellation is not a refund. - Signature: Dodo uses Standard Webhooks headers `webhook-id`, `webhook-timestamp` and `webhook-signature`. Deliveries older than five minutes are rejected. - Test mode and live mode have separate webhooks; connect test mode only to a test site. - Each delivery is verified with an HMAC signature over the exact request body, compared in constant time. Unsigned or altered requests are rejected with `401` and never change revenue. - Redelivered events are recognized and ignored, so provider retries never add a payment twice. Refunds reduce the original payment and never exceed it. - The signing secret is stored encrypted, bound to this site, and never shown in reports, share links or logs. Disconnecting deletes it; imported revenue stays in your reports. - Free trials and zero-amount orders are not revenue. A subscription cancellation is not a refund: revenue already paid stays. - Webhooks deliver new events only. Payments from before you connected are not imported, and live MRR is currently available for Stripe only. - Use one revenue path per purchase: don't also send the same orders through the Payments API. --- # Connect WooCommerce > Receive WooCommerce orders and refunds through a signed webhook and attribute them to visits. Source: https://metricraven.com/docs/woocommerce Markdown: https://metricraven.com/docs/woocommerce.md Section: Revenue WooCommerce sends order updates to a webhook signed with a secret MetricRaven generates. A small plugin stores the visitor ID on each order. ## Set up the webhook 1. **Install the MetricRaven script on your store** Install the [MetricRaven WordPress plugin](https://metricraven.com/docs/install-wordpress) so checkout pages load the tracker and orders carry the visitor ID. 2. **Open Settings → Integrations → WooCommerce** Choose **Set up WooCommerce**. Copy the webhook URL and the secret; the secret is shown once. 3. **Add a webhook in WooCommerce** In WordPress, open **WooCommerce → Settings → Advanced → Webhooks → Add webhook**. Set **Status** to Active, **Topic** to **Order updated**, paste the delivery URL and secret, keep **WP REST API Integration v3**, and save. Order updated fires when an order is paid and when it is refunded. Optionally add a second webhook for **Order created** with the same URL and secret. ## Store the visitor ID on orders The [MetricRaven WordPress plugin](https://metricraven.com/docs/install-wordpress) does this for you: keep its **Revenue attribution** setting on. Without the plugin, save this as a must-use plugin. It keeps the visitor ID in a session cookie on checkout and stores it on the order as hidden meta `_metricraven_visitor_id` (classic and block checkout): ```php title="metricraven-attribution.php" update_meta_data('_metricraven_visitor_id', $id); } } add_action('woocommerce_checkout_create_order', 'metricraven_attribute_order'); add_action('woocommerce_store_api_checkout_update_order_meta', 'metricraven_attribute_order'); ``` You can also set `metricraven_user_id` order meta from your own code. Signed-in customers' billing emails are matched to emails you sent to the Identify API. MetricRaven links each payment to a visit using the first of these that is available: 1. The `metricraven_visitor_id` passed through checkout. 2. The `metricraven_user_id` (or the provider's customer external ID), matched to a visitor you linked with the [Identify API](https://metricraven.com/docs/identify-users). 3. An earlier payment from the same provider customer, so renewals keep the original attribution. 4. The checkout email, matched to an email you sent to the Identify API. 5. The checkout email, matched to a browser that called [payment({ email })](https://metricraven.com/docs/attribute-by-email) within an hour of the payment. > **Verify it works** > > After a delivery, the WooCommerce card in **Settings → Integrations** shows **Connected**, the number of verified events and the last event type. A live payment appears in **Revenue** in its own currency; with a visitor ID it is credited to that visit's source in **Traffic**. > > If the card says a delivery was rejected, the signing secret doesn't match. Paste it again or generate a replacement. ## Details - Orders count once they are paid (status processing or completed, with a paid date). Revenue is the order total including tax and shipping, in the order currency. Refunds reduce it by the order's refund totals. - WooCommerce Subscriptions renewal orders are counted as renewals. - WooCommerce sends an unsigned ping when you save a webhook; MetricRaven answers it without recording anything. - Signature: `X-WC-Webhook-Signature` is the base64 HMAC-SHA256 of the raw request body with your secret. - WooCommerce disables a webhook after repeated failed deliveries. If deliveries stop, check **WooCommerce → Status → Logs** and re-enable it. - Each delivery is verified with an HMAC signature over the exact request body, compared in constant time. Unsigned or altered requests are rejected with `401` and never change revenue. - Redelivered events are recognized and ignored, so provider retries never add a payment twice. Refunds reduce the original payment and never exceed it. - The signing secret is stored encrypted, bound to this site, and never shown in reports, share links or logs. Disconnecting deletes it; imported revenue stays in your reports. - Free trials and zero-amount orders are not revenue. A subscription cancellation is not a refund: revenue already paid stays. - Webhooks deliver new events only. Payments from before you connected are not imported, and live MRR is currently available for Stripe only. - Use one revenue path per purchase: don't also send the same orders through the Payments API. --- # Connect Shopify > Receive Shopify orders and refunds through signed webhooks and attribute them to visits. Source: https://metricraven.com/docs/shopify Markdown: https://metricraven.com/docs/shopify.md Section: Revenue Shopify sends paid and updated orders to webhooks signed with your store's webhook key. A cart attribute carries the visitor ID onto each order. ## Set up the webhooks 1. **Install the MetricRaven script in your theme** Follow [Install on Shopify](https://metricraven.com/docs/install-shopify). 2. **Copy the webhook URL** In MetricRaven, open **Settings → Integrations → Shopify** and copy the webhook URL. 3. **Create two webhooks in Shopify** In Shopify admin, open **Settings → Notifications → Webhooks → Create webhook**. Create one for **Order payment** and one for **Order update**, both in **JSON** format with the latest API version and this URL. 4. **Paste the signing key in MetricRaven** Below the webhook list Shopify shows the key your webhooks are signed with. Paste it into the Shopify card and choose **Connect Shopify**. For a custom app's webhooks, use the app's client secret instead. ## Add the visitor ID to the cart Cart attributes are copied to the order's `note_attributes`. Add this to your theme after the MetricRaven script; it sets the `metricraven_visitor_id` attribute: ```html title="layout/theme.liquid" ``` Customers' emails are also matched to emails you sent to the Identify API. MetricRaven links each payment to a visit using the first of these that is available: 1. The `metricraven_visitor_id` passed through checkout. 2. The `metricraven_user_id` (or the provider's customer external ID), matched to a visitor you linked with the [Identify API](https://metricraven.com/docs/identify-users). 3. An earlier payment from the same provider customer, so renewals keep the original attribution. 4. The checkout email, matched to an email you sent to the Identify API. 5. The checkout email, matched to a browser that called [payment({ email })](https://metricraven.com/docs/attribute-by-email) within an hour of the payment. > **Verify it works** > > After a delivery, the Shopify card in **Settings → Integrations** shows **Connected**, the number of verified events and the last event type. A live payment appears in **Revenue** in its own currency; with a visitor ID it is credited to that visit's source in **Traffic**. > > If the card says a delivery was rejected, the signing secret doesn't match. Paste it again or generate a replacement. ## Details - Revenue is the order total in your shop currency (including tax and shipping). Refunds come from the order's current total, so they need the **Order update** webhook. - Test orders are acknowledged but not counted. Unpaid and pending orders are not revenue. - Signature: `X-Shopify-Hmac-Sha256` is the base64 HMAC-SHA256 of the raw request body with the signing key. - Orders from subscription apps marked with source `subscription_contract` count as renewals. - Each delivery is verified with an HMAC signature over the exact request body, compared in constant time. Unsigned or altered requests are rejected with `401` and never change revenue. - Redelivered events are recognized and ignored, so provider retries never add a payment twice. Refunds reduce the original payment and never exceed it. - The signing secret is stored encrypted, bound to this site, and never shown in reports, share links or logs. Disconnecting deletes it; imported revenue stays in your reports. - Free trials and zero-amount orders are not revenue. A subscription cancellation is not a refund: revenue already paid stays. - Webhooks deliver new events only. Payments from before you connected are not imported, and live MRR is currently available for Stripe only. - Use one revenue path per purchase: don't also send the same orders through the Payments API. --- # Attribute payments by email > Credit provider payments to the right visit with the buyer's email when the checkout can't carry the visitor ID. Source: https://metricraven.com/docs/attribute-by-email Markdown: https://metricraven.com/docs/attribute-by-email.md Section: Revenue Some checkouts can't carry the visitor ID: Podia, Kajabi, Ghost, hosted pages and third-party tools. On the page buyers see after paying, tell MetricRaven which email this visitor paid with. A payment from your connected provider with that customer email is then credited to the visit. ## Call payment() on the thank-you page 1. **Connect your payment provider** Payments still come from [Stripe](https://metricraven.com/docs/stripe), a provider webhook ([Lemon Squeezy](https://metricraven.com/docs/lemonsqueezy), [Polar](https://metricraven.com/docs/polar), [Paddle](https://metricraven.com/docs/paddle), [Dodo Payments](https://metricraven.com/docs/dodo-payments)) or the [Payments API](https://metricraven.com/docs/payments-api). The browser call only says who paid; it never creates revenue. 2. **Pass the buyer's email after a successful payment** ```javascript // On the page buyers see after paying, with the email they paid with. window.metricraven.payment({ email: "customer@example.com" }); ``` `window.analytics.payment({ email })` is the same call. With the [npm package](https://metricraven.com/docs/npm-package): ```javascript title="@metricraven/web" import { payment } from "@metricraven/web"; payment({ email: user.email }); ``` 3. **Make the call safe before the script loads (recommended)** Add this to the page's ``, above other scripts. Calls made before the MetricRaven script loads are queued and sent as soon as it does. ```html title="Queue stub" ``` > **Verify it works** > > Make a test purchase from a fresh browser. Once the provider's payment arrives, it appears in **Revenue** credited to that visit's source instead of Unattributed. The order doesn't matter: the call can come before or after the payment reaches MetricRaven. ## Send the email from your server If you record payments with the [Payments API](https://metricraven.com/docs/payments-api), add the customer's `email` (or `customer_email`) when you have no visitor or user ID: ```bash curl --request POST 'https://metricraven.com/api/v1/payments' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" \ --header 'Content-Type: application/json' \ --data '{ "amount": 49, "currency": "USD", "transaction_id": "order_123", "email": "customer@example.com" }' ``` With [@metricraven/node](https://metricraven.com/docs/node-sdk), pass `email` to `payment()`: ```javascript title="Server" import { payment } from "@metricraven/node"; await payment({ amount: 49, currency: "USD", transactionId: order.id, email: order.customerEmail, // matched, never stored as text }); ``` The email first matches a visitor you linked with a server-side [identify](https://metricraven.com/docs/identify-users) call, then a browser that called `payment()` around the payment time. ## How matching works | Rule | What it means | | --- | --- | | Last resort only | A claim is used only for a payment with no visitor ID, no user ID and no email that matches a server-side identify. It never replaces any of those. | | One-hour window | It matches payments created up to an hour before or after the call, by the provider's payment time. | | First claim wins | Repeat calls from the same browser are ignored and don't move the window. After the hour, a new call starts a new window. | | Contested claims match nothing | If a second browser claims the same email within the hour, nobody gets the payment, and anything already credited through that claim goes back to Unattributed. | | Renewals follow | Once a customer's payment is matched, their later renewals and subscription goals keep the same visit, as with any other link. | | Rate limited | Each visitor can vouch for at most 3 emails a day, and each IP address can make 5 calls an hour. | | Provider | Where the customer email comes from | | --- | --- | | Stripe | The customer's email, else the charge's billing or receipt email. | | Lemon Squeezy | The order's `user_email`. | | Polar | The order's customer email. | | Paddle | Transactions only carry a customer ID. Select `customer.created` and `customer.updated` in your notification destination so MetricRaven can match them. | | Dodo Payments | The payment's customer email. | | Payments API | `email` or `customer_email`. | ## Privacy - Emails are never stored in plain text. MetricRaven keeps a keyed hash (HMAC-SHA256 with a server secret, different for every site), for three days. - Nothing is stored in the browser, and no event is recorded for the call. - The response is the same whether or not anything matched, so the call can't be used to test whether an email bought something. - [Deleting a person](https://metricraven.com/docs/delete-person-data) by email or visitor ID removes their claims too. ## Security model The call is self-asserted, like browser [identify](https://metricraven.com/docs/identify-users): anyone who can run JavaScript on your pages can make it. The rules above keep the damage small. | A malicious browser | Can it? | | --- | --- | | See revenue, payments or whether an email bought something | No. The call returns nothing about payments. | | Create, change or refund revenue | No. Revenue comes only from your provider or your server. | | Override a visitor ID passed at checkout, a user ID, or a server-side identify | No. Claims only fill in payments that have no other link. | | Credit someone's payment to its own visit | Only if it knows the buyer's email, claims it within an hour of their purchase, and the buyer's own thank-you page never makes the call. If the buyer's page does call, the claim is contested and the payment stays unattributed. | > **Warning:** The worst case is a payment credited to the wrong traffic source, never lost or extra revenue. When you can, pass the visitor ID at checkout or identify buyers from your server: both are exact and can't be claimed by a browser. ## Details - Emails are compared trimmed and case-insensitively. The browser call accepts a string or `{ email }` and returns `false` for an invalid email. - Without analytics consent the visitor ID is daily; a payment matched to it still counts toward that visit's source. When consent is granted later the same day, the claim moves to the persistent ID. - Guides: [Podia](https://metricraven.com/docs/attribution-guide-with-podia), [Kajabi](https://metricraven.com/docs/attribution-guide-with-kajabi), [Ghost](https://metricraven.com/docs/attribution-guide-with-ghost), [Lemon Squeezy](https://metricraven.com/docs/lemonsqueezy-other-methods), [Polar](https://metricraven.com/docs/polar-other-methods) and [Paddle](https://metricraven.com/docs/paddle-other-methods). --- # Attribute revenue with Podia > Credit Podia course, download and membership sales to the visits that brought the buyer, through Stripe. Source: https://metricraven.com/docs/attribution-guide-with-podia Markdown: https://metricraven.com/docs/attribution-guide-with-podia.md Section: Revenue Podia takes payments through your Stripe account but doesn't let you add the visitor ID to its checkout. Its conversion tracking code knows the buyer's email, so MetricRaven can match the Stripe payment to the visit. > **Note:** First [install MetricRaven on Podia](https://metricraven.com/docs/install-podia) and [connect Stripe](https://metricraven.com/docs/stripe). ## Add the conversion tracking code 1. **Open Settings → Third-party code** In your Podia dashboard, open **Settings**, then **Third-party code**, and scroll to **Conversion tracking code**. 2. **Paste this code** The first script keeps the call until MetricRaven has loaded; the second passes the buyer's email. ```html title="Conversion tracking code" ``` 3. **Save** Podia runs the conversion code only after a successful purchase. > **Verify it works** > > Make a test purchase in a private window, logged out of Podia. When Stripe sync imports the payment (about a minute), it appears in **Revenue** credited to that visit's source. ## Notes - Free products and free trials aren't revenue; their checkouts are matched to nothing. - Renewals of a matched subscription keep the same visit through the Stripe customer. - Matching rules, privacy and the security model: [Attribute by email](https://metricraven.com/docs/attribute-by-email). --- # Attribute revenue with Kajabi > Credit Kajabi course, coaching and membership sales to the visits that brought the buyer, through Stripe. Source: https://metricraven.com/docs/attribution-guide-with-kajabi Markdown: https://metricraven.com/docs/attribution-guide-with-kajabi.md Section: Revenue Kajabi charges through your Stripe account, but its checkout can't carry the visitor ID. Pass the buyer's email from the thank-you page and MetricRaven matches the Stripe payment to the visit. > **Note:** First [install MetricRaven on Kajabi](https://metricraven.com/docs/install-kajabi) and [connect Stripe](https://metricraven.com/docs/stripe). ## Add the code to your thank-you page 1. **Open the offer's thank-you page** In Kajabi, open **Sales → Offers**, choose the offer, then **Checkout settings → Thank you page**. Edit the page and add a **Custom code** block. 2. **Paste this code** ```html title="Custom code block" ``` Check that your thank-you page actually shows or receives the email: open it after a test purchase and look for `?email=` in the address or the email on the page. Adjust the selector if your theme puts it elsewhere. 3. **Save and publish** Repeat for each offer you sell, or use a shared thank-you page. If your theme has a site-wide **Footer code** (Website → Design → Customize), you can paste the code there instead; it only calls MetricRaven on pages that carry an email. > **Verify it works** > > Make a test purchase in a private window. Once Stripe sync imports the payment (about a minute), it appears in **Revenue** credited to that visit's source. ## Notes - Repeat loads of the thank-you page are ignored. - Payment plans and subscription renewals keep the same visit through the Stripe customer. - Matching rules, privacy and the security model: [Attribute by email](https://metricraven.com/docs/attribute-by-email). --- # Attribute revenue with Ghost > Credit paid Ghost memberships to the visits that brought the member, through Stripe. Source: https://metricraven.com/docs/attribution-guide-with-ghost Markdown: https://metricraven.com/docs/attribution-guide-with-ghost.md Section: Revenue Ghost sells paid memberships through your Stripe account. Its checkout can't carry the visitor ID, but the member's email can link the Stripe payment to the visit. > **Note:** First [install MetricRaven on Ghost](https://metricraven.com/docs/install-ghost) and [connect the Stripe account Ghost uses](https://metricraven.com/docs/stripe). ## Add the code with Code injection 1. **Open Settings → Advanced → Code injection** In Ghost Admin, open **Code injection** and choose **Site header**. 2. **Paste this code below the MetricRaven tag** ```html title="Site header" ``` It keeps the email typed into a member form for this tab only, and sends it once Ghost returns from Stripe with `?stripe=success`. 3. **Save** Members who upgrade while signed in pay with their account email, which is the one sent. > **Verify it works** > > Sign up for a paid tier in a private window. Once Stripe sync imports the payment (about a minute), it appears in **Revenue** credited to that visit's source. ## Without code You can also send paid members from your own backend or an automation (for example a Zapier “new member” trigger) to the [Payments API](https://metricraven.com/docs/payments-api) with their `email`. Don't do this as well as connecting Stripe, or payments count twice. ## Notes - Free members aren't revenue. Track free signups as a goal instead. - Renewals keep the same visit through the Stripe customer. - Matching rules, privacy and the security model: [Attribute by email](https://metricraven.com/docs/attribute-by-email). --- # Payments API > Send payments and refunds from a custom payment system with your site's secret key. Source: https://metricraven.com/docs/payments-api Markdown: https://metricraven.com/docs/payments-api.md Section: Revenue Use this for a custom payment system. Stripe, RevenueCat and the payment provider webhooks already send their supported payments, so choose one path per purchase. ## Send a payment 1. **Set your secret key on the server** Store the site secret as `METRICRAVEN_SECRET_KEY`. Call this API only from your backend. 2. **POST the payment after it succeeds** ```bash curl --request POST 'https://metricraven.com/api/v1/payments' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" \ --header 'Content-Type: application/json' \ --data '{ "amount": 29, "currency": "USD", "transaction_id": "order_123", "visitor_id": "0123456789abcdef0123456789abcdef", "user_id": "user_42", "plan": "pro", "renewal": false }' ``` `amount` is in major currency units: `29` means $29, not 29 cents. Include the three-letter currency, a unique transaction ID, and a visitor ID or a previously identified user ID for attribution. From JavaScript or TypeScript, use `payment()` and `refund()` in [@metricraven/node](https://metricraven.com/docs/node-sdk). > **Verify it works** > > The payment appears in **Revenue**, converted into the site's reporting currency at the ECB reference rate of its UTC day; the stored amount and currency never change. Repeat deliveries with the same transaction ID don't add a second payment. ## Request fields `POST https://metricraven.com/api/v1/payments` with the site secret key. Account tokens can't send payments. | Field | Type | Required | Description | | --- | --- | --- | --- | | amount | number | Yes | Amount paid in major units (29 means $29), 0 to 1,000,000,000. For a refund, the total refunded so far. | | currency | string | Yes | Three-letter ISO 4217 code, any case, for example USD. Stored uppercase; reports show one currency at a time. | | transaction_id | string | Yes | Your unique payment ID, 1–200 characters. A second payment with the same ID is ignored. | | visitor_id | string | No | The buyer's visitor ID (24 or 32 lowercase hex characters) for attribution. | | user_id | string | No | Your user ID (1–200 characters). Attributes the payment through identify calls, even from another device. | | email | string | No | The customer's email, when you have no visitor_id or user_id. Matches a visitor identified with this email, then a browser that called payment({ email }) within an hour. Used for matching only; never stored as text. customer_email is accepted too. | | customer_id | string | No | Your payment provider's customer ID, up to 200 characters. Groups payments into one customer for lifetime value. | | plan | string | No | Plan or product name, 1–100 characters, for revenue by plan. | | renewal | boolean | No | true for a recurring charge after the first one. Renewals count as renewal revenue, not new revenue. Default false. | | is_free_trial | boolean | No | true for a trial start that isn't revenue. Defaults to true when amount is 0, otherwise false. Trials are left out of revenue. | | refunded | boolean | No | true to record a refund of transaction_id (see below). | | timestamp | string | No | When the payment happened, as a UTC ISO 8601 timestamp ending in Z. Default: now. Use it to import past payments. | Send `visitor_id`, `user_id` or `email` (see [Attribute by email](https://metricraven.com/docs/attribute-by-email)); without a match, the payment counts in revenue as Unattributed. Unknown fields are ignored. ## Responses | Response | Meaning | | --- | --- | | 201 { "ok": true, "duplicate": false, "transaction_id": "order_123" } | Recorded. | | 200 { "ok": true, "duplicate": true, "transaction_id": "order_123" } | Already recorded; nothing changed, so retries are safe. | | 200 { "ok": true, "refunded": true, "transaction_id": "order_123" } | Refund applied to a payment already recorded. | | 400 invalid_json, invalid_payload | The body isn't JSON or a field is invalid; invalid_payload adds details with the fields. | | 401 unauthorized | Not a site secret key, or a demo website. | | 402 tracking_paused | The plan's monthly event allowance is used up. Refunds of recorded payments still work. | ## Renewals, trials and past payments Send each subscription charge as its own payment with its own `transaction_id`: the first with `renewal: false`, later ones with `renewal: true` and the same `user_id` or `customer_id`. Send a trial start with `amount: 0` (or `is_free_trial: true`) so it appears in the buyer's journey without counting as revenue. To import history, add each payment's original `timestamp`. ```bash title="Renewal example" curl --request POST 'https://metricraven.com/api/v1/payments' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" \ --header 'Content-Type: application/json' \ --data '{ "amount": 29, "currency": "USD", "transaction_id": "in_1QxYz2026", "user_id": "user_42", "customer_id": "cus_R2d2", "plan": "pro", "renewal": true, "timestamp": "2026-11-01T10:00:00Z" }' ``` ## Record a refund Send the original transaction ID with `refunded: true` and the **cumulative amount refunded**. For example, a $10 refund followed by another $5 refund means sending `amount: 15` on the second request. ```bash title="Partial refund example" curl --request POST 'https://metricraven.com/api/v1/payments' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" \ --header 'Content-Type: application/json' \ --data '{ "amount": 10, "currency": "USD", "transaction_id": "order_123", "refunded": true }' ``` The refunded amount is capped at the original amount; once it reaches it, the payment counts as fully refunded. A refund for a transaction ID that was never sent is stored as a fully refunded payment. ## Delete payments `DELETE /api/v1/payments` removes recorded payments and free trials, for example test checkouts or a wrong import. Filters, in priority order: `transaction_id` deletes one payment (dates are ignored); `visitor_id` or `user_id` delete that person's payments, optionally only between `startAt` and `endAt`; with only `startAt` and `endAt`, every payment in that range goes. Send both bounds or neither (`start`/`end` work as aliases); one bound alone is a 400, never an all-time delete. ```bash curl --request DELETE "https://metricraven.com/api/v1/payments?transaction_id=order_123" \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" ``` ```json title="Response" { "ok": true, "dryRun": false, "deleted": 1, "matched": 1, "match": { "transaction_id": "order_123" }, "timeRange": null, "message": "Deleted 1 payments." } ``` Add `dry_run=1` to count without deleting. The site secret key works, and so does an account token with the write scope and `site=WEBSITE_ID` when you are an admin or the owner. Revenue, LTV, customers and attribution update at once; each delete is kept in an audit log with its filter, count and who sent it. To erase a person (events and identity) for a privacy request, use [Delete a person's data](https://metricraven.com/docs/delete-person-data) instead, which keeps their payments anonymised. ## Details - New payments (not refunds) trigger [payment alerts](https://metricraven.com/docs/goal-alerts). - Renewals (`renewal: true`) and refunds also record the [subscription_renewed and payment_refunded goals](https://metricraven.com/docs/goals-funnels#payment-provider-goals). - Resending a transaction ID never updates the stored payment; only refunds change it. - The visitor ID comes from `window.analytics.getVisitorId()` in the buyer's browser; pass it through your checkout to your backend. --- # Link users to visits > Connect signed-in users to their visits from the browser or your backend, with optional profile traits. Source: https://metricraven.com/docs/identify-users Markdown: https://metricraven.com/docs/identify-users.md Section: Revenue After a user signs in, link your own stable user ID to their visits, so goals, payments and visits on other devices join one journey. There are two ways to identify. The browser call is quickest. The server call is verified, because only your backend holds the secret key. You can use both. ## Identify from the browser 1. **Call identify after sign-in** Pass your user ID and, optionally, a `name`, an `email` and up to 10 custom traits. The helper queues the call if the tracker hasn't loaded yet. ```javascript // After sign-in (and on page loads while signed in), with your own user ID. function identifyUser(user) { const traits = { name: user.name, email: user.email, image: user.avatarUrl, plan: user.plan }; if (window.metricraven?.identify) { window.metricraven.identify(user.id, traits); } else { window.metricraven = window.metricraven || { q: [] }; window.metricraven.q.push(["identify", user.id, traits]); } } ``` 2. **Call reset on logout** On a shared device, this stops the next person's visits from joining the previous user's profile. ```javascript // On logout: forget the identify state. With consent, a new visitor ID starts. window.metricraven?.reset(); ``` Using the npm package or the React Native SDK? Call `identify(userId, traits)` and `reset()` from there. See [NPM package](https://metricraven.com/docs/npm-package) and [React Native & Expo](https://metricraven.com/docs/react-native). ## What browser identify can and can't do | Rule | Why | | --- | --- | | It labels the current visitor only | The server works out the visitor from the request itself, the same way it does for pageviews. A page can't name another visitor's ID. | | It never replaces a server identity | Once your backend has identified a visitor with the secret key, browser calls for that visitor are ignored. | | It's marked as unverified | Anyone can run JavaScript on their own visit. Visitor profiles mark these identities “Unverified”, and revenue attribution by user ID prefers server identities. | | It returns nothing | The response holds only the caller's own visitor ID, never profile or report data. | | It's rate-limited | 10 calls per minute per IP address and site. Repeating the same call on one page sends it once. | | It works without consent | Nothing is stored in the browser. Without consent the link uses today's daily visitor ID, so it covers today's visit only. With consent it covers the persistent ID. | A browser identify's email never links payments by email. For guest checkouts where only the email is known, call `payment({ email })` on the thank-you page instead; it has its own limits, described in [Attribute by email](https://metricraven.com/docs/attribute-by-email). ## Profile traits | Trait | Limits | | --- | --- | | name | Up to 200 characters. Shown instead of the anonymous visitor name. | | email | A valid email address. Searchable in Visitors. | | image | A profile picture URL: https:// only, up to 500 characters, no user name or password in it. Shown instead of the generated avatar in visitor profiles, Visitors, journeys and the live map. | | Custom traits | Up to 10. Keys use lowercase letters, digits, _ and - (up to 64 characters, starting with a letter). Values are strings (up to 255 characters), numbers or booleans. | The browser tracker drops invalid traits and sends the rest. The server API rejects the whole request instead. Each identify call replaces the stored traits for that visitor. Pictures are loaded by your dashboard straight from the URL you sent, lazily and with no referrer, so the image host never learns which report asked for it; it does see the dashboard viewer's IP address, like any image. A URL that fails to load falls back to the generated avatar. Share links never show pictures. The server API also accepts `image` as a top-level field next to `email` and `name`. ## Identify from your backend 1. **Read the visitor ID in the browser** Call `window.metricraven.getVisitorId()` (also available as `window.analytics`) and send the result to your backend with the signed-in request. 2. **Call Identify from your backend** Set `METRICRAVEN_SECRET_KEY` in your server environment first. Use your own user ID, not a MetricRaven dashboard account ID. `email`, `name` and `traits` are optional. ```bash title="Server terminal · replace example values" curl --request POST 'https://metricraven.com/api/v1/identify' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" \ --header 'Content-Type: application/json' \ --data '{ "visitor_id": "0123456789abcdef0123456789abcdef", "user_id": "user_42", "email": "ana@example.com", "name": "Ana", "traits": { "plan": "pro", "company": "Acme" } }' ``` 3. **Reuse the same user ID everywhere** Use the same user ID in your payment provider metadata and server events. > **Verify it works** > > In **Traffic**, open **Visitors** for a period that includes the visit and search for the user's email or your user ID. Their journey joins visits, goals and payments. The profile lists the user ID and custom traits. ## Details - `getVisitorId()` can return `null` until the first collection request completes. Retry after the browser's `analytics:visitor` event, or defer linking until an ID is available. - Don't send the literal example visitor ID. - Server identify always wins. It replaces a browser identity for the same visitor, including its name, email and traits. - On your backend, take the user ID from your authenticated session, never from a value the browser sends. - You decide which personal data to send. Add name and email only when your privacy policy covers it. Deleting a person in MetricRaven removes their identities too. See [Delete a person's data](https://metricraven.com/docs/delete-person-data). --- # Send verified server events > Report actions your backend can confirm, such as account creation, as goals. Source: https://metricraven.com/docs/server-events Markdown: https://metricraven.com/docs/server-events.md Section: Revenue Send events for actions your backend confirms, such as creating an account or completing onboarding. ## Send an event 1. **Wait until the action succeeds** Use the same user ID as [Identify](https://metricraven.com/docs/identify-users). 2. **POST the event from your backend** ```bash curl --request POST 'https://metricraven.com/api/v1/events' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" \ --header 'Content-Type: application/json' \ --data '{ "name": "signup", "user_id": "user_42", "props": { "plan": "free" } }' ``` Supply `visitor_id` when available, or let MetricRaven resolve the user's existing identity link. From JavaScript or TypeScript, [@metricraven/node](https://metricraven.com/docs/node-sdk) sends the same event with `metricraven.track(…)`. > **Verify it works** > > The event appears in the **Goal** tab at the bottom of Traffic after refreshing. ## Delete goal events `DELETE /api/v1/events` removes goal events (from the tracker or your server), for example test signups. Filter by `name`, `visitor_id` or `user_id`, each optionally between `startAt` and `endAt`, or send only `startAt` and `endAt` to delete every goal event in that range. Pageviews are never deleted. `dry_run=1` only counts. The site secret key works, and so does an account token with the write scope when you are an admin or the owner. ```bash curl --request DELETE "https://metricraven.com/api/v1/events?name=signup&startAt=2026-11-01T00:00:00Z&endAt=2026-11-02T00:00:00Z" \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" ``` The response has `deleted`, `matched`, the filter and the range. Goals, funnels and conversion reports update at once. The CLI asks before deleting: `metricraven goals delete-events --goal signup --from 2026-11-01 --to 2026-11-01`. ## Details - Unlike payments, server events have no deduplication key. Avoid blindly retrying successful submissions. - Server events linked to a visitor later keep their original event time in goals and funnels. --- # Dates, filters & segments > Choose reporting periods and timezones, filter visitors, save segments and read chart metrics. Source: https://metricraven.com/docs/filters-segments Markdown: https://metricraven.com/docs/filters-segments.md Section: Features Pick a period, narrow it to a group of visitors, and save that group as a segment. Dates, currency and filters stay in the URL, so you can bookmark or reload a view. ## Choose dates 1. **Pick a reporting period** In Traffic, choose a period. Choose Custom dates in the period menu for a calendar; both dates are included. 2. **Set the reporting timezone** Open the globe button in the toolbar (**Timezone and currency**) → **Reporting timezone** and search for a city or IANA zone, such as `Europe/Copenhagen`. The default is UTC. Calendar dates, today and yesterday, week and month starts, charts and heatmaps follow it, including in shared links and embeds. 3. **Watch the last 30 minutes** **Last 30 minutes** (shortcut `N`) shows the most recent half hour by minute and moves forward every minute while the dashboard is open. 4. **Choose what to compare with** The **Compare** control next to the date picker applies to Traffic, Revenue and AI visibility: **vs previous period** (the default), **vs same period last year**, **vs custom start** (pick the first day; the window keeps the length of your range) or **No comparison**. It sets the % change on every KPI tile and the change columns, and the chart's grey dashed line follows it: yesterday for Today, the 7 days before for Last 7 days, or the window you chose. Hover a point to see both, with revenue. Hide the line in **Chart options** → **Previous period**. The choice stays in the page address with your filters. 5. **Use All time for full history** **All time** covers all retained history, from the earliest event, payment or crawler request through now. Open **History coverage** to see when each source starts. 6. **Save a favorite period** Click the star beside the period to save it in this browser. Favorites appear first in the period menu; select one and click its star again to remove it. You can keep up to 20. ## Filter visitors 1. **Start a filter** Click a breakdown row, or choose **Filter** in the toolbar and pick a dimension with **is**, **is not**, **contains** or **does not contain**. 2. **Select values** Search the available values and select one or more. Search covers every value in the period, including values outside the first 50 rows. For contains filters you can type your own text. For **Visits**, enter a number with **more than**, **fewer than**, **is** or **is not**: visits each visitor made in the period, where a visit ends after 30 minutes without a pageview. 3. **Apply it** Choose **Apply filter**. Active chips stay visible as you scroll. Click a chip to edit it, use its × to remove it, or choose Clear filters. > **Verify it works** > > Apply **Country is US or GB** and check that the Traffic overview and its Goal and Funnel tabs use that visitor group. Remove the chip to restore the full period. ## How filters combine - Values in one filter use **OR**: Country is US or GB includes visitors from either country. - Separate filters use **AND**: adding Browser is Safari narrows that group to Safari visitors. - Exact matches are case-sensitive. Contains ignores letter case and treats characters such as `.*` literally. - Filters select visitors, then include all their activity in the period. A negative filter excludes anyone who matched any selected value, even if they also had a nonmatching pageview. - Traffic, goals, funnels, journeys and Revenue share these filters. Search Console, crawler and live reports are not narrowed by them. - **Payment filters** (Plan, Payment currency, New or renewal, Refund, Payment provider) test each payment on Revenue. On Traffic they keep the visitors who made a matching payment in the period. - On Revenue, visit filters keep the payments of visitors who match them in the period. Cards that can't be filtered (live subscriptions, Meta ad spend, lifetime value) say so in the card. - AI visibility has its own filters (assistant, prompt, topic, brand named, your mention, cited domain); see [AI visibility](https://metricraven.com/docs/ai-visibility#filters). ## Save a segment 1. **Apply the filters you want to reuse** Then choose **Save segment** next to the active filters. 2. **Name it and save** Give it a unique name and review the filters before saving. 3. **Apply a saved segment** Choose **Segments** to search and apply one. It replaces your current filters; dates, currency and report tab stay the same. 4. **Rename or update it** Use the pencil to rename. To change its filters, first adjust your report filters, edit the segment and select **Replace saved filters with the current report filters**. Review the preview, then choose **Save changes**. Segments belong to a site and are available to all its members: up to 50 per site, with names up to 64 characters. ## Read the traffic chart - Click a headline metric in Traffic overview to chart it; the colored underline marks selected metrics. - Open **More metrics & chart options** for pageviews, new or returning visitors, payments and paying customers. - Click a figure in the top row (or use **Chart options**) to chart purchase and bounce rates, revenue per visitor or session time; click it again to return to visitors and revenue. - Choose **Monthly** in the chart interval dropdown for calendar months. Auto uses monthly buckets for long ranges such as Last year. - Open **View chart data and metric definitions** for exact values. ## Explore breakdowns - Choose **Details · view all** beneath a breakdown to search its complete list. Switch between visitors (pageviews for Top pages) and revenue, and use **Load more**. Details keeps all active filters; country search accepts names such as Denmark as well as codes. - **Traffic → Pages → Hostnames** compares the domains and subdomains where pageviews were recorded. Choose a row to filter visitors who viewed that hostname. - In **Traffic → Geography → Map**, hover or focus a country to see its located visitors, then select it to filter. **Country** shows the ranked list, including small territories absent from the map. - For UTM combinations, see [Campaign & referral links](https://metricraven.com/docs/campaign-links). ## Details - Daylight-saving days can be 23 or 25 hours; repeated hourly groups show their UTC offsets separately. Half-hour timezone changes can create partial hourly groups. Rolling periods such as Last 24 hours keep their exact elapsed duration. Previous-period KPI comparisons use an equal elapsed duration, which can cross different local clock times around DST. The chart's previous-period line instead starts where the previous period starts (for Today, yesterday at midnight) and covers the same elapsed time, so each point lines up with the same clock time. Event storage and collection timestamps remain UTC. - Imported payments can predate visitor tracking; missing historical visits cannot be reconstructed. All time has no previous-period comparison. Long custom ranges are supported; the chart uses coarser intervals when needed to stay readable. - Preset favorites such as Last 7 days follow today and reset the previous-period offset. Custom favorites save the displayed reporting-zone dates, including any previous-period shift. Applying a favorite keeps your filters, currency and chart interval. - Unknown values are included by negative filters. Channel and entry page use the visitor's first pageview in the selected period; new/returning uses their first-ever tracked pageview. - Deleting a segment (the trash button asks first) removes it for everyone but keeps the filters already applied to your report. If another window or teammate changed a segment, reload it before editing to avoid overwriting their changes. - Shared and embedded dashboards can use report filters but cannot read or manage saved segments. A copied report URL includes the applied filters, so it keeps working if a segment is renamed or deleted. - New visitors have no retained pageview before the selected period; returning visitors do, even if that earlier visit had a different country or source. These cohorts stay fixed across the period. Changed or anonymous visitor IDs and expired history can make someone appear new. - Each chart bucket counts distinct visitors and customers, so adding daily counts can count the same person more than once. Monthly buckets are not sums of daily unique counts; the first and last months can be partial, and empty months stay on the axis. - Purchase rate uses only period visitors with a linked positive payment, divided by period visitors, matching Buyers in Insights. A customer who pays without a recorded pageview in these dates still counts toward revenue and customers, but not this rate. Payments exclude trials and full refunds; revenue is net of partial refunds in your selected currency. - A visit (session) is a visitor's activity split after 30 minutes without any event, and counts in the bucket where it starts. Bounce rate is visits with a single pageview divided by visits; session time is the longer of a visit's first-to-last event span and its recorded visible time. Purchase rate needs both a pageview and a linked positive payment in the bucket. Percentages, currency per visitor and seconds use separate scales; these ratios can't be added or averaged without their denominators. Buckets with no visitors show a gap and a dash in the data table, even if a payment or engagement event was recorded there. - Breakdown revenue credits the customer's first tracked visit, which may be before the selected dates. A row can have revenue and zero visitors in this period; those rows can't be used as visitor filters. Outbound links report clicks and visitors; they don't attribute revenue to the destination. - Hostname filters keep the visitor's pageviews on other hostnames in the report. Negative hostname filters exclude anyone who viewed a matching host. Unknown hostnames remain in overall totals. Older records may combine www with the root domain; new collection records them separately while keeping the anonymous daily visitor ID consistent. - Locations are approximate; visits without a known location are omitted from geography breakdowns. Earlier visits recorded without a location may remain unknown. --- # Goals & funnels > Track clicks, visible sections and JavaScript actions as goals, then chain them into funnels. Source: https://metricraven.com/docs/goals-funnels Markdown: https://metricraven.com/docs/goals-funnels.md Section: Features A goal records an action. A funnel shows how many people complete a sequence of actions in order. ## Track clicks and visible sections Add `data-goal` to clickable elements and `data-scroll-goal` to sections. Extra `data-goal-*` attributes become goal properties. ```html
Pricing…
``` ## Track JavaScript actions This helper works both before and after the tracker loads: ```javascript function trackGoal(name, props = {}) { if (window.analytics?.track) { window.analytics.track(name, props); } else { window.analytics = window.analytics || { q: [] }; window.analytics.q.push(["track", name, props]); } } trackGoal("checkout_started", { plan: "pro" }); ``` > **Note:** A click such as `signup_clicked` only records intent. Send `signup` from your backend after the account is actually created; see [Server events](https://metricraven.com/docs/server-events). ## Build a funnel 1. **Open Traffic → Funnel** The conversions card at the bottom of Traffic lists goals, funnels, journeys and customers. 2. **Choose Edit funnels** Select the ordered steps, such as a page, goals and payment. > **Verify it works** > > Perform the action once and refresh Traffic: the goal appears in the Goal tab. A later funnel step counts only when the same tracked visitor completed every earlier step in order. ## Automatic payment-provider goals With a payment provider connected, MetricRaven records subscription changes as goals by itself. They show up in the Goal tab and work in funnels (`event:subscription_started`), filters, the visitor journey and [goal alerts](https://metricraven.com/docs/goal-alerts), credited to the customer's visit. | Goal | Recorded when | | --- | --- | | trial_started | A subscription trial started. | | trial_converted | A trial converted to a paid subscription. | | subscription_started | A paid subscription started, directly or when a trial converted. | | subscription_upgraded | The subscription's recurring amount went up. | | subscription_downgraded | The subscription's recurring amount went down. | | subscription_renewed | A renewal payment was received. | | subscription_cancel_scheduled | A cancellation was scheduled for the end of the billing period. | | subscription_reactivated | A scheduled cancellation was undone, or an ended subscription resumed. | | subscription_ended | The subscription ended. Use it to find churned customers. | | payment_refunded | A payment was refunded, fully or partly (recorded once per payment). | **Revenue → Subscription events** counts each of them for the selected period with the change against the previous one, and the Revenue headline's **Churned** figure is `subscription_ended`. See [Revenue dashboard](https://metricraven.com/docs/revenue-dashboard). | Source | What it records | | --- | --- | | Stripe | Subscription created, updated and deleted events from the regular sync: every goal above. | | Lemon Squeezy | The `subscription_created`, `subscription_updated`, `subscription_cancelled`, `subscription_resumed`, `subscription_expired`, `subscription_paused` and `subscription_unpaused` webhook events. Lemon Squeezy sends no amounts, so no upgrades or downgrades. | | Paddle, Polar, Dodo Payments | Their `subscription.*` webhook events. | | RevenueCat | `INITIAL_PURCHASE`, `RENEWAL`, `CANCELLATION`, `UNCANCELLATION`, `EXPIRATION` and `BILLING_ISSUE`. | | Any provider and the Payments API | `subscription_renewed` from renewal payments and `payment_refunded` from refunds. | Select the subscription events when you set up a provider's webhook; each provider guide lists them. To turn these goals off for a site, open **Settings → Integrations → Subscription goals**, or send `{"lifecycleGoals": false}` to `PUT /api/v1/site`. > **Warning:** These names are reserved. Don't send your own goals with them, or they mix with provider events. - Each change is recorded once: redelivered webhooks and repeated syncs are recognised, and starts and conversions count once per subscription. - The goal is credited to the customer's visit the same way as their payments (visitor ID, user ID, earlier payments, email). If the customer is linked later, earlier goals follow. - Only changes after you connect are recorded; existing subscriptions aren't replayed as new ones. - They don't count toward your monthly event allowance. ## Make a goal your headline KPI 1. **Open Settings → Primary goal** Choose **Tracked goal**. 2. **Enter the exact event name and save** Names are case-sensitive. You can configure it before the first event arrives. The setting applies to this site, including shares and embeds. Choose **Purchase rate (default)** to switch back. Purchase metrics and revenue stay available in either mode. ## Rename, colour and hide goals 1. **Open Settings → Goals & funnels → Goals** Goals tracked in the last 90 days are listed. To set one up before its first event, enter its exact name. 2. **Choose Edit** Set a display name (such as **Signup started** for `signup_started`), a colour, an internal description, or **Hide from the conversions report**. **Reset to defaults** removes them all. Display names and colours show up in the Goal tab, its chart, funnel steps, the goal detail view and alert emails, including on shares and embeds. Descriptions are for your team and never appear on shares. A hidden goal is still tracked, counted and usable in funnels, filters and alerts; the Goal tab offers **Show hidden goals** to bring it back temporarily. > **Note:** Customising a goal never renames the event. Tracking code, filters, funnels, alerts, the API and exports keep using the raw, case-sensitive name. Admins and owners can change goals. With the API, use an [account token](https://metricraven.com/docs/api-authentication) (read to list, write to change) or the site’s secret key, with `?site=`: | Request | Does | | --- | --- | | GET /api/v1/goals/customizations | Customised goals (goal, displayName, color, description, hidden, updatedAt) and the allowed colors. Optional goal=\; include=tracked adds tracked, the goal names seen in 90 days. | | PUT /api/v1/goals/customizations | Body: goal plus any of displayName (up to 80 characters), color (a hex from colors), description (up to 300) and hidden. Send only what changes; null or an empty string clears a field. | | DELETE /api/v1/goals/customizations?goal=\ | Resets the goal to its raw name, default colour and visible. 404 when it has no customisation. | ```bash curl -X PUT "https://metricraven.com/api/v1/goals/customizations?site=$SITE_ID" \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" \ --header "Content-Type: application/json" \ --data '{"goal":"signup_started","displayName":"Signup started","color":"#3b82f6"}' ``` Reports keep the raw `name` and add `displayName`, `color`, `description` and `hidden` next to it: each goal in `/stats/conversions` (plus `hiddenGoals`), event funnel steps, `/stats/goal` and the primary goal in `/stats/traffic`. The CLI has `analytics goals list | set | reset | colors`, and the MCP server has matching tools. ## Details - Goal names start with a letter and may contain letters, numbers, underscores, colons, dots and hyphens. Each event accepts up to ten string, number or boolean properties. Keep secrets out of names, URLs and properties. - Scroll goals fire when the share in `data-scroll-threshold` is visible (default 0.5), after an optional `data-scroll-delay` in milliseconds (default 0), once per element per page. - The primary goal counts distinct tracked visitor IDs with both a pageview and the goal in the period, after filters. Repeated goals count once; server events can use linked visitor IDs, including links added later. Goal-only accounts without a period pageview are excluded. - Conversion rate divides converted visitors by period visitors; with no visitors the rate is unavailable. It is a period association regardless of which action came first; use a funnel when order matters. Changes compare with the preceding equal-duration period; All time has no comparison. - Funnel steps use recorded timestamps within the period. Equal timestamps are allowed because separate events can share a millisecond. One pageview can satisfy different overlapping steps, such as `visit` and `page:/pricing`; repeating the same step needs a separate occurrence. Page steps match a literal path prefix. - Server events linked to a visitor later keep their original event time. The `identify` step uses the stored first-link and latest-update timestamps, so it is not a complete login history; track an explicit login goal if you need every login. - Counts use tracked visitor IDs; linking a user may associate server actions with several visitor IDs. - The `payment` step counts positive net payments in the selected currency, excluding trials and full refunds. Value per visitor is the period's net revenue for visitors who reached that step, not a claim that the step caused those payments. - A site can customise up to 500 goals. Colours come from a fixed palette that reads on both light and dark dashboards. The Goal tab's chart draws the top goals by visitors before hiding, so hiding one of them leaves fewer lines rather than adding the next goal. --- # Explore visitors > Search the visitors in a reporting period and open their complete journey. Source: https://metricraven.com/docs/visitor-explorer Markdown: https://metricraven.com/docs/visitor-explorer.md Section: Features List the visitors behind any report, search them, and open one to see their journey. ## Find a visitor 1. **Set up the report** Choose dates, filters and currency in Traffic, then click **Visitors** in Traffic overview. The list uses that report's exact interval and filters. 2. **Search and sort** Search a name, email, visitor ID or your application's account ID. Choose **Payment matched** or **No matched payment**, and sort by first pageview, latest pageview or pageview count. The next 50 visitors load as you scroll to the end of the list; **Load more visitors** does the same. 3. **Narrow it down** **Filter visitors** adds search filters on top of the report's own, without changing the dashboard: a visited page, a completed goal, the referrer, channel, UTM tags, country, device, browser or OS, and **Visits** more than, fewer than or exactly a number. For example, visitors who saw /pricing, signed up and came back more than twice. 4. **Open a visitor** You see their retained journey, which can include linked visitor IDs and dates outside this period. Close it to return to the list. ## Activity grid Above the timeline, a grid shows the days this person was active: one column per week, Monday to Sunday from top to bottom. Darker squares mean more pageviews and goals that day, and a dot marks days with a payment. Hover or focus a day to see its pageviews, visits, goals and payments, and click it (or press Enter) to jump to that day in the timeline. Arrow keys move between days. Days follow the report's timezone, as do the timeline's dates and times. The grid covers up to the last 26 weeks: it starts at the week of the first visit, or 26 weeks back for longer histories. For someone last seen more than 26 weeks ago, it ends on their last active day instead. Names and emails come from the current recorded identity; see [Link users to visits](https://metricraven.com/docs/identify-users). ## Details - Each row is a tracked visitor ID with a pageview in the period. One person can have several IDs, and cookieless IDs rotate daily. - First and latest pageviews are within the selected dates; source, country, device and entry page come from the first pageview in the period. - Search and sorting cover all matching visitors, not just the loaded page. - Visits are counted in the selected dates: a visit ends after 30 minutes without a pageview. The same **Visits** filter works across every report (Traffic, goals, revenue). - From the API, pass the same search as plain parameters (visitedPage, completedGoal, visits_gt, …); see [Visitors API](https://metricraven.com/docs/visitors-api#search). - Payment matched means a positive net transaction linked by the visitor ID or the transaction's account ID, in the selected dates and currency. Trials, full refunds and zero net amounts don't qualify. - A customer who paid without a pageview in the period stays in revenue and customer totals but is absent from this list. These are the same paying-visitor rules as Traffic and buyer Insights. - The activity grid counts the same linked visitor IDs and payments as the journey. A visit counts on the day it started (30 idle minutes starts a new one); goals are custom events. - Visitor lists, profile search, journeys and activity grids are private to site members and secret-key access. Public share and embed links show aggregate reports only. The [live demo](https://metricraven.com/demo) is the one exception: its visitors are sample data, so you can open the visitor list and click any visitor to see their journey there. --- # Realtime visitors > See who is on your site right now on a globe or flat map, follow the live activity feed, replay the last 8 hours and estimate what the visitors online are worth. Source: https://metricraven.com/docs/realtime Markdown: https://metricraven.com/docs/realtime.md Section: Features The realtime view shows the visitors active on your site in the last five minutes: where they are, what they are reading, where they came from and what they just did. ## Open realtime In **Traffic**, the **Online** tile in Traffic overview counts the visitors active in the last five minutes. Click it, or **Open realtime globe** in the live card further down, to open the globe. On phones, use the floating realtime button with the live dot. The live card itself shows the same count, a bar chart of the last 30 minutes and, under **Recent visitors & activity**, the active visitors and their latest pageviews, goals and payments. ## Globe and map Each marker is a place with active visitors; bigger markers mean more people there. The panel in the top left shows how many visitors are on your site now. The toolbar next to it has these controls: | Control | What it does | | --- | --- | | Replay | Plays back the last 8 hours (see below). Click again to return to live. | | Globe / flat map | Switches between the 3D globe and a flat world map. | | Map style | Solid, Dots or Outline land. Your choice is remembered in this browser. | | Auto-rotate | Turns the globe slowly and flies to new activity as it arrives. | | Visitor list | A text list of everyone active, with the same details as the markers. | | Full screen | Fills the whole screen with the globe. | Click a marker to see who is there: their device, browser and operating system, the page they are on and the source they came from. Members of the website can click a visitor to open their journey, as in the [visitor explorer](https://metricraven.com/docs/visitor-explorer). ## Filter by referrer, country or device Under the visitor count, **Referrers**, **Countries** and **Devices** list what the people online have in common, with a count for each. Click one to show only those visitors on the map, in the list and in the feed; click it again to remove it. Filters combine, and each row counts within the filters picked in the others. **Clear filters** removes them all. These filters only apply to realtime, never to your reports. ## Live activity feed The bottom left corner shows the latest activity as it happens: pageviews, goals and payments, newest at the bottom. Click an item to jump to that visitor on the globe. New visits appear on their own; there is nothing to refresh. ## Replay the last 8 hours **Replay** steps through the last 8 hours in 15-minute windows. Press play to watch them in order, or drag the slider to a window; the bars above it show how busy each one was. For the window shown, the panel lists how many visitors were active and its top referrers, countries and devices. Replay shows approximate locations and counts only, not individual visitors, so the visitor list is off while it runs. ## Value of the visitors online When your site has revenue, the count reads, for example, **12 visitors on Acme (est. value: $86)**. The estimate is the number of visitors online times your revenue per visitor over the last 30 days: net revenue in the report currency divided by the visitors in those 30 days. Only payments in that currency count, and without revenue in it there is no estimate. Hover the value to see the numbers behind it. ## From the API The same data is available with a secret key or an account token: [GET /stats/live](https://metricraven.com/docs/reports-api#live) for visitors online and recent activity, [GET /stats/live/replay](https://metricraven.com/docs/reports-api#live-replay) for the 15-minute windows and [GET /stats/live/value](https://metricraven.com/docs/reports-api#live-value) for revenue per visitor. ## Details - Realtime covers the last five minutes, refreshes every 15 seconds and ignores the report's dates and filters. - Locations are approximate IP locations rounded to 0.1° (Cloudflare and DB-IP). Visitors without a location still count toward the total. - On busy sites the globe, filters and list use the 500 most recently active visitors; the total still counts everyone. - Realtime is private to the website's members: share links and embeds don't include it. The [live demo](https://metricraven.com/demo) shows it with sample visitors. --- # Buyer insights > Compare paying visitors with everyone else, see when people buy and what they did first. Source: https://metricraven.com/docs/buyer-insights Markdown: https://metricraven.com/docs/buyer-insights.md Section: Features Choose a site, period and currency in **Traffic**, then open **Insights**. It uses the same dates and visitor filters. ## Buyers Compare paying visitors with period visitors. Repeat payments count once per visitor ID; distinct customers and transactions are shown separately. Country and source rankings use each visitor's first pageview in the period, need at least 10 visitors per group, and rank all groups before showing the top 10. They are observed rates, not a statistical significance test. ## Purchase heatmap - Compare **Payments**, **Customers** or **Revenue** by weekday and hour in your reporting timezone. - Hover or select a cell for counts, use arrow keys while focused, or choose **Show data table**. ## Purchase timing See each customer's first observed positive payment in the selected currency, the median days and visits to buy, and goals recorded in the 30 days before payment. Each customer counts once per goal; this shows association, not what caused a sale. ## Sources Compare source totals with the immediately preceding interval of equal duration. ## Details - Customers without a period pageview can still contribute payments and revenue. - Customers can appear in several heatmap cells, so cell totals need not equal distinct customers. - Timing only includes customers whose first observed positive payment falls in this period. Customers without known pre-purchase pageviews are counted separately and excluded from timing and visit percentages. - An observed visit starts after more than 30 minutes between pageviews. Imported or missing history, daily visitor IDs and consent choices limit these observations; they don't establish a first-ever purchase or a prediction. - Average daily visitors sums daily distinct counts and divides by reporting-zone calendar days, including zero-traffic and partial boundary days. - Insights excludes free trials and fully refunded or zero/net-negative payments. Partial refunds reduce revenue. Visitor filters restrict payments through linked visitor IDs. - Shared dashboards receive these aggregates, without identities or individual journeys. --- # Campaign & referral links > Tag links with UTM and ref/source/via parameters, compare them in Acquisition and Campaigns, and see how channels such as AI and Affiliate are grouped. Source: https://metricraven.com/docs/campaign-links Markdown: https://metricraven.com/docs/campaign-links.md Section: Features Add tags to links that send people to your website, then compare them in Traffic. ## Tag your links ```html title="Tagged links to your website" Campaign link Referral link ``` ## Where they appear - **Traffic → Acquisition → UTM**: each UTM source, medium, campaign, term and content value. - **Traffic → Acquisition → Ref**: choose ref, source or via to compare. Each is recorded separately; link source is independent of UTM source. - **Traffic → Campaigns**: each complete combination of the five UTM tags. A dash means the tag was absent. - **Revenue → Paid acquisition**: Meta Ads and Google Ads spend per campaign, ad group and ad next to the revenue it brought. See [Meta Ads](https://metricraven.com/docs/meta-ads) and [Google Ads](https://metricraven.com/docs/google-ads) for the URL parameters to add to your ads. ## How channels are grouped Each visitor's first pageview in the period gets one channel, in this order. The first rule that matches wins. - **Paid**: utm_medium contains cpc, ppc, paid, display or cpm (so `paid-social` counts), or the link has an ad click ID (gclid, fbclid, msclkid…). Only which network the ID belongs to is kept, never the ID itself. Facebook also adds fbclid to links in organic posts. - **Email**: utm_medium email or newsletter, or a newsletter/email tool as the source. - **Affiliate**: utm_medium affiliate or partner, or a `via` link parameter. - **AI**: a referrer or source from an AI assistant: chatgpt.com, chat.openai.com, perplexity.ai, claude.ai, gemini.google.com, copilot.microsoft.com, you.com, phind.com, poe.com, deepseek.com, meta.ai and grok.com. This includes `utm_source=chatgpt.com`, which ChatGPT adds to links it cites. - **Social**: a social network as the source or referrer, or utm_medium social. - **Organic Search**: a search engine referrer (Google, Bing, DuckDuckGo, Yahoo, Ecosia, Brave…). - **Referral**: any other referring website. - **Campaign**: tagged links (UTM or ref/source) with no referrer. - **Direct**: no referrer and no tags. Filter by any channel from the toolbar's **Filter** menu or by clicking a row in Acquisition → Channel. ## Filter by a campaign 1. **Click a row or use Filter** Details searches every recorded value and can sort by revenue. 2. **Filter an exact combination** In Acquisition → **Campaign**, choose **All UTM combinations** and click **Filter** on a row, or choose **Campaign combination** in the toolbar's **Filter** menu. Select several combinations, or use **is not** to exclude them. 3. **Save it as a segment** Reuse a campaign filter later from Segments. > **Verify it works** > > Open a tagged link to your site and check Acquisition and Campaigns for the same values. If your own browser is excluded, use one that allows tracking. ## Details - The Primary referral tag view uses ref first, then via, then source. It also includes older visits whose original parameter is unknown; older combined values can't be split into their original parameters. - Reload an already open tracked page to load the current script. - Parameter revenue credits the customer's first tracked visit, even when they paid later. UTM and referral metadata are still sent when URL query stripping is enabled. - Combination filters match all five tags on one pageview. Separate UTM filters may match different pageviews from the same visitor. - Search in Campaigns matches any of the five tags; sorting and Load more cover all combinations. Visitors can appear in several combinations, so row counts are not additive. - Combination revenue credits the first tracked combination. It can appear on a row without visitors in these dates, which can't be used as a visitor filter. --- # Meta Ads spend and ROAS > Connect a Facebook and Instagram ad account to see spend, attributed revenue, ROAS and cost per customer for every Meta campaign, ad set and ad. Source: https://metricraven.com/docs/meta-ads Markdown: https://metricraven.com/docs/meta-ads.md Section: Features See which Facebook and Instagram campaigns bring paying customers: Meta's spend next to the visitors, customers and revenue MetricRaven attributes to each campaign, ad set and ad. ## Before you start - The [tracking script](https://metricraven.com/docs/install) on every landing page. - Revenue from a [payment provider](https://metricraven.com/docs/stripe) or the [Payments API](https://metricraven.com/docs/payments-api). Without revenue you still get spend, visitors and cost per visit, but no ROAS. - A Facebook account with access to the ad account in Meta Business Settings. ## Connect Meta Ads 1. **Open Settings → Integrations → Meta Ads** Choose **Connect Meta Ads**. Website admins and owners can connect; viewers see the status. 2. **Sign in with Facebook and allow ads_read** MetricRaven asks only for read access to ad performance (`ads_read`). It can't create, edit or pay for ads. 3. **Choose the ad account** Each website tracks one ad account. MetricRaven imports the last 90 days of spend, impressions and clicks per ad and day, then syncs once a day. **Sync now** fetches the latest numbers. 4. **Add the URL parameters to your ads** See below. Visits only match a campaign when its ads carry these tags. 5. **Open Revenue → Paid acquisition** Spend, attributed revenue, ROAS, cost per customer and your top campaigns for the period. A **ROAS** tile joins the Revenue headline figures. **Full table** switches between campaigns, ad sets and ads. Click a campaign for its daily spend against the revenue it brought, its ad sets and ads, and its customers (each opens their journey). On Traffic, Acquisition → Campaign links here (**Ad spend & ROAS on Revenue**). > **Verify it works** > > The settings panel shows the ad account, its currency and timezone, and the last synced day. Click one of your own ads, then check that the visit appears on its campaign row. ## Recommended URL parameters In Ads Manager, open each ad (or select several and edit them together), go to **Tracking → URL parameters** and paste: ```bash title="URL parameters (IDs, recommended)" utm_source=meta&utm_medium=paid-social&utm_campaign={{campaign.id}}&utm_term={{adset.id}}&utm_content={{ad.id}} ``` Meta fills in the `{{…}}` values when someone clicks. IDs never change when you rename a campaign, so history stays matched. If you prefer readable values, names work too (a campaign, ad set or ad still matches the names it had in synced days before a rename, and is shown under its current name): ```bash title="URL parameters (names)" utm_source=meta&utm_medium=paid-social&utm_campaign={{campaign.name}}&utm_term={{adset.name}}&utm_content={{ad.name}} ``` > **Note:** `utm_medium=paid-social` puts these visits in the **Paid** channel. Keep one template on every ad so all of them are matched the same way. ## How visits are matched to campaigns For each visit, the strongest signal wins: - `utm_content` equal to an ad ID: that ad, its ad set and campaign. - `utm_campaign` equal to a campaign ID, or to a campaign name (ignoring case). `utm_term` then picks the ad set and `utm_content` the ad, by ID or name. - `utm_term` equal to an ad set ID: that ad set and its campaign. - Meta-looking traffic (`utm_source` meta, facebook, fb, instagram, ig…, or an `fbclid` click) whose `utm_campaign` contains a campaign name. The longest matching name wins. Paid Meta visits that match no synced campaign (for example a click with only `fbclid`, or Meta tags with an unknown campaign) appear on their own line below the totals, **Unattributed Meta clicks (fbclid only, may include organic shares)**, with their visitors, customers and revenue but no spend or ROAS. Facebook also adds `fbclid` to links shared in organic posts, so these visits can't be told apart from ad clicks: they are **left out of the totals, ROAS and cost per customer**. Tag your ads to keep this line small. ## Metrics | Column | Meaning | | --- | --- | | Spend | Meta's reported spend, converted into your reporting currency. | | Link clicks | Clicks to the ad's destination (Meta's inline link clicks). | | Ad visitors | Visitors who arrived from this campaign, ad set or ad in the period, each counted once (on their first visit from it). | | Customers | Distinct paying customers whose first tracked visit matched this row, for payments in the period. | | Revenue | Those payments, net of refunds, in your reporting currency (first touch). | | ROAS | Revenue ÷ spend. 2.50x means each unit spent brought back 2.50 in revenue. | | Cost / customer | Spend ÷ customers. | | Assisted revenue | Revenue in the period from customers who clicked this row's ads before paying but whose first tracked visit came from somewhere else (another campaign, search, a newsletter…). Not part of Revenue or ROAS. | **Why ad visitors and revenue can disagree**: revenue is credited to the customer's first tracked visit, the same rule as the rest of the dashboard, even when that visit was before the period. Ad visitors are the people who arrived from the ad during the period. A campaign can have many ad visitors and little revenue (they haven't paid yet, or first came from elsewhere: see Assisted revenue), or revenue from customers who first clicked months ago. **Assisted revenue** counts each payment on every row whose ads the customer clicked before paying, except the row that already gets it as first touch, so rows don't add up to the total. The total counts payments whose first touch was no campaign of the ad account at all, once each. Totals add every campaign row; customers are counted once. Unattributed Meta clicks are not included. Revenue uses the same first-touch rule as the [Campaigns table](https://metricraven.com/docs/campaign-links), so the two never double count: a customer whose first visit came from search and a later one from a Meta ad is credited to search. Revenue includes renewals, like the rest of the dashboard. A visitor counts once, on their first visit in the period that matches a campaign or the unattributed line; an untagged or organic Facebook visit before it doesn't hide it. Ratios show — when their denominator is zero. The API also returns cost per link click. ## Currency and timezone Spend arrives in the ad account's currency. When it differs from your reporting currency, each day is converted at that day's European Central Bank reference rate, like payments (each payment at its own day's rate), so ROAS compares both in one currency. Days without a rate yet are left out of spend, ROAS and cost per link click, and the table says how much. Meta reports whole days in the ad account's timezone, while your period follows the dashboard's timezone. An ad account day counts in the period that contains its midpoint (noon in the ad account's timezone), so a one-day period always gets one day of spend and two neighbouring periods never share a day. A period that runs up to now also includes the ad account's current day, spend so far. When both timezones are the same, the days are exactly the dates you selected. The table shows which days it summed. Rolling periods get as many ad account days as the dashboard period has, counting today's partial day as one: **Last 7 days** (the last 7 × 24 hours, as on the traffic report) is today and the 6 days before it, never 8 days of spend. **Last 30 days** is 30 days, **Last 24 hours** is today. Calendar periods (Today, This week, This month, a custom range) keep every day they contain. ## API, MCP and CLI - REST: `GET /api/v1/stats/ads?provider=meta` (or the alias `/api/v1/stats/meta-ads`) with the usual period parameters and `level=campaign|adset|ad`; `provider=all` combines every connected network. See the [Reports API](https://metricraven.com/docs/reports-api#ads). - MCP: `get_ad_spend` (every network) or `get_meta_ads`. - CLI: `metricraven analytics ads [campaign|adgroup|ad] --provider meta` or `metricraven analytics meta-ads [campaign|adset|ad]`. ## Troubleshooting - **No ad accounts listed**: the Facebook login you used has no access to the ad account. Ask for access in Business Settings, then choose **Reconnect Meta**. - **ROAS is empty**: there is no spend in the period, or no attributed revenue yet. Check the payment integration and that ads carry the URL parameters. - **Connection expires**: Meta user access lasts about 60 days and can only be renewed by signing in again. Settings → Integrations → Meta Ads shows **Connection expires on** the date, turns into a warning 7 days before, and admins and the owner get an email then and again when it has expired. Choose **Reconnect Meta**: your ad account and history are kept and the sync catches up. - **Numbers differ from Ads Manager**: Meta counts modeled and view-through conversions from its pixel. MetricRaven counts tracked visitors and payments only, so revenue is usually lower and closer to what you were paid. ## Details - Disconnecting deletes the imported spend and the stored Meta token. Visits and payments stay. - Switching ad accounts removes the old account's spend and imports 90 days of the new one. - Each sync re-reads the last three days, because Meta can still adjust recent spend, and replaces them (no double counting). Paused, archived and deleted ads are imported too, so spend matches the account total in Ads Manager. - Very large ad accounts: when Meta answers that a request is too large, times out, or a window has more rows than one read allows, the sync runs the same query as one of Meta's asynchronous report jobs and reads the result when it is ready. - Traffic filters don't apply to this table: spend can't be split by visitor properties. - The public demo shows a synthetic ad account with sample spend. --- # Google Ads spend and ROAS > Connect a Google Ads account to see spend, attributed revenue, ROAS and cost per customer for every Google campaign, ad group, ad and Performance Max asset group, next to Meta Ads. Source: https://metricraven.com/docs/google-ads Markdown: https://metricraven.com/docs/google-ads.md Section: Features See which Google Ads campaigns bring paying customers: Google's spend next to the visitors, customers and revenue MetricRaven attributes to each campaign, ad group and ad, with combined totals when Meta Ads is connected too. ## Before you start - The [tracking script](https://metricraven.com/docs/install) on every landing page. - Revenue from a [payment provider](https://metricraven.com/docs/stripe) or the [Payments API](https://metricraven.com/docs/payments-api). - A Google account with access to the Google Ads account, directly or through a manager account (MCC). ## Connect Google Ads 1. **Open Settings → Integrations → Ads → Google Ads** Choose **Connect Google Ads**. Website admins and owners can connect; viewers see the status. 2. **Sign in with Google and allow Google Ads access** Google asks for access to your Google Ads data in its own consent step. MetricRaven only reads reports: it never creates, edits or pays for ads. This is separate from Search Console: connecting or disconnecting Google Ads doesn't change your Search Console connection. 3. **Choose the Google Ads account** Accounts you reach through a manager account are listed with **via manager**. Each website tracks one account. MetricRaven imports the last 90 days of spend, impressions and clicks per ad and day, then syncs once a day. **Sync now** fetches the latest numbers. 4. **Add the final URL suffix** See below. Visits only match a campaign when its clicks carry these tags. 5. **Open Revenue → Paid acquisition** With more than one network connected, a switch shows **All** (combined spend, revenue, ROAS and cost per customer) or one network. **Full table** lists campaigns, ad groups and ads of every network. > **Verify it works** > > The settings panel shows the account, its currency and timezone, and the last synced day. Click one of your own ads, then check that the visit appears on its campaign row. ## Final URL suffix In Google Ads, open **Admin → Account settings → Tracking → Final URL suffix** and paste: ```bash title="Final URL suffix (account)" utm_source=google&utm_medium=cpc&utm_campaign={campaignid}&utm_term={adgroupid}&utm_content={creative} ``` Google fills in the ValueTrack values on every click: `{campaignid}` is the campaign ID, `{adgroupid}` the ad group ID and `{creative}` the ad ID. IDs never change when you rename a campaign, so history stays matched. Auto-tagging (`gclid`) can stay on. Performance Max campaigns have no ad groups or ads. Set this suffix on each Performance Max campaign (**Campaign settings → Campaign URL options → Final URL suffix**), which overrides the account's: ```bash title="Final URL suffix (Performance Max)" utm_source=google&utm_medium=cpc&utm_campaign={campaignid}&utm_content={assetgroupid} ``` > **Note:** `utm_medium=cpc` puts these visits in the **Paid** channel. MetricRaven also recognises `utm_source` adwords, google_ads and googleads, and the mediums ppc and paid. ## How visits are matched to campaigns - `utm_content` equal to an ad ID (or a Performance Max asset group ID): that ad, its ad group and campaign. - `utm_campaign` equal to a campaign ID or name; `utm_term` then picks the ad group and `utm_content` the ad. - `utm_term` equal to an ad group ID: that ad group and its campaign. - Google traffic (`utm_source=google` with a paid medium, or a `gclid`, `gbraid` or `wbraid` click) whose `utm_campaign` contains a campaign name. A visit tagged for another network (`utm_source=facebook`, an `fbclid`) never matches a Google campaign, even with the same campaign name. Google clicks that match no synced campaign (a click with only `gclid`, `gbraid` or `wbraid`, or Google tags with an unknown campaign) appear on their own line, **Unattributed Google clicks**, and are left out of totals, ROAS and cost per customer. ## Metrics The same as [Meta Ads](https://metricraven.com/docs/meta-ads#metrics): spend, revenue (first touch), ROAS, customers, cost per customer, ad visitors, assisted revenue and clicks (Google's clicks are clicks to your site). Spend is `metrics.cost_micros` ÷ 1,000,000 in the account's currency, converted at ECB rates. Spend outside ads and asset groups (campaign types without ads) is kept as **Campaign-level spend** on its campaign, so totals match the account. With several networks, **All** adds their spend and revenue, counts each customer once, and sums ad visitors. | Google Ads | MetricRaven | | --- | --- | | Campaign | Campaign | | Ad group | Ad group (Meta: ad set) | | Ad | Ad | | Performance Max asset group | Ad group and ad | ## Currency, timezone and days Google reports whole days in the account's timezone; days are summed the same way as [Meta Ads](https://metricraven.com/docs/meta-ads#currency) (Last 7 days is 7 account days, today included). Each sync re-reads the last 7 days, because Google can still adjust recent spend (invalid click credits). Removed ads are imported too. ## API, MCP and CLI - REST: `GET /api/v1/stats/ads?provider=google` (or `provider=all` for every network). See the [Reports API](https://metricraven.com/docs/reports-api#ads). - MCP: `get_ad_spend` with `provider: "google"`. - CLI: `metricraven analytics ads --provider google`. ## Troubleshooting - **No accounts listed**: the Google login has no access to a Google Ads account. Ask for access in Google Ads (Admin → Access and security) or use the login of its manager account, then **Reconnect Google Ads**. - **"Still limited to test accounts"**: MetricRaven's Google Ads API access is waiting for Google's approval. The connection stays and syncing starts by itself once approved. - **Access revoked or expired**: the panel asks you to reconnect (for example after you removed MetricRaven in your Google Account). History is kept. - **Numbers differ from Google Ads**: Google counts modeled conversions and its own attribution windows. MetricRaven counts tracked visitors and payments only. ## Details - Disconnecting deletes the imported spend and the stored Google token. It doesn't revoke the token at Google, because that would also end your Search Console access; remove MetricRaven in your Google Account (Security → Third-party access) to revoke everything. - Switching accounts removes the old account's spend and imports 90 days of the new one. - Traffic filters don't apply to ad spend: spend can't be split by visitor properties. - The public demo shows a synthetic Google Ads account next to the Meta sample. --- # Connect Google Search Console > Authorize read-only Search Console access to see Google keywords, clicks and estimated revenue. Source: https://metricraven.com/docs/search-console Markdown: https://metricraven.com/docs/search-console.md Section: Features See the Google queries that bring visitors, with clicks, impressions, CTR and position. ## Connect 1. **Verify your website in Google Search Console** In [Google Search Console](https://search.google.com/search-console), using a Google account that can read that property. 2. **Open Settings → Integrations → Search Console** Choose **Connect Google Search Console**. 3. **Grant read-only access** Pick the Google account with property access. This is separate from signing in to MetricRaven with Google. 4. **Choose the matching property** Only properties that account can access and that match your site's domains are listed. 5. **Open Traffic → Acquisition → Keyword** The Keyword tab ranks your Google queries by clicks, or by estimated revenue with the card's sort button. **Details** opens the full Search Console report with landing pages, impressions, CTR and average position. Select a period old enough for Google to have processed the data. > **Verify it works** > > Connected means the property was verified against your Google access. Clicks, impressions, CTR and average position appear when Google has data; fresh sites can be empty. ## No property listed? Check the Google account, property permissions and the domains in **Settings → General → Site**. Domain properties use `sc-domain:example.com`; URL-prefix properties can include `https://www.example.com/`. ## Details - Google's data usually lags by a few days. - Revenue per keyword is an estimate: a landing page's attributed organic-search revenue is divided among its keywords in proportion to their clicks. Keyword estimates are not individual purchase attribution. - Google retains up to 16 months of history and reports inclusive calendar dates in Pacific time. This card labels its own window separately from your reporting timezone, including when your selection exceeds Google's retained history. Estimated revenue uses payments in that same Pacific window. - Google omits some queries. - Search Console data lives in one place, the Keyword tab, next to the other acquisition sources, so it isn't repeated as a separate card. Visitor filters don't apply to Google's data. --- # Bots & AI crawlers > Track GPTBot, ClaudeBot, Perplexity and other crawlers from your server with @metricraven/ai-crawl and an ingest-only bot token, or report them through the API. Source: https://metricraven.com/docs/bots-ai-crawlers Markdown: https://metricraven.com/docs/bots-ai-crawlers.md Section: Features Most crawlers don't run the browser script. `@metricraven/ai-crawl` reports their requests from your server or edge proxy, so you can see which AI and search bots read which pages. ## Install the package 1. **Add the package** ```bash npm install @metricraven/ai-crawl ``` 2. **Set the bot token on the server** In **Settings → Tracking & install → Bot traffic**, create a [bot token](https://metricraven.com/docs/bot-traffic-settings#bot-token) and store it as `METRICRAVEN_BOT_TOKEN`. It can only report crawler requests, so it's the safe credential for middleware and edge code. On Workers, add it as a secret binding. ```bash # .env on the server (Settings → Tracking & install → Bot traffic → Create bot token) METRICRAVEN_BOT_TOKEN=mrbot_… # Optional: refuse reports if the token belongs to another website METRICRAVEN_SITE_ID=ana_pub_… ``` The site secret key (`METRICRAVEN_SECRET_KEY`) also works. If both are set, crawler reports use the bot token. Reported URLs must be on one of the site's domains. 3. **Add it to your server** ```typescript title="Next.js proxy" // proxy.ts (Next.js 16+). On Next.js 15 and earlier: middleware.ts with // export const middleware = withAiCrawl(); import { withAiCrawl } from "@metricraven/ai-crawl/next"; export const proxy = withAiCrawl(); export const config = { matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], }; ``` ```typescript title="Wrap an existing proxy" // Already have a proxy? Wrap it; its response is returned unchanged. export const proxy = withAiCrawl(async (request) => { // your existing logic return NextResponse.next(); }); ``` ```typescript title="Express / Connect" import { aiCrawl } from "@metricraven/ai-crawl/express"; app.use(aiCrawl()); // reports after the response, with its status ``` ```typescript title="Hono (Workers, Bun, Deno, Node)" import { aiCrawl } from "@metricraven/ai-crawl/hono"; app.use(aiCrawl()); // Workers: reads the METRICRAVEN_BOT_TOKEN binding from c.env ``` ```typescript title="Any Fetch handler" import { trackCrawl } from "@metricraven/ai-crawl"; export default { async fetch(request, env, ctx) { const response = await handle(request); // env.METRICRAVEN_BOT_TOKEN: wrangler secret put METRICRAVEN_BOT_TOKEN trackCrawl(request, response, { env, waitUntil: (p) => ctx.waitUntil(p) }); return response; }, }; ``` > **Verify it works** > > Simulate a crawler with `curl -A "GPTBot/1.2" https://your-site.com/`, then open **Bots & AI crawlers**. The request appears as GPTBot (OpenAI, AI training). The package only sends a request to MetricRaven when the user-agent matches a known crawler. It never delays the response: reports go out in the background, through `waitUntil` when the runtime has one. ## Options | Option | What it does | | --- | --- | | types | Crawler types to report. Defaults to all. Pass AI_CRAWLER_TYPES for AI training, AI search and AI assistants only. | | methods | Defaults to GET and HEAD. | | trackAssets | Also reports scripts, styles, images, fonts and /_next/ files. Off by default; robots.txt, llms.txt, sitemaps and .md files are always reported. | | ignore | ({ url, userAgent, crawler }) => boolean, to skip requests. | | clientIp | (request) => string, to read the crawler's IP yourself, or false to never send it. Default: CF-Connecting-IP, X-Vercel-Forwarded-For, Fly-Client-IP, X-Real-IP, then the first X-Forwarded-For entry (Express: then req.ip). | | botToken, siteId | Default to METRICRAVEN_BOT_TOKEN and METRICRAVEN_SITE_ID (Workers: the env bindings). | | secretKey, apiHost, timeoutMs, onError | Client options, as in the Node.js SDK. | ## Send crawler requests with the API For other languages, or a server you control fully, call the endpoint directly. 1. **Capture the original request** In your server or edge middleware, read the request's user-agent, URL and response status. 2. **POST it with your bot token** ```bash curl --request POST 'https://metricraven.com/api/v1/crawls' \ --header "Authorization: Bearer $METRICRAVEN_BOT_TOKEN" \ --header 'Content-Type: application/json' \ --data '{ "url": "https://example.com/pricing", "userAgent": "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", "status": 200 }' ``` The example simulates a crawler. Replace the user-agent, URL and status with the real values. Optional fields: `ip`, and `siteId` to check the token's website. The secret key works the same way: ```bash curl --request POST 'https://metricraven.com/api/v1/crawls' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" \ --header 'Content-Type: application/json' \ --data '{ "url": "https://example.com/pricing", "userAgent": "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", "status": 200 }' ``` MetricRaven classifies supported bots and AI crawlers from the user-agent. Requests from normal browsers are ignored. Send `ip` whenever you can: it is what lets MetricRaven verify the crawler. ## What gets recorded | Category | Examples | | --- | --- | | AI answers | ChatGPT-User, Claude-User, Perplexity-User, Google-Agent, MistralAI-User: an assistant opening a page because a person asked. | | Indexing | Googlebot, Bingbot, OAI-SearchBot, Claude-SearchBot, PerplexityBot, Amzn-SearchBot: search and AI search indexes. | | Training | GPTBot, ClaudeBot, Google-CloudVertexBot, Applebot-Extended, meta-externalagent, CCBot, Bytespider: crawlers collecting training data. | | Other | OAI-AdsBot, AdsBot-Google, link previews, feed fetchers and SEO tools. | The [crawler directory](https://metricraven.com/crawlers) lists every crawler MetricRaven recognizes, with its user-agent token, purpose, official documentation and IP range sources. Unknown bots are recorded as `other`. ## IP verification A user agent is only a claim: anyone can send `GPTBot`. When a crawl event carries the request's IP, MetricRaven checks it against the IP ranges the crawler's operator publishes (OpenAI, Anthropic, Google, Perplexity, Microsoft, Apple, Amazon, Common Crawl, DuckDuckGo, Mistral, Kimi and others) and stores the result with the request. | Result | Meaning | | --- | --- | | verified: true | The IP is inside the operator's published ranges. | | verified: false | The IP is outside them: the user agent is probably spoofed. | | verified: null | No IP was sent, or the operator publishes no ranges for this crawler. | The ranges are refreshed about twice a day. **All crawlers** shows how many of each crawler's requests were verified, the [crawlers report](https://metricraven.com/docs/reports-api#crawlers) returns the counts, and [Record verified crawlers only](https://metricraven.com/docs/bot-traffic-settings#verified-only) drops requests that fail the check. > **Warning:** Send the address your platform or proxy reports for the original request. Behind your own load balancer, read it only from a header that proxy sets, and pass it with `clientIp`; a client can put anything in `X-Forwarded-For` when nothing overwrites it. ## Choose what's recorded Exclude crawler companies or single agents in [bot traffic settings](https://metricraven.com/docs/bot-traffic-settings#exclude). Excluded requests return `ignored: "excluded"` and aren't stored or counted. You can also refuse reports that aren't sent with the bot token, and drop crawlers whose IP fails verification. Each plan includes a monthly bot request allowance; when it runs out, reports get `402 bot_tracking_paused` until the reset, and the response to the crawler is never affected. ## Details - Call this endpoint and the package only from your backend. Prefer the bot token: it can't read reports, send payments or change settings. - A Next.js proxy runs before the page renders, so it only records a status when the proxy answers the request itself, for example with a redirect. Express and Hono record the final status. - The package uses [@metricraven/node](https://metricraven.com/docs/node-sdk), which has no dependencies. --- # Bot traffic settings > Choose which crawler companies and agents are recorded, send crawl events with an ingest-only bot token, and track the bot request allowance. Source: https://metricraven.com/docs/bot-traffic-settings Markdown: https://metricraven.com/docs/bot-traffic-settings.md Section: Features Decide which crawlers MetricRaven records, keep your secret key out of the middleware that reports them, and see how many bot requests your plan has left. ## Exclude companies or agents In **Settings → Tracking & install → Bot traffic**, turn off a whole company (for example Meta) or a single agent (for example GPTBot). Excluded crawl events are dropped when they arrive: they aren't stored, shown in reports or counted toward your allowance. The change applies to new requests only; past data stays as it was. Admins and the owner can change these settings. Viewers can see them. Names match without regard to case, and you can exclude agents MetricRaven doesn't list yet by name. ## Bot token The bot token (`mrbot_…`) can only send crawl events to `POST /api/v1/crawls`. It can't read reports, send payments or change settings, so it's the safer credential for edge or server middleware. 1. **Create the token** Create it under Bot traffic. It's shown once; only a hash is stored. 2. **Send crawl events with it** With [@metricraven/ai-crawl](https://metricraven.com/docs/bots-ai-crawlers) or the [Node.js SDK](https://metricraven.com/docs/node-sdk#bot-token), set `METRICRAVEN_BOT_TOKEN` and you're done. With the API: ```bash curl --request POST 'https://metricraven.com/api/v1/crawls' \ --header "Authorization: Bearer $METRICRAVEN_BOT_TOKEN" \ --header 'Content-Type: application/json' \ --data '{ "url": "https://example.com/pricing", "userAgent": "Mozilla/5.0 (compatible; GPTBot/1.2; +https://openai.com/gptbot)", "status": 200, "ip": "203.0.113.7" }' ``` `ip` is optional. You can add `siteId` (or `?site=`) to check the token belongs to that website. 3. **Optionally require it** Turn on **Only accept crawl events sent with the bot token** to refuse crawl events sent with the secret key (HTTP 403, `bot_token_required`). Deploy the token first. Rotating the token replaces it at once, and the old token stops working. Deleting it also turns off the requirement, so crawl events sent with the secret key are accepted again. ## Record verified crawlers only When a crawl event carries the request's IP, MetricRaven checks it against the IP ranges the crawler's operator publishes (see [IP verification](https://metricraven.com/docs/bots-ai-crawlers#verification)). Turn on **Record verified crawlers only** to drop events whose IP is outside those ranges: someone sending a crawler's user agent from another network. They return `ignored: "unverified"` and aren't stored or counted. > **Note:** Crawlers whose operator publishes no ranges, and events sent without an IP, are still recorded as unverified. Before turning this on, check in **All crawlers** that requests from GPTBot or Googlebot show as IP-verified: if your server forwards a proxy's address instead of the crawler's, every check fails. ## Crawl event responses | Response | Meaning | | --- | --- | | 200 { ok: true, bot, type, verified } | Recorded and counted. verified: true or false after an IP check, null when none was possible. | | 200 { ignored: "not_a_crawler" } | A normal browser user agent. Nothing recorded. | | 200 { ignored: "excluded", excludedBy } | The company or agent is excluded. Nothing recorded or counted. | | 200 { ignored: "unverified" } | Verified only is on and the IP is outside the crawler's published ranges. Nothing recorded or counted. | | 401 unauthorized | Missing or unknown token or secret key. | | 402 bot_tracking_paused | The allowance is used up for this cycle. Recording resumes at resetAt. | | 403 bot_token_required | The website only accepts crawl events sent with its bot token. | | 403 domain_not_allowed / wrong_site | The URL isn't on the website's domains, or the token is for another website. | ## Bot request allowance Each plan includes a monthly allowance of AI and bot requests, pooled across your websites and separate from your events. You get an email at 80% and when it runs out. At the limit, new crawl events aren't recorded until the cycle resets. Nothing extra is charged, and pageviews, goals and payments keep working. Usage is shown under [Billing & usage](https://metricraven.com/app/billing), and excluding crawlers you don't need stretches the allowance. ## API, CLI and MCP These endpoints act as a person: use an [account access token](https://metricraven.com/docs/api-authentication) with `?site=`, not the secret key or the bot token. Changes need the write scope from an admin or the owner. | Request | Does | | --- | --- | | GET /api/v1/bot-traffic/settings?site=… | Exclusions, token status and the catalog of known companies and agents. | | PUT /api/v1/bot-traffic/settings?site=… | Replaces excludedCompanies and excludedAgents (and optionally rejectWithoutToken and verifiedOnly). | | PATCH /api/v1/bot-traffic/settings?site=… | Incremental: disableCompanies, enableCompanies, disableAgents, enableAgents, rejectWithoutToken, verifiedOnly. Other exclusions are kept. | | GET /api/v1/bot-traffic/token?site=… | Token status: hint, createdAt, lastUsedAt. Never the token. | | POST /api/v1/bot-traffic/token?site=… | Creates the token and returns it once. | | PUT /api/v1/bot-traffic/token?site=… | Rotates it. The old token stops working at once. | | DELETE /api/v1/bot-traffic/token?site=… | Deletes it and turns off the requirement. | | GET /api/v1/bot-traffic/usage?site=… | Requests used, allowance, remaining, refused, resetAt and websites that used them. | ```bash curl --request PATCH 'https://metricraven.com/api/v1/bot-traffic/settings?site=YOUR_SITE_ID' \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" \ --header 'Content-Type: application/json' \ --data '{ "disableCompanies": ["Meta"], "disableAgents": ["GPTBot"] }' ``` CLI: `metricraven bots settings`, `bots usage`, `bots exclude GPTBot`, `bots include Meta --company`, `bots require-token on`. MCP: `get_bot_settings`, `get_bot_usage` and `update_bot_settings` (which asks you to confirm first). > **Note:** Sending both a full list and incremental changes for the same list, or enabling and disabling the same name at once, returns 400 conflicting_update. ## Details - At most 200 excluded companies and 200 excluded agents per website. - Demo websites have no bot traffic settings. --- # AI visibility: what assistants say about you > Track the questions buyers ask ChatGPT, Claude, Perplexity, Gemini, Grok and Google's AI Mode and AI Overviews, and see whether the answers mention you, in what position, and which pages they cite. Source: https://metricraven.com/docs/ai-visibility Markdown: https://metricraven.com/docs/ai-visibility.md Section: Features The AI crawler report shows which assistants read your pages. AI visibility shows what they then say: MetricRaven asks your tracked prompts to each assistant every week and measures whether your brand appears, where, next to which competitors, and which pages get cited. > **Note:** AI visibility is part of the Growth plan, with an allowance included in the price (no top-ups). The free trial includes it with the 100k allowance, so you get about two weekly checks before choosing a plan. Starter shows a sample preview with an upgrade link. ## How it works - **Prompts.** You list questions a buyer might ask, such as *best privacy-friendly analytics with revenue tracking*. **Suggest prompts** drafts ideas from your brand, domain, competitors and mention keywords. - **Assistants.** Each prompt is sent to ChatGPT, Claude, Perplexity, Gemini and Grok (how many depends on your tier) with web search on, the way their apps answer. Requests go through OpenRouter to each provider's current fast model. - **Google's own answers (optional).** Switch on **Google AI Mode** and **Google AI Overviews** in the settings to see what Google itself answers for the same prompts. They come on top of your tier's assistants and use 1 credit each per prompt. - **Weekly checks.** Every active prompt runs on every enabled assistant once a week. **Run now** starts a check immediately, once a day. - **Parsing.** Each answer is read for the brands it names (in order, with the sentence around each and its tone) and the sources it cites. Your brand counts when the answer names it, an alias, or your domain as a whole word. ## Set it up 1. **Open the AI visibility tab and choose Prompts** Admins and the owner can edit; viewers and share links see the results. 2. **Add three to five prompts** Ask the way a buyer would, without your brand name (except comparisons like *X vs Y*). Group them if you like, for example *Pricing* or *Alternatives*. 3. **Check your brand name and add competitors (optional)** The brand defaults to your website's name. Every brand the assistants name appears in the leaderboard anyway; listed competitors are always matched and their pages are flagged in citations. 4. **Wait a few minutes** The first check runs shortly after you add prompts, then weekly. ## Gemini, Google AI Mode and AI Overviews Three ways Google can answer a buyer, each measured on its own: | Assistant | What it measures | | --- | --- | | Gemini | The Gemini model answering with Google Search grounding, as in the Gemini app. | | Google AI Mode | The answer in the AI Mode tab of Google Search, with the sources it links. | | Google AI Overviews | The AI Overview above the regular search results, for your prompt typed as a search. Google shows one only for some searches. | - Turn them on under **Settings → Assistants → Google's own answers**. They're off by default and don't count toward your tier's number of assistants. - **Search from** sets the country Google answers for, in that country's language. The default is the United States, in English. The other assistants have no location. - When Google shows no AI Overview for a search, the prompt grid shows **n/a** for it. That answer is left out of your scores and of the **Not mentioned** filter, because there was nothing to mention you in. It still uses its credit, since the search ran. ## Metrics | Metric | Meaning | | --- | --- | | Visibility score | 0–100, position-weighted: named first = 100, second = 50, third = 33, not named = 0, averaged over every answer. | | Mentioned in | Share of answers that name your brand. | | Average position | Your place among the brands an answer names, when it names you (#1 is first). | | Named first | Share of answers that name you before anyone else. | | Pages cited | Share of answers that cite one of your pages as a source. | | Consistency | When one assistant names you for a prompt, the share of assistants that do too. | Below the headline figures: the score over time (overall or per assistant), results per assistant, the brands assistants mention ranked by the same score, a prompt-by-assistant grid (select a prompt to read the answers, brands in order and sources), and the domains and pages cited most. Every row opens a detail: - **An assistant**: its score, mention rate, position and citation rate, how its score moved across your checks, and its latest answer to each prompt. - **A brand** in the leaderboard: the share of answers naming it across your checks next to yours, its average position, and the prompts where it's named and you aren't. - **A cited domain or page**: how often answers cited it across your checks, which prompts and assistants cite it now, and (for a domain) which of its pages. - **A prompt**: every assistant's latest answer, the brands in the order named and the sources. Answers listed in the other details open their prompt too. ## Credits and limits One credit is one prompt asked to one assistant; Gemini costs 2 credits per prompt because Google charges for each web search it makes, and Google AI Mode and AI Overviews cost 1 each. A check costs active prompts × the credits of each chosen assistant. Your monthly allowance covers weekly checks of every prompt on every assistant your tier includes, Gemini among them, plus both Google answers; Run now uses the same credits. | Growth tier | Prompts · assistants · credits a month | | --- | --- | | 10k | 10 · 3 · 265 | | 100k | 20 · 3 · 525 | | 200k | 30 · 4 · 915 | | 500k | 50 · 4 · 1,525 | | 1M | 75 · 5 · 2,610 | | 2M | 100 · 5 · 3,480 | | 5M | 150 · 5 · 5,215 | | 10M and up | 200 · 5 · 6,955 | - The prompt limit counts active prompts across all your websites. Pause a prompt to free its slot. - Credits reset with your monthly usage. When they run out, checks pause until the reset; nothing is ever billed extra. A larger tier is the way to get more. - Answers that fail (an assistant times out or errors) don't use credits. ## Choose the period The tab uses the same toolbar as Traffic and Revenue: the date range (presets, pinned periods or custom dates), the arrows for the previous and next period, Compare, Filter, the reporting timezone (the globe) and Refresh. The period is shared across the tabs, so switching tabs keeps it. The headline figures and the cards describe the latest check up to the end of the period; **Visibility over time** shows every check in it. Checks run once a week, so a period of a few days often has none: the tab then says so and offers a longer range. ## Compare The % changes under the headline figures compare the latest check with the one before by default. Choose **vs same period last year** or **vs custom start** to compare with the latest check in that window instead, or **No comparison** to hide the changes. The choice is shared with Traffic and Revenue. ## Filter the answers Choose **Filter** in the toolbar to look at some of the answers: an **Assistant**, a **Prompt**, a **Topic** (the prompt group), a **Brand named** in the answer, **Your brand** (mentioned, not mentioned, or your page cited) or a **Cited domain**, each with **is** or **is not**. Every card is recomputed from the matching answers: the scores and their change, the trend, assistants, brands, prompts, citations and the details behind each row. Filters live in the page address, so a filtered view can be bookmarked or shared. ## API and MCP Read the results with [GET /api/v1/stats/ai-visibility](https://metricraven.com/docs/reports-api#ai-visibility) or the MCP tool `get_ai_visibility`. Both read stored checks and never start one. Add `competitor`, `domain`, `page` or `model` for one brand, cited site, page or assistant in detail, or `prompt` for a prompt's latest answers. ```bash curl "https://metricraven.com/api/v1/stats/ai-visibility?range=90d" \ -H "Authorization: Bearer $METRICRAVEN_SECRET_KEY" ``` ## Details - Answers vary from run to run; a weekly trend across several prompts is more telling than one answer. - Full answer text is kept for 30 days. The brands, positions and sources parsed from it are kept with your data. - The public demo shows sample data: fictional brands, answers and sites. Compare it with the [AI crawler report](https://metricraven.com/docs/bots-ai-crawlers), which shows which assistants fetch your pages. --- # Share & embed > Create a read-only dashboard link, embed it in an iframe and sync its theme with your app. Source: https://metricraven.com/docs/share-embed Markdown: https://metricraven.com/docs/share-embed.md Section: Features Share a read-only dashboard with a link, or embed it in your own pages. ## Create a share link 1. **Open Settings → Share & embed** Create a read-only link. 2. **Send or embed the link** Anyone with the link can view the shared reports. Shared dashboards hide people's names, emails, visitor lists, live visitors, journeys and settings. Replace the link to invalidate the previous one, or turn sharing off. ## Embed in an iframe ```html ``` `embed=1` hides the site header. Choose `theme=light` or `theme=dark`. ## Follow your app's theme 1. **Add your app's origin to the iframe URL** Add `parent=https://YOUR_APP_DOMAIN`: the exact origin of the page containing the iframe, including its port if needed. 2. **Send the current mode** Send it on iframe load and whenever your app changes theme: ```javascript const frame = document.querySelector("iframe"); const metricravenOrigin = new URL(frame.src).origin; function syncDashboardTheme(theme) { if (theme !== "light" && theme !== "dark") return; frame.contentWindow.postMessage({ channel: "metricraven:private-embed:v1", type: "theme", theme }, metricravenOrigin); } // Call with your app's current mode after load and on theme changes. // Also resend when this exact frame reports that it is ready: window.addEventListener("message", (event) => { if (event.origin !== metricravenOrigin || event.source !== frame.contentWindow) return; if (event.data?.channel !== "metricraven:private-embed:v1" || event.data.type !== "ready") return; syncDashboardTheme(document.documentElement.dataset.theme === "dark" ? "dark" : "light"); }); ``` ## Give a teammate access Share links never show people. When a teammate needs visitor details or settings, [invite them](https://metricraven.com/docs/team-members#invite) from Settings → Members as a viewer (every report, no changes) or an admin (manages the site). ## Details - The embedded dashboard follows theme messages until its own light/dark button is used. That choice lasts until the iframe reloads and doesn't change your app's theme or the native MetricRaven preference. --- # Team members and roles > Invite teammates to a website as admins or viewers, transfer ownership, and rotate the site's secret key. Source: https://metricraven.com/docs/team-members Markdown: https://metricraven.com/docs/team-members.md Section: Features Invite the people you work with to a website. Each person signs in with their own account and gets a role on that website: owner, admin or viewer. ## Roles | Role | Who | | --- | --- | | Owner | One per website: the person whose plan pays for it, usually whoever added it. Has full control. | | Admin | Manages the website: settings, funnels, integrations, the share link, the secret key and the team. | | Viewer | Sees every report, including visitor details, and changes nothing. | | Action | Owner | Admin | Viewer | | --- | --- | --- | --- | | Traffic, revenue, goals, funnels and realtime reports | Yes | Yes | Yes | | Visitor details: journeys, visitor explorer, customer names and emails | Yes | Yes | Yes | | Change settings, exclusions, appearance, funnels, segments and chart notes | Yes | Yes | No | | Connect Stripe, RevenueCat and Search Console | Yes | Yes | No | | Manage the share link and rotate the secret key | Yes | Yes | No | | Delete a person's data | Yes | Yes | No | | Invite, resend, revoke, change roles, remove members | Yes | Yes, except the owner | No | | Transfer ownership and manage billing | Yes | No | No | | Leave the website | Transfer first | Yes | Yes | Viewers see visitor details because they are people you invited by name, signed in with their own account. To show numbers without any personal data, use a [read-only share link](https://metricraven.com/docs/share-embed) instead. ## Invite someone 1. **Open Settings → Members** On the website's dashboard, open the Settings tab. Admins and the owner see the invite form. 2. **Enter their email and choose a role** They get an email with a link. You can also copy the link and send it yourself. The link works once and expires after 7 days. 3. **They accept** The link opens a page where they sign in, or create a free account, with the invited email address, then click **Accept invitation**. Only that address can accept, so a forwarded link is useless to anyone else. Pending invitations are listed under the members. **Resend** sends a new link and restarts the 7 days; the previous link stops working. **Revoke** cancels the invitation. ## Change roles, remove and leave Admins and the owner can switch anyone except the owner between admin and viewer, or remove them. Changes apply at once, including to that person's [access tokens](https://metricraven.com/docs/api-authentication#roles). Any member except the owner can leave a website from the same list. To script this, use the [members API](https://metricraven.com/docs/account-api#members), the [CLI](https://metricraven.com/docs/cli-commands#changes) (`metricraven members …`) or the [MCP tools](https://metricraven.com/docs/mcp-tools#write-tools). ## Transfer ownership The owner can make another member the owner from Settings → Members → **Transfer ownership**, by typing the website's name to confirm. The website's events then count toward the new owner's plan, and you stay on as an admin. If their plan has no room for another website or for its team, the transfer is refused until they upgrade. ## Plan limits | Plan | Team members | | --- | --- | | Starter | 1 (you) | | Growth | Up to 30 | | Free trial | Up to 30 | Team members are counted per owner, across all the websites you own: someone on three of your websites is one team member, and you count as one. Pending invitations count until they expire or are revoked. Share links and embeds are unlimited and never count. If you downgrade, nobody is removed; you can't invite more people until you're under the limit. ## Rotate your site key The site secret key (`ana_sec_…`) lets your server send payments and events and read reports. Rotate it if it may have leaked, or when someone who had it leaves. 1. **Open Settings → Secret key** Admins and the owner see the current key's first characters, when it was created and when it was last used. 2. **Click Rotate key and choose what happens to the old key** **Stop it now** for a leaked key. **Keep it working for 24 hours** or **7 days** to give your servers time to switch. 3. **Copy the new key and deploy it** It is shown once. Put it in your server's environment and redeploy. The previous key's **Last used** time tells you when nothing sends it anymore; then click **Stop accepting previous keys now**, or let the grace period end. ## Named keys Give each server or integration its own key with **Add a named key** in Settings → Secret keys (up to 10 besides the default key). Each one shows when it was last used, can be rolled with the same grace periods, and can be revoked on its own: requests with a revoked key fail with 401 at once. The default key can be rolled but not revoked. Keys can also be managed with the [API](https://metricraven.com/docs/site-api#secret-keys) and `metricraven websites keys` in the [CLI](https://metricraven.com/docs/cli-commands). > **Warning:** After the grace period, requests with the old key fail with 401. The tracking script doesn't use the secret key, so pageviews keep coming in during a rotation. ## Details - Only hashes of invitation links and secret keys are stored. A lost key can't be shown again: rotate it. - Adding a website with **Add an existing site** and its current secret key makes you an admin of it. A previous key still in its grace period can't be used for this. - Last-used times are updated at most every 5 minutes. - Demo websites can't have team members. --- # Dashboard appearance > Set your dashboard's light and dark background and graph colors, including for shares and embeds. Source: https://metricraven.com/docs/appearance Markdown: https://metricraven.com/docs/appearance.md Section: Features Give a site's dashboard its own colors for light and dark mode. 1. **Open Settings → Appearance** In the site you want to style. 2. **Pick colors for each mode** Set background and graph colors independently for light and dark mode with the color pickers, or enter a six-digit hex color. The preview shows your draft; card and text colors adjust to the background. 3. **Choose Save appearance** To undo everything, choose **Reset to defaults** to restore the original backgrounds and graph colors for both modes. ## Details - These choices belong to the site. Every member, shared link and embedded dashboard uses the same saved palettes; the light/dark toggle still chooses which palette is active. - Reload an already-open share link to see a newly saved palette. - Appearance settings don't change tracking or report data. --- # SPA & virtual pages > Track single-page-app routes automatically, send manual pageviews for virtual screens and opt in to hash routes or plain #anchors. Source: https://metricraven.com/docs/spa-virtual-pages Markdown: https://metricraven.com/docs/spa-virtual-pages.md Section: Features Automatic tracking is the default. If your router uses the browser history API, keep the normal script tag. ## Automatic routes The tracker records the initial page and path changes through `history.pushState`, `history.replaceState` and back/forward navigation. Replacing state or changing only the query string doesn't add another pageview. ## Manual or virtual screens 1. **Turn off automatic pageviews** Add `data-auto-pageviews="false"` to your existing script tag. Don't install a second tracker. ```html ``` 2. **Call analytics.pageview from your router** Call it when a screen opens. The helper queues calls until the tracker is ready. ```javascript // Call from your browser router or when a virtual screen opens. function trackPageview(path) { if (window.analytics?.pageview) { window.analytics.pageview(path); } else { window.analytics = window.analytics || { q: [] }; window.analytics.q.push(["pageview", path]); } } trackPageview("/checkout/contact"); // After the visitor advances to another virtual screen: // trackPageview("/checkout/payment"); ``` 3. **Call it for every intended view** Include the initial screen. Manual calls always record a new view, even for the same path. Passing no path records the current URL path. You can also send manual views alongside automatic tracking for extra virtual screens. If you manually track every router change, disable automatic pageviews to avoid counting the same navigation twice. ## Hash routers For URLs like `https://example.com/#/pricing` or `https://example.com/#!/checkout`, add `data-track-hash="true"`. It works with automatic or manual mode and with query stripping. ```html ``` Use the full reported path, such as `/#/pricing`, in filters, exclusions and funnel page-prefix steps. ## Plain #anchors For one-page sites and landing pages whose sections are anchors, such as `https://example.com/#pricing`, use `data-track-hash="all"`. Each anchor the visitor moves to counts as its own page (`/#pricing`, `/#faq`), alongside hash routes and normal paths. It is an attribute on the same script, not a separate script file. ```html ``` Only short anchor names are recorded: a letter followed by up to 63 letters, digits, `-` or `_`. Fragments that carry data never qualify, so `#access_token=…`, `#state=…&code=…`, JWTs and long random strings are left out, and the visit is recorded under the path alone. > **Verify it works** > > Navigate between two routes and refresh Traffic: each intended route appears once. In manual mode, loading the app alone adds no pageview until your initial screen call runs. Check a goal and the resulting journey against the same logical path. ## Details - Virtual paths must start with a single `/`, stay on your current origin and be at most 500 characters after URL encoding. External URLs, backslashes and control characters are refused. - Virtual query strings and fragments are omitted unless they are recognized hash routes you enabled. - Goals and visible-time reports follow the last logical page until the next view. Scroll goals reset per logical page; delayed goals from the previous page are canceled. - A `true` return value from `pageview()` only means the tracker accepted the call locally; verify delivery in your dashboard. - With `data-track-hash="true"`, only hash paths beginning with `#/` or `#!/` are recognized. Hash query parameters, ordinary anchors such as `#pricing` (unless you chose `"all"`) and OAuth-style fragments such as `#access_token=…` are not stored as routes. Changing only a hash query doesn't add a view. Keep secrets out of path segments. - Browser events carry a capture time so fast views and goals keep their order even when requests arrive out of sequence. Future times are capped at receipt time; clocks more than five minutes off fall back to server receipt time. This doesn't change payment-provider transaction timestamps. --- # Cross-domain tracking > Keep one visitor across separate domains of the same site, such as a landing site and an app, with consent-aware, short-lived links. Source: https://metricraven.com/docs/cross-domain Markdown: https://metricraven.com/docs/cross-domain.md Section: Features When a visitor follows a link from `example.com` to `app.example.io`, MetricRaven can keep counting them as the same visitor, so the signup or payment on the app is credited to the campaign that brought them to the landing site. Subdomains of one domain (`www`, `app`, `shop`) don't need this: they share the visitor through `data-domain`. Use cross-domain tracking for different root domains, like a marketing site and an app, a site and a separate store, or several brand domains. ## Set it up 1. **Add every domain to the site** In **Settings → General → Site**, list all the domains under the same site. Visits are only accepted from these domains, and links are only joined when they come from one of them. 2. **Install the same script on each domain** Use the same site ID everywhere, and add `data-allowed-hostnames` with your other domains, comma-separated. Listing the current domain too is fine, so the same tag works on all of them. ```html ``` 3. **Links and forms are handled for you** When a visitor clicks a link or submits a form that leads to a listed domain (or one of its subdomains), the tracker adds a short `mr_link` parameter. The page that opens reads it, removes it from the address bar straight away and continues the visit. ## Links you build in code Navigation through `window.location`, `window.open` or a router doesn't go through a link click, so add the parameter yourself, right before navigating: ```javascript // Links and forms are linked automatically. For navigation in code, add the token first: window.location.href = window.analytics.linkUrl("https://app.example.io/signup?plan=pro"); // Or append the parameters yourself (empty until the tracker has a fresh token): const params = new URLSearchParams(window.analytics.linkParams()); ``` With the npm package: ```typescript import { init, linkUrl, linkParams } from "@metricraven/web"; init({ siteId: "YOUR_SITE_ID", allowedHostnames: ["example.com", "app.example.io"] }); // Build the URL right before navigating: tokens expire after 30 minutes. window.open(linkUrl("https://app.example.io/signup"), "_blank"); linkParams(); // { mr_link: "…" } or {} ``` `linkUrl` only changes URLs that lead to one of your listed domains, so it is safe to call on any URL. It returns the URL unchanged until the tracker has its first response. See [NPM package](https://metricraven.com/docs/npm-package#api). ## Privacy model No visitor ID ever appears in a URL. The parameter is an opaque token that only MetricRaven's server can read, and it is useless anywhere but in the browser it was made for. | Rule | Why | | --- | --- | | Encrypted and signed | The token holds the visitor ID and an expiry, encrypted and authenticated by the server. Nobody can read the ID from it or forge one. | | Bound to one browser | It only opens for a request from the same IP address and browser it was issued to. A copied, shared or bookmarked link doesn't merge someone else into the visitor. | | Short-lived | It expires 30 minutes after it was issued. The tracker keeps it in memory only and refreshes it with each pageview. | | Only from your own pages | The arrival page's referrer must be another of the site's domains, so a link pasted into chat or email is ignored. | | Added at the moment of the click | Links get the parameter as they are followed and lose it right after, so the token isn't left in your page or in copied links. | Consent works the same as on a single domain: - **No consent on the receiving domain:** nothing is stored in the browser there. The server remembers that this browser's daily ID on that domain belongs to the linked visitor until the end of the UTC day, the same lifetime as the daily ID itself, then forgets it. - **Consent on the receiving domain:** the visit continues with the linked visitor's ID, as subdomains do with the shared cookie. If the visitor only had a daily ID on the first domain, today's visits from it join this domain's consented ID, just as visits from before consent do on one domain. - **Consent granted later on the receiving domain:** the stored ID becomes the linked visitor's, so the journey stays in one piece. See [Consent & visitor IDs](https://metricraven.com/docs/consent) for how IDs work before and after consent. Deleting a person also deletes these same-day links. > **Verify it works** > > On the first domain, add `?metricraven_debug=1` to the URL and open the console: it lists the linked hosts. Click through to the other domain: the address bar shows no `mr_link`, and in **Traffic → Visitors** the visitor's journey shows pages from both domains. ## Details - Clicks to your listed domains aren't counted as outbound clicks. Visits between them aren't referrals. - GET forms carry the token as a hidden field; other forms carry it on their action URL. Both are removed right after submitting. - Linking needs the browser to send a referrer. The default policy does; a page with `Referrer-Policy: no-referrer` can't link. If you proxy the tracker on one domain only, forward the visitor's IP (`x-visitor-ip`) so both domains see the same address. See [Proxy](https://metricraven.com/docs/proxy). - A token that is expired, from another browser or not from one of your domains is ignored silently and the visit counts as a new visitor. Debug mode logs it. - Native apps don't use this. To join web visits to app users, use [identify](https://metricraven.com/docs/identify-users) on both. --- # Conversion alerts > Get an email when someone completes a goal, pays, or becomes a new customer, with where they came from. Source: https://metricraven.com/docs/goal-alerts Markdown: https://metricraven.com/docs/goal-alerts.md Section: Features An alert emails you the moment a conversion is recorded: a goal such as `signup`, any payment, or a customer’s first payment. Each email says where that person came from and links to their full journey. ## Create an alert 1. **Open Settings → Notifications** In **Conversion alerts**, choose **New alert**. 2. **Choose what triggers it** **A goal is completed** takes the exact, case-sensitive goal name (suggestions list goals tracked in the last 90 days; you can also set one up before its first event). **Any payment** fires for every new payment, renewals included. **A new customer pays** fires only on a customer’s first payment. 3. **Choose who gets it** **Just me** or **All members of this site**. Every member can later switch **Email me** on or off for each alert, and the unsubscribe link in each email does the same for that one alert. Optionally replace the subject and add your own message, for example who should follow up. A site can have up to 25 alerts; turn one off to pause it without losing its settings. > **Verify it works** > > Trigger the goal once (or send a test payment) and check your inbox. The alert’s row shows how often it fired today and in total, and **Recent alert emails** lists every email with its status. ## What the email shows | Field | Meaning | | --- | --- | | Source | Where the converting visit came from: UTM source, ref tag or referrer, with its channel (Organic Search, Social…). | | Campaign | The visit’s utm_campaign, when present. | | Landing page / Converted on | The first page of that visit, and the page where the goal fired. | | Location and device | City and country, device type, browser and OS. | | Visits and first seen | How many visits this visitor made and when we first saw them. | | Customer | Name and email, when you identify users. | | Amount and plan | For payment alerts. | | View visitor journey | Opens the dashboard with that visitor’s timeline of visits, pages, goals and payments. | Server-side goals and payments are linked to a visitor through `visitor_id`, `user_id` or [identify](https://metricraven.com/docs/identify-users). When nothing links them, the email still arrives without the source. ## Busy hours: summaries instead of a flood Each alert sends up to five individual emails in any hour. After that, further conversions are collected and arrive as one summary an hour later, with counts by source and the latest conversions. While an alert is that busy it keeps sending at most one summary per hour, and it goes back to individual emails after a quiet hour. > **Note:** Alerts are sent right after the conversion is stored and never slow down tracking. Retried requests and repeated webhook deliveries alert only once. Historical imports and backfills from Stripe or RevenueCat never alert. ## Manage alerts with the API Use an [account token](https://metricraven.com/docs/api-authentication) (read to list, write to change) or the site’s secret key, with `?site=`. | Request | Does | | --- | --- | | GET /api/v1/alerts | Alerts with trigger, recipients, isEnabled, triggerCount, dailyCount, monthlyCount, lastTriggeredAt and lastError, plus goals (names tracked in 90 days). | | POST /api/v1/alerts | Creates an alert. Body: trigger ({ type: goal, goalName } or { type: payment \| new_customer }), optional name, template.subject, template.message, notify (me \| members), isEnabled. | | GET /api/v1/alerts/{id} | One alert. | | PATCH /api/v1/alerts/{id} | Changes name, trigger, template or isEnabled; subscribe: true \| false adds or removes the calling member. | | DELETE /api/v1/alerts/{id} | Deletes the alert. Its history is kept. | | GET /api/v1/alerts/history | Sent emails, newest first, with counts of sent and failed. Optional alertId and limit (up to 100). | ```bash curl -X POST "https://metricraven.com/api/v1/alerts?site=$SITE_ID" \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" \ --header "Content-Type: application/json" \ --data '{"name":"Trial started","trigger":{"type":"goal","goalName":"trial_started"},"template":{"message":"Reply within the hour."}}' ``` ```json title="Response" { "status": "success", "data": [{ "id": "6727c0f1a2b3c4d5e6f70812", "websiteId": "ana_pub_…", "name": "Trial started", "trigger": { "type": "goal", "goalName": "trial_started" }, "destination": { "type": "email" }, "template": { "message": "Reply within the hour." }, "isEnabled": true, "recipients": [{ "userId": "…", "email": "you@example.com" }], "subscribed": true, "triggerCount": 0, "dailyCount": 0, "monthlyCount": 0, "lastTriggeredAt": null, "lastError": null, "createdAt": "2026-11-02T09:00:00.000Z", "updatedAt": "2026-11-02T09:00:00.000Z" }] } ``` The same actions are available as [MCP tools](https://metricraven.com/docs/mcp-tools) (`list_alerts`, `create_alert`, `update_alert`, `delete_alert`, `get_alert_history`) and in the [CLI](https://metricraven.com/docs/cli-commands) as `metricraven alerts`. --- # Track X and Reddit mentions > See posts on X and Reddit that mention your website or brand right on the traffic chart. Source: https://metricraven.com/docs/social-mentions Markdown: https://metricraven.com/docs/social-mentions.md Section: Features See when people talk about you on X and Reddit, next to the traffic and revenue those posts bring. > **Note:** Mentions are part of the Growth plan. Starter shows a preview with an upgrade link. ## How it works - Once a week MetricRaven searches X and Reddit for your website's domain and the keywords you add. Your domain is always tracked; you don't need to add it. - Each post appears on the traffic chart as its author's picture on that day's point. Hover a day to see its posts; click a picture to read them in full with likes, reposts and replies (X) or score and comments (Reddit). - Reddit search is fuzzy, so a post is kept only when its title, text or link contains your domain or a keyword as a whole word. - The chart's mention dialog has a **Show mentions on chart** switch and X / Reddit checkboxes. They are remembered in your browser. ## Add keywords 1. **Open Settings → Integrations → X and Reddit** Choose the **Mentions** tab. 2. **Add brand words, @handles or #hashtags** For example `MetricRaven`, `@metricraven` or `#BuildInPublic`. One list is used for both X and Reddit. 3. **Wait for the weekly check, or choose Check now** Check now runs once a day per website. New posts appear on the chart after the check finishes. ## Keyword rules | Rule | Detail | | --- | --- | | Length | 4–15 characters, up to 10 keywords. | | Matching | Case-insensitive, whole words: raven does not match ravenous. | | Handles | Start with @ to find posts that mention an X account. | | Avoid generic words | Words like analytics or software bring noise and use up the monthly X allowance. | ## Traffic spikes On every plan, days when one source sends far more visitors than usual get the source's icon at the top of the chart. The day's tooltip explains it, for example *5.6× more traffic than usual from X*. A day counts as a spike when a source brings at least 10 visitors, 2.5 times its median over the previous 14 days, and at least 15% of the day's visitors. ## API, MCP and CLI Read stored mentions with [GET /api/v1/stats/mentions](https://metricraven.com/docs/reports-api#mentions), the MCP tool `list_mentions` or `metricraven mentions`. They never start a new search. ## Details - X search covers the last 7 days, so the first check finds posts from the past week. - Each website collects up to 500 X posts a month. Once reached, X resumes next month; Reddit continues. - Retweets are skipped. Likes and other numbers are refreshed when a later check finds the post again. - Want to know which tweets sent visitors? See [X link attribution](https://metricraven.com/docs/x-link-attribution). --- # Traffic spike alerts > An email when your site suddenly gets far more visitors than usual, with the sources behind it. Source: https://metricraven.com/docs/traffic-spike-alerts Markdown: https://metricraven.com/docs/traffic-spike-alerts.md Section: Features When a post takes off, you hear about it while it is happening. MetricRaven compares the last hour with your usual traffic and emails you once per spike, with the sources and pages that drive it. ## Turn it on or off 1. **Open Settings → Notifications** Under **Your emails**, switch **Traffic spike alerts**. It is on by default for every member, and each member chooses for themselves. ## When an alert is sent Every few minutes we count unique visitors in the last 60 minutes and compare them with the site’s last 7 days, hour by hour. It counts as a spike when the last hour has at least: | Rule | Why | | --- | --- | | 25 visitors | So a quiet site needs a real crowd, not three friends. | | 3× the usual hour | So a steady site needs several times its normal traffic. | | The usual hour plus four standard deviations | So a site whose traffic swings a lot has to beat its own noise. | All three must hold. Sites with less than three days of history don’t get spike alerts yet. ## One email per spike - After an alert, nothing more is sent while the spike lasts. - A spike ends when the last hour drops below 60% of the threshold. - A new spike can alert at the earliest 12 hours after the previous email. ## What the email shows Visitors in the last hour against your usual hour, the top sources (each compared with its own usual traffic, such as “168× more than usual”, or marked new), the top pages, and a link to the dashboard to watch it live. > **Note:** Bots and excluded visits never count. The email’s unsubscribe link turns off spike alerts for that site only. --- # Attribute traffic and revenue to X posts > See the actual X posts that sent visitors and revenue instead of t.co links. Source: https://metricraven.com/docs/x-link-attribution Markdown: https://metricraven.com/docs/x-link-attribution.md Section: Features X wraps every link in a t.co short link. MetricRaven matches those links to the posts that contain them, so you see which tweets brought visitors and customers. > **Note:** Posts are resolved on the Growth plan. Other plans still see each t.co link with its numbers. ## See the posts 1. **Open the Acquisition card → Referrer** Click X (t.co or x.com) to filter by it. 2. **Read the list of links** Each row is one t.co link with its visitors and revenue. When MetricRaven has found the post with that link, the row shows the author and the post instead of the link. Click a post to open it on X. ## How posts are found - **Posts linking to your site.** The weekly X check also finds posts that link to your domain. Nothing to set up. - **Link in bio.** If your website is in the profile of your own or an affiliate X account, add the handle in Settings → Integrations → X and Reddit → **Link attribution**. Visits through that profile link show as the account. ## Details - Attribution works for visits recorded after this feature shipped, because the tracker now keeps the t.co link of X visits. Earlier X visits stay in one `t.co` row. - Some X apps send only `https://t.co/` or `x.com` without the link; those visits stay in one row. - Revenue is credited to the link of the customer's first visit, as everywhere else. - Only add handles whose profile links to your website. Profiles are refreshed at each weekly check. - From code: [GET /api/v1/stats/x-referrers](https://metricraven.com/docs/reports-api#mentions) or the MCP tool `get_x_referrers`. --- # Weekly & monthly email reports > A Monday-morning summary of visitors, revenue and top sources, compared with the week before. Source: https://metricraven.com/docs/email-reports Markdown: https://metricraven.com/docs/email-reports.md Section: Features Each member can get a short report per site: every Monday for the previous week, and optionally on the 1st for the previous month. ## Choose your reports 1. **Open Settings → Notifications** Under **Your emails**, switch **Weekly report** (on by default) and **Monthly report** (off by default). 2. **Check your time zone** Reports arrive at 8:00 in the time zone you pick, and weeks and months start at midnight there. Use the shortcut to take your browser’s zone. The choice is yours alone; other members of the site keep their own settings. The email’s unsubscribe link turns off that report for that site. ## What’s in a report | Section | Shows | | --- | --- | | Headline | Visitors and revenue, and the change in visitors compared with the period before. | | Key numbers | Visitors, pageviews, revenue (when you have payments), conversion rate or your primary goal, bounce rate and time on site, each with its change. | | Top channels and referrers | Where visitors came from, with revenue when payments are linked. | | Top pages and countries | The five most visited of each. | | Open full report | The dashboard for exactly that period. | - Weekly: Monday 00:00 to Monday 00:00, compared with the week before. - Monthly: the previous calendar month, compared with the month before. - Revenue is shown in the currency most of the period’s payments used. - With a [primary goal](https://metricraven.com/docs/goals-funnels) set, the report shows its conversions instead of the purchase rate. > **Note:** A week without visits still sends a short report, so you notice if tracking stopped. Each report goes out once per person and period, even if delivery is retried. ## Preferences in the API `GET` and `PATCH /api/v1/notifications?site=` read and change the calling member’s preferences (account token or dashboard session; a site key has no member). ```bash curl -X PATCH "https://metricraven.com/api/v1/notifications?site=$SITE_ID" \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" \ --header "Content-Type: application/json" \ --data '{"weeklyReport":true,"monthlyReport":true,"spikeAlerts":false,"timeZone":"Europe/Madrid"}' ``` --- # Chart notes > Mark launches, campaigns and releases on the traffic chart, so you remember later why the numbers moved. Source: https://metricraven.com/docs/chart-notes Markdown: https://metricraven.com/docs/chart-notes.md Section: Features A chart note is a short, dated line such as “Launched on Product Hunt”. It sits on the traffic chart at its date, so anyone looking at a spike or a dip can see what happened that day. ## Add a note 1. **Open chart notes** In **Traffic**, click the note button in the chart toolbar of Traffic overview. Its badge shows how many notes fall in the selected period. 2. **Pick the date and write the note** The date starts at today in your reporting timezone; pick an earlier day for something that already happened. Notes are one line of up to 280 characters. 3. **Click Add note** The note appears on the chart at once, for everyone on the team. ## Read notes on the chart A day with notes gets a small marker above the chart line, showing the number of notes when there is more than one. Hover the day to see its notes in the tooltip, under the day's numbers. On weekly or monthly charts, a marker collects every note in that week or month. ## Edit or delete a note Open chart notes: every note in the period is listed, newest first. Click **Edit** to change its date or text, then **Save note** (or **Cancel**). **Delete** removes it straight away. ## From the API Scripts and deploy pipelines can add notes too, for example one per release. See [Chart notes in the Site API](https://metricraven.com/docs/site-api#notes). To see code changes on the chart without writing notes, [connect GitHub](https://metricraven.com/docs/github). ## Details - Admins and the owner can add, edit and delete notes. Viewers see them but can't change them. - Notes are private to your team: share links and embeds don't show them (the [live demo](https://metricraven.com/demo) shows sample notes). - A website can have up to 1,000 notes. --- # GitHub commits on your chart > Connect a repository to see commits next to traffic and revenue, and spot which releases moved the numbers. Source: https://metricraven.com/docs/github Markdown: https://metricraven.com/docs/github.md Section: Features Connect one GitHub repository per website. Its commits appear on the traffic chart, so you can see what you shipped right before traffic or revenue changed. ## Connect a repository 1. **Open site settings → GitHub** Enter the repository as owner/name or paste its GitHub URL. Leave the branch empty to use the default branch. 2. **Add a token for private repositories** On GitHub, create a [fine-grained personal access token](https://github.com/settings/personal-access-tokens/new) with access to that one repository and the read-only **Contents** permission. Paste it in the Access token field. Public repositories work without a token. 3. **Wait for the import** The last 90 days of commits import within a minute. New commits appear on their own afterwards. ## What MetricRaven reads | Data | Used for | | --- | --- | | Commit message and type | The chart tooltip, and filtering by conventional-commit type such as feat: or fix:. | | Author name, username and avatar | Who shipped it. | | Time and branch | Placing the commit on the chart. | | Lines added and removed | The size of the change (with a token only). | Your source code is never read or stored. ## See every commit Hovering a day lists its newest commits. Click a commit marker to open all of its commits, grouped by day, with search and links to GitHub, or use **View all commits** in the chart toolbar for the whole period. ## Choose which commits show The commits button in the chart toolbar hides the markers or keeps only some types, for example features and fixes. Types come from [conventional commit](https://www.conventionalcommits.org) prefixes such as `feat:` and `fix(api):`; commits without one are listed as Other. Your choice is remembered in this browser. ## From the API ```bash curl 'https://metricraven.com/api/v1/commits?site=YOUR_SITE_ID&range=all&type=feat,fix' \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" ``` ## Details - With a token, MetricRaven checks for new commits every 10 minutes; without one, every 30 minutes. - Changing the repository or branch replaces the imported history. Disconnecting removes it. - Commits are private to your team: share links and embeds never show them. - Tokens are encrypted at rest. To revoke access, delete the token on GitHub or `Disconnect` in settings. --- # Import from Plausible > Bring your Plausible Analytics history into MetricRaven from an export ZIP or the Stats API, as daily totals labelled as imported. Source: https://metricraven.com/docs/import-from-plausible Markdown: https://metricraven.com/docs/import-from-plausible.md Section: Features Keep your history when you switch. MetricRaven imports Plausible's daily totals and shows them in your reports for the days before MetricRaven tracking started, labelled as imported. ## Option A: upload a Plausible export 1. **Export from Plausible's site settings** In Plausible, open your site's **Site settings → Imports & exports** and choose **Export data**. Plausible emails a download link when the ZIP is ready. The CSV download on the Plausible dashboard is a different file and can't be imported. 2. **Upload the ZIP** In MetricRaven, open **Settings → Integrations → Import from Plausible**, keep **Export ZIP**, choose the file and select **Import ZIP**. ZIPs up to 50 MB are accepted; for a larger site, export a shorter period and import each part. ## Option B: connect the Stats API 1. **Create a Stats API key** In Plausible, open **Account settings → API keys** and create a **Stats API** key. 2. **Paste it into MetricRaven** In **Settings → Integrations → Import from Plausible**, choose **Stats API key**, enter your Plausible site domain (for example `example.com`) and the key, and select **Import from API**. Self-hosting Plausible? Enter its https address too. The key is stored encrypted, used only for this import and for **Re-import**, and deleted when you undo the import. The card shows progress while the import runs. Most imports finish within a few minutes; you can leave the page. ## What is imported | Plausible | In MetricRaven | | --- | --- | | Visitors, visits, pageviews, bounces, visit duration | Traffic totals and the chart (daily, weekly and monthly views) | | Sources and referrers | Channels (classified like live traffic) and referrer hosts | | UTM source, medium, campaign, term, content | The matching UTM lists | | Pages and entry pages | Pages and entry pages | | Countries | Countries (regions and cities are not imported) | | Devices, browsers, operating systems | Devices, browsers and operating systems, renamed to MetricRaven's names | Not imported: custom events, goals and custom properties (set up [goals](https://metricraven.com/docs/goals-funnels) again in MetricRaven), exit pages, regions and cities. Plausible exports only contain daily totals, so imported days have no individual visits, journeys or revenue attribution. ## How imported days appear - Imported days are added only before the day MetricRaven tracking started, so nothing is counted twice even if both tools ran together. - Traffic reports that include them show **Includes imported Plausible data** with the imported dates. Totals such as visitors add up daily visitors, as Plausible does for its own imports. - Reports with filters, hourly charts, revenue, goals, funnels and the visitor explorer use MetricRaven data only. - **All time** starts at your first imported day. ## Re-import and undo Importing again (a new ZIP, or **Re-import** with the saved API key) replaces the previous import once the new one finishes. If it fails, the previous import stays. **Undo import** removes all imported days and the saved key; your own MetricRaven data is never changed. > **Note:** Imported days never count towards your plan's monthly events. History is imported as far back as your plan's data retention: 3 years on Starter, 10 on Growth. ## Details - Imports need an admin or owner of the site. Viewers see the import status only. - Days use the timezone of your Plausible site. Reports in another timezone place each imported day on that calendar date. - The API import reads Plausible's own first-hand data, not data you previously imported into Plausible. Plausible's API allows 600 requests an hour; a large site uses a few dozen. - An import that was interrupted (for example by a deploy) shows as interrupted; start it again. --- # Managed proxy > Serve the MetricRaven script and events from your own subdomain with one CNAME record. MetricRaven handles routing and the certificate. Source: https://metricraven.com/docs/managed-proxy Markdown: https://metricraven.com/docs/managed-proxy.md Section: Proxy Route MetricRaven through a subdomain of your website without running any proxy code. You add one DNS record; MetricRaven serves the script and receives events there, with its own TLS certificate. > **Note:** **Recommended for most sites.** Use the managed proxy unless your app already proxies requests through its own server. To compare both options, see [Serve tracking from your own domain](https://metricraven.com/docs/proxy). ## Why use a proxy? Ad blockers often block known analytics domains. A subdomain of your own website is blocked less often, so you see more of your visits and more of your [revenue attribution](https://metricraven.com/docs/stripe). You don't need a proxy to start; add one before a launch or when you want more complete data. Without a proxy, the script and its events go to `metricraven.com`. With the managed proxy they go to your subdomain instead, for example `a.example.com/js/script.js` and `a.example.com/api/v1/collect`. ## Set it up 1. **Enable the managed proxy** - Open **Settings → Tracking & install → Managed proxy** on your website's dashboard. - Enter a subdomain of your website, such as `a.example.com`, and select **Enable**. > **Note:** Pick a short, neutral name such as `a` or `cdn`. Blocklists often target names like `analytics`, `tracking`, `stats` or `metrics`. Use a subdomain, not your main site's hostname. Admins and the owner can set it up; each website has one managed proxy. 2. **Add the DNS record** The dashboard shows the exact record. For a subdomain it's one CNAME: | DNS field | Value | | --- | --- | | Type | CNAME | | Name / Host | `a` | | Target / Value | `proxy.metricraven.com` | | TTL | Auto (or 300 seconds) | | Proxy status | DNS only, proxy off (Cloudflare: grey cloud) | The dashboard looks up where your domain's DNS is hosted and, for the providers below, shows that provider's steps, its own field names, a link to its DNS page and a copy button for each field. `a` is only an example: for `cdn.example.com` the name is `cdn`. If the dashboard also shows a TXT record, add it only when the status stays on **Waiting for DNS** for more than 10 minutes. You can delete the TXT record once the proxy is active. DNS changes usually show up within 1–10 minutes. MetricRaven checks automatically (every minute at first) and issues the certificate as soon as the record is in place. Select **Check again** to look right away. 3. **Update your script tag** When the status is **Active**, replace your MetricRaven tag with this one. The Install card in Settings shows it with your site ID filled in. ```html title="Your website's shared layout" ``` The tracker sends its events to the host it was loaded from, so there's no separate events URL to set. If your tag has a `data-api` attribute from a self-hosted proxy, remove it. ## Steps by DNS provider Add the record where your domain's DNS is hosted, which isn't always where you bought the domain. The dashboard detects it from your domain's nameservers. Field names differ by provider: | Provider | Where | Name field | Target field | TTL | Proxy / forwarding | | --- | --- | --- | --- | --- | --- | | Cloudflare | DNS → Records → Add record | Name | Target | Auto | **DNS only (grey cloud)** | | Amazon Route 53 | Hosted zones → your domain → Create record | Record name | Value | 300 | Alias off | | Google Cloud DNS | Cloud DNS → your zone → Add standard | DNS name | Canonical name | 300 | Off | | GoDaddy | Domain Portfolio → your domain → DNS → Add New Record | Name | Value | 1/2 Hour | DNS record, not forwarding | | Namecheap | Domain List → Manage → Advanced DNS → Add New Record | Host | Value | Automatic | CNAME Record, not URL Redirect | | Squarespace | Domains → your domain → DNS → DNS settings → Custom records | Host | Data | Default | Custom record, not forwarding | | IONOS | Domains & SSL → your domain → DNS → Add record | Host name | Points to | 1 hour | DNS record, not forwarding | | Vercel | Domains → your domain → DNS Records | Name | Value | 60 | Off | | Netlify | Domains → your domain → DNS records → Add new record | Name | Value | 3600 | Off | | DigitalOcean | Networking → Domains → your domain → CNAME | Hostname | Is an alias of | 300 | Off | | Hetzner | Hetzner Console → project → DNS → your zone | Name | Value | Default | Off | Any other provider works the same way: add a CNAME with the name and target from the dashboard, keep the default TTL, and keep any proxy, CDN, forwarding or redirect option off for this record. > **Note:** **On Cloudflare, set Proxy status to DNS only (grey cloud).** With the orange cloud (Proxied), requests go through your own Cloudflare zone first and don't reach the MetricRaven proxy, so the status stays on Waiting for DNS or the subdomain returns errors. Turning the cloud grey fixes it within a few minutes; select **Check again**. Why it matters: the record has to point straight at `proxy.metricraven.com`. The certificate for your subdomain is issued only once the name resolves there, and that's how requests to your subdomain reach MetricRaven. ## Statuses | Status | Meaning | | --- | --- | | Waiting for DNS | The CNAME record isn't visible yet. The dashboard shows the last check's result. | | Issuing certificate | DNS is correct and the TLS certificate is being issued, usually within a few minutes. | | Active | The subdomain serves the script and receives events. Switch your tag now. | | Needs attention | Something broke, for example the CNAME was removed or never appeared. The dashboard says what to fix. | If no record appears within 7 days, MetricRaven stops checking and the status becomes **Needs attention**. Add the record and select **Check again** to start over. Active proxies are rechecked every few hours. > **Verify it works** > > - Open your live website, not the proxy subdomain itself (its root page is a 404 on purpose). > - In the browser's Network panel, `script.js` loads from your subdomain and events go to `https://a.example.com/api/v1/collect` with `ok: true`. > - Your visit appears in the realtime view. ## Troubleshooting | Problem | What to check | | --- | --- | | Status stays on Waiting for DNS | The CNAME matches the dashboard exactly; it's a DNS record, not a redirect or forwarding rule; your DNS provider's proxy mode (such as Cloudflare's orange cloud) is off for this record. Then wait a few minutes and select Check again. | | Needs attention | Re-check the record's type, name and target. Make sure you're editing the DNS zone of your website's domain. Add the TXT record if the dashboard shows one. | | Still blocked or not loading | Avoid obvious names (analytics, tracking, stats, metrics). The script src uses https:// and your subdomain, not metricraven.com. | | Content Security Policy errors | Allow your proxy subdomain in script-src and connect-src. | | Events refused with domain_not_allowed | The page sending them isn't one of your website's domains. The proxy subdomain doesn't need to be listed; the pages that load the script do. | ## Remove the managed proxy First point your script tag back at `https://metricraven.com/js/script.js`. Then, in **Settings → Tracking & install → Managed proxy**, select **Remove**; the subdomain stops serving the script at once. Finally delete the DNS record. ## API, CLI and MCP With an [account token](https://metricraven.com/docs/api-authentication) and `?site=` (changes need the write scope and an admin or the owner): | Request | What it does | | --- | --- | | GET /api/v1/managed-proxy | Hostname, status, error, the DNS records to add and the script tag. | | POST /api/v1/managed-proxy { "hostname": "a.example.com" } | Sets it up (201). 409 if the website already has one or the hostname is taken. | | POST /api/v1/managed-proxy/check | Looks for the DNS record now. | | GET /api/v1/managed-proxy/dns-provider | Where the subdomain's DNS is hosted: zone, nameservers and provider (or null). | | DELETE /api/v1/managed-proxy | Removes it. | CLI: `metricraven proxy status`, `proxy enable a.example.com`, `proxy check` and `proxy remove`. MCP: `get_managed_proxy`. ## Managed or self-hosted? | | Managed proxy | Self-hosted proxy | | --- | --- | --- | | Best for | Most sites | Apps that already proxy requests through their own server | | Code required | No | Yes | | DNS | One CNAME | None | | TLS certificate | Handled by MetricRaven | Your host's | | Script tag | src on your subdomain | src and data-api on your own paths | | Maintenance | Handled by MetricRaven | Yours | For self-hosted setups, see the [proxy recipes](https://metricraven.com/docs/proxy#recipes). ## Details - The subdomain answers only the script and the events endpoint. Every other path is a 404. - Only your website can send events through its subdomain, and events are still checked against your website's domains, as without a proxy. - Visitor locations and per-visitor rate limits work as without a proxy: the visitor's IP is passed along. - The script is cached for up to an hour, so tracker updates reach your subdomain within the hour. - A proxy doesn't guarantee that every blocker will allow tracking. --- # Serve tracking from your own domain > How a same-origin proxy for the MetricRaven script and events works, and recipes for your stack. Source: https://metricraven.com/docs/proxy Markdown: https://metricraven.com/docs/proxy.md Section: Proxy Serving the script and events from your own domain helps when extensions block third-party tracker URLs. There are two ways: the managed proxy (one DNS record) or a proxy on your own server. ## Managed proxy: one DNS record (recommended) Add a CNAME such as `a.example.com → proxy.metricraven.com` and MetricRaven serves the script and receives events on that subdomain, with the TLS certificate handled for you. No code to deploy or maintain. Set it up in **Settings → Tracking & install → Managed proxy**, or read [Managed proxy](https://metricraven.com/docs/managed-proxy). ## Self-hosted proxy Use this when your app already proxies requests through its own server, or you want same-origin paths such as `/a/script`. It exposes only the public script and collection endpoint, never your secret key. ## How it works Your server answers two paths and relays them to MetricRaven: | Your path | Relays to | | --- | --- | | GET /a/script | `https://metricraven.com/js/script.js`. Caching it for an hour is fine. | | POST /a/event | `https://metricraven.com/api/v1/collect`, with the request body unchanged. Never cache it. | Then the script tag points at those paths: - `src="/a/script"` loads the tracker from your domain. - `data-api="/a/event"` sends events to your domain. Any path names work as long as the tag and the proxy agree. The recipes use `/a/script` and `/a/event`. ## Headers the proxy must send | Header | Value | | --- | --- | | x-visitor-ip | The visitor's IP address. Required for correct locations and per-visitor limits. | | origin | Copied from the browser's request. Checked against your site's allowed domains. | | user-agent | Copied from the browser's request. Used for device and browser reports and bot filtering. | | content-type | Copied from the browser's request (the tracker sends text/plain). | | x-visitor-country, x-visitor-region, x-visitor-city, x-visitor-latitude, x-visitor-longitude | Optional. If your host already knows the visitor's location, forward it with the IP. Without them, MetricRaven looks up the IP. URL-encode non-ASCII names. | > **Warning:** A plain rewrite or reverse proxy that forwards to MetricRaven without `x-visitor-ip` is not enough. MetricRaven then sees your host's IP for every visitor: locations are wrong, and busy sites hit the per-IP rate limit and lose events. This includes host rewrite rules on Vercel, Netlify and Firebase, which can't add the header. Those recipes use a small function instead. ## Recipes - [Next.js](https://metricraven.com/docs/proxy-nextjs) - [Express (Node.js)](https://metricraven.com/docs/proxy-express) - [Flask](https://metricraven.com/docs/proxy-flask) - [FastAPI](https://metricraven.com/docs/proxy-fastapi) - [PHP](https://metricraven.com/docs/proxy-php) - [Laravel](https://metricraven.com/docs/proxy-laravel) - [Astro](https://metricraven.com/docs/proxy-astro) - [Vue (Vite dev server) and Nuxt](https://metricraven.com/docs/proxy-vue-nuxt) - [Nginx](https://metricraven.com/docs/proxy-nginx) - [Caddy](https://metricraven.com/docs/proxy-caddy) - [Cloudflare Workers](https://metricraven.com/docs/proxy-cloudflare-workers) - [Vercel](https://metricraven.com/docs/proxy-vercel) - [Netlify](https://metricraven.com/docs/proxy-netlify) - [Firebase Hosting](https://metricraven.com/docs/proxy-firebase) - [DigitalOcean App Platform](https://metricraven.com/docs/proxy-digitalocean) Anything else that can make an HTTP request can follow the same pattern. Take the visitor IP only from the socket or from a header your own infrastructure sets and overwrites, never from a header the browser could send. ## Details - The proxy forwards only `Content-Type`, `Origin`, `User-Agent` and the visitor's IP as `x-visitor-ip`. Your site's cookies and auth headers never leave your server. - `Origin` matters: MetricRaven checks it against your site's allowed domains. A missing or wrong origin is refused with `domain_not_allowed`. - MetricRaven allows about 120 events per minute per visitor IP. Without `x-visitor-ip`, all of your visitors share the proxy's IP, so busy sites lose events as well as locations. - A proxy doesn't guarantee that every blocker will allow tracking. --- # Proxy with Next.js > Serve the MetricRaven script and events from your Next.js app with one App Router route handler. Source: https://metricraven.com/docs/proxy-nextjs Markdown: https://metricraven.com/docs/proxy-nextjs.md Section: Proxy One route handler in the App Router serves the script and relays events. ## Set it up 1. **Add a route handler** ```typescript title="app/a/[kind]/route.ts" // app/a/[kind]/route.ts — Next.js App Router import { NextRequest } from "next/server"; async function proxy(req: NextRequest, context: { params: Promise<{ kind: string }> }) { const { kind } = await context.params; const script = kind === "script" && req.method === "GET"; const event = kind === "event" && req.method === "POST"; if (!script && !event) return new Response("Not found", { status: 404 }); const headers = new Headers(); for (const name of ["content-type", "origin", "user-agent"]) { const value = req.headers.get(name); if (value) headers.set(name, value); } // Trust these only if your hosting provider overwrites them. const ip = req.headers.get("cf-connecting-ip") || req.headers.get("x-forwarded-for")?.split(",")[0]?.trim(); if (ip) headers.set("x-visitor-ip", ip); for (const [source, target] of [["cf-ipcountry", "x-visitor-country"], ["cf-region", "x-visitor-region"], ["cf-ipcity", "x-visitor-city"]]) { const value = req.headers.get(source); if (value) headers.set(target, value); } const upstream = await fetch(script ? "https://metricraven.com/js/script.js" : "https://metricraven.com/api/v1/collect", { method: req.method, headers, body: event ? await req.arrayBuffer() : undefined, cache: "no-store", signal: AbortSignal.timeout(15000), }); return new Response(upstream.body, { status: upstream.status, headers: { "Content-Type": upstream.headers.get("content-type") || "application/octet-stream", "Cache-Control": script ? "public, max-age=3600" : "no-store", }}); } export { proxy as GET, proxy as POST }; ``` On Vercel, `x-forwarded-for` is set by the platform. Behind Cloudflare, `cf-connecting-ip` and Cloudflare's visitor location headers are used when present. 2. **Load the tracker with next/script** In your root layout, set `src="/a/script"` and `data-api="/a/event"` on the existing `Script` component. 3. **Point your script tag at your own paths** Replace your MetricRaven tag with this one. `src` loads the tracker from your domain and `data-api` sends its events there too. ```html title="Your website's shared layout" ``` > **Verify it works** > > Open your live site. In the browser's Network panel, `/a/script` loads the tracker and `/a/event` answers with `ok: true`. Nothing goes to `metricraven.com` from the browser. > > In **Traffic**, your own visit should show your real country and city. If every visit appears to come from your server's or host's location, the visitor IP isn't being forwarded. ## Notes - The handler reads `x-forwarded-for`. Trust it only if your host overwrites it; a self-hosted Next.js server without a proxy in front should use the socket address instead. - If you use Next.js middleware with a matcher for all paths, exclude `/a/` so it doesn't redirect or rewrite the tracker's requests. How the proxy works and which headers it must send: [Use your own domain](https://metricraven.com/docs/proxy). ## Details - The proxy forwards only `Content-Type`, `Origin`, `User-Agent` and the visitor's IP as `x-visitor-ip`. Your site's cookies and auth headers never leave your server. - `Origin` matters: MetricRaven checks it against your site's allowed domains. A missing or wrong origin is refused with `domain_not_allowed`. - MetricRaven allows about 120 events per minute per visitor IP. Without `x-visitor-ip`, all of your visitors share the proxy's IP, so busy sites lose events as well as locations. - A proxy doesn't guarantee that every blocker will allow tracking. --- # Proxy with Express > Serve the MetricRaven script and events from an Express server on Node.js. Source: https://metricraven.com/docs/proxy-express Markdown: https://metricraven.com/docs/proxy-express.md Section: Proxy Two Express routes relay the script and events, using Node's built-in fetch. ## Set it up 1. **Add the routes** Add the two routes to your existing app, before any catch-all route. Node.js 18 or later is required for `fetch`. ```javascript title="metricraven-proxy.js" // metricraven-proxy.js · Express 4 or 5 on Node.js 18+ import express from "express"; const app = express(); // Behind a load balancer or CDN? Trust it so req.ip is the visitor, not the balancer. // app.set("trust proxy", 1); async function relay(req, res, url, cacheControl) { const headers = { "x-visitor-ip": req.ip.replace(/^::ffff:/, "") }; for (const name of ["content-type", "origin", "user-agent"]) { if (req.headers[name]) headers[name] = req.headers[name]; } try { const upstream = await fetch(url, { method: req.method, headers, body: req.method === "POST" ? req.body : undefined, signal: AbortSignal.timeout(15000), }); res .status(upstream.status) .set("Content-Type", upstream.headers.get("content-type") || "application/octet-stream") .set("Cache-Control", cacheControl) .send(Buffer.from(await upstream.arrayBuffer())); } catch { res.status(502).end(); } } app.get("/a/script", (req, res) => relay(req, res, "https://metricraven.com/js/script.js", "public, max-age=3600"), ); // The tracker sends JSON as text/plain; keep the raw body untouched. app.post("/a/event", express.raw({ type: () => true, limit: "64kb" }), (req, res) => relay(req, res, "https://metricraven.com/api/v1/collect", "no-store"), ); // Mount these routes in your existing app, or run this file on its own: app.listen(process.env.PORT || 3000); ``` 2. **Set trust proxy if you're behind a load balancer** `req.ip` is the visitor only when Express knows which proxies to trust. On Heroku, Render, Railway or behind Nginx, uncomment `app.set("trust proxy", 1)` (one hop). 3. **Point your script tag at your own paths** Replace your MetricRaven tag with this one. `src` loads the tracker from your domain and `data-api` sends its events there too. ```html title="Your website's shared layout" ``` > **Verify it works** > > Open your live site. In the browser's Network panel, `/a/script` loads the tracker and `/a/event` answers with `ok: true`. Nothing goes to `metricraven.com` from the browser. > > In **Traffic**, your own visit should show your real country and city. If every visit appears to come from your server's or host's location, the visitor IP isn't being forwarded. ## Notes - If a CDN or load balancer sits in front of this server, the connecting address is that proxy, not the visitor. Use the visitor IP header it sets (and only one it overwrites, so visitors can't fake it). - The event route uses `express.raw()` so the body is relayed byte for byte. A global `express.json()` doesn't touch it, because the tracker sends `text/plain`. How the proxy works and which headers it must send: [Use your own domain](https://metricraven.com/docs/proxy). ## Details - The proxy forwards only `Content-Type`, `Origin`, `User-Agent` and the visitor's IP as `x-visitor-ip`. Your site's cookies and auth headers never leave your server. - `Origin` matters: MetricRaven checks it against your site's allowed domains. A missing or wrong origin is refused with `domain_not_allowed`. - MetricRaven allows about 120 events per minute per visitor IP. Without `x-visitor-ip`, all of your visitors share the proxy's IP, so busy sites lose events as well as locations. - A proxy doesn't guarantee that every blocker will allow tracking. --- # Proxy with Flask > Serve the MetricRaven script and events from a Flask app. Source: https://metricraven.com/docs/proxy-flask Markdown: https://metricraven.com/docs/proxy-flask.md Section: Proxy Two Flask routes relay the script and events with the requests library. ## Set it up 1. **Install requests** ```bash pip install requests ``` 2. **Add the routes to your app** ```python title="metricraven_proxy.py" # metricraven_proxy.py · Flask 2.2+ with requests import requests from flask import Flask, Response, request app = Flask(__name__) # Behind a load balancer or CDN? Trust one proxy hop so remote_addr is the visitor: # from werkzeug.middleware.proxy_fix import ProxyFix # app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1) def relay(url, cache_control): headers = { name: request.headers[name] for name in ("Content-Type", "Origin", "User-Agent") if name in request.headers } headers["X-Visitor-IP"] = request.remote_addr or "" try: upstream = requests.request( request.method, url, headers=headers, data=request.get_data() if request.method == "POST" else None, timeout=15, ) except requests.RequestException: return Response(status=502) return Response( upstream.content, status=upstream.status_code, headers={ "Content-Type": upstream.headers.get("Content-Type", "application/octet-stream"), "Cache-Control": cache_control, }, ) @app.get("/a/script") def metricraven_script(): return relay("https://metricraven.com/js/script.js", "public, max-age=3600") @app.post("/a/event") def metricraven_event(): return relay("https://metricraven.com/api/v1/collect", "no-store") ``` 3. **Trust your load balancer, if any** Behind Nginx, Gunicorn on a platform router, or a CDN, `request.remote_addr` is that proxy. Enable the `ProxyFix` lines with the number of proxies in front of Flask. 4. **Point your script tag at your own paths** Replace your MetricRaven tag with this one. `src` loads the tracker from your domain and `data-api` sends its events there too. ```html title="Your website's shared layout" ``` > **Verify it works** > > Open your live site. In the browser's Network panel, `/a/script` loads the tracker and `/a/event` answers with `ok: true`. Nothing goes to `metricraven.com` from the browser. > > In **Traffic**, your own visit should show your real country and city. If every visit appears to come from your server's or host's location, the visitor IP isn't being forwarded. ## Notes - If a CDN or load balancer sits in front of this server, the connecting address is that proxy, not the visitor. Use the visitor IP header it sets (and only one it overwrites, so visitors can't fake it). How the proxy works and which headers it must send: [Use your own domain](https://metricraven.com/docs/proxy). ## Details - The proxy forwards only `Content-Type`, `Origin`, `User-Agent` and the visitor's IP as `x-visitor-ip`. Your site's cookies and auth headers never leave your server. - `Origin` matters: MetricRaven checks it against your site's allowed domains. A missing or wrong origin is refused with `domain_not_allowed`. - MetricRaven allows about 120 events per minute per visitor IP. Without `x-visitor-ip`, all of your visitors share the proxy's IP, so busy sites lose events as well as locations. - A proxy doesn't guarantee that every blocker will allow tracking. --- # Proxy with FastAPI > Serve the MetricRaven script and events from a FastAPI app. Source: https://metricraven.com/docs/proxy-fastapi Markdown: https://metricraven.com/docs/proxy-fastapi.md Section: Proxy Two async FastAPI routes relay the script and events with httpx. ## Set it up 1. **Install httpx** ```bash pip install httpx ``` 2. **Add the routes to your app** ```python title="metricraven_proxy.py" # metricraven_proxy.py · FastAPI with httpx import httpx from fastapi import FastAPI, Request, Response app = FastAPI() client = httpx.AsyncClient(timeout=15) async def relay(request: Request, url: str, cache_control: str) -> Response: headers = { name: request.headers[name] for name in ("content-type", "origin", "user-agent") if name in request.headers } if request.client: headers["x-visitor-ip"] = request.client.host try: upstream = await client.request( request.method, url, headers=headers, content=await request.body() if request.method == "POST" else None, ) except httpx.HTTPError: return Response(status_code=502) return Response( upstream.content, status_code=upstream.status_code, headers={ "Content-Type": upstream.headers.get("content-type", "application/octet-stream"), "Cache-Control": cache_control, }, ) @app.get("/a/script") async def metricraven_script(request: Request): return await relay(request, "https://metricraven.com/js/script.js", "public, max-age=3600") @app.post("/a/event") async def metricraven_event(request: Request): return await relay(request, "https://metricraven.com/api/v1/collect", "no-store") ``` 3. **Pass the visitor IP through Uvicorn** Behind a load balancer, start Uvicorn with `--proxy-headers` and set `--forwarded-allow-ips` to your proxy's address, so `request.client.host` is the visitor. 4. **Point your script tag at your own paths** Replace your MetricRaven tag with this one. `src` loads the tracker from your domain and `data-api` sends its events there too. ```html title="Your website's shared layout" ``` > **Verify it works** > > Open your live site. In the browser's Network panel, `/a/script` loads the tracker and `/a/event` answers with `ok: true`. Nothing goes to `metricraven.com` from the browser. > > In **Traffic**, your own visit should show your real country and city. If every visit appears to come from your server's or host's location, the visitor IP isn't being forwarded. ## Notes - If a CDN or load balancer sits in front of this server, the connecting address is that proxy, not the visitor. Use the visitor IP header it sets (and only one it overwrites, so visitors can't fake it). How the proxy works and which headers it must send: [Use your own domain](https://metricraven.com/docs/proxy). ## Details - The proxy forwards only `Content-Type`, `Origin`, `User-Agent` and the visitor's IP as `x-visitor-ip`. Your site's cookies and auth headers never leave your server. - `Origin` matters: MetricRaven checks it against your site's allowed domains. A missing or wrong origin is refused with `domain_not_allowed`. - MetricRaven allows about 120 events per minute per visitor IP. Without `x-visitor-ip`, all of your visitors share the proxy's IP, so busy sites lose events as well as locations. - A proxy doesn't guarantee that every blocker will allow tracking. --- # Proxy with PHP > Serve the MetricRaven script and events from one plain PHP file with curl. Source: https://metricraven.com/docs/proxy-php Markdown: https://metricraven.com/docs/proxy-php.md Section: Proxy One PHP file relays the script and events. It works on most shared hosting. ## Set it up 1. **Upload a.php to your web root** It needs PHP 7.4 or later with the `curl` extension, which most hosts enable. ```php title="a.php" 'Content-Type', 'HTTP_ORIGIN' => 'Origin', 'HTTP_USER_AGENT' => 'User-Agent']; foreach ($forward as $key => $name) { if (!empty($_SERVER[$key])) $headers[] = $name . ': ' . $_SERVER[$key]; } $ch = curl_init($isScript ? 'https://metricraven.com/js/script.js' : 'https://metricraven.com/api/v1/collect'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => 15, CURLOPT_ENCODING => '', ]); if ($isEvent) { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, file_get_contents('php://input')); } $body = curl_exec($ch); if ($body === false) { http_response_code(502); exit; } http_response_code(curl_getinfo($ch, CURLINFO_RESPONSE_CODE)); header('Content-Type: ' . (curl_getinfo($ch, CURLINFO_CONTENT_TYPE) ?: 'application/octet-stream')); header('Cache-Control: ' . ($isScript ? 'public, max-age=3600' : 'no-store')); echo $body; ``` 2. **Map /a/script and /a/event to it** On Apache, add this to the `.htaccess` in the same folder: ```apacheconf title=".htaccess" # .htaccess · Apache with mod_rewrite: serve a.php at /a/script and /a/event RewriteEngine On RewriteRule ^a/(script|event)$ a.php?kind=$1 [L,QSA] ``` Can't use rewrites? Skip this step and use this tag instead of the one below: ```html ``` 3. **Point your script tag at your own paths** Replace your MetricRaven tag with this one. `src` loads the tracker from your domain and `data-api` sends its events there too. ```html title="Your website's shared layout" ``` > **Verify it works** > > Open your live site. In the browser's Network panel, `/a/script` loads the tracker and `/a/event` answers with `ok: true`. Nothing goes to `metricraven.com` from the browser. > > In **Traffic**, your own visit should show your real country and city. If every visit appears to come from your server's or host's location, the visitor IP isn't being forwarded. ## Notes - If a CDN or load balancer sits in front of this server, the connecting address is that proxy, not the visitor. Use the visitor IP header it sets (and only one it overwrites, so visitors can't fake it). - On WordPress, Laravel or another framework, prefer its own routing; see [Laravel](https://metricraven.com/docs/proxy-laravel). How the proxy works and which headers it must send: [Use your own domain](https://metricraven.com/docs/proxy). ## Details - The proxy forwards only `Content-Type`, `Origin`, `User-Agent` and the visitor's IP as `x-visitor-ip`. Your site's cookies and auth headers never leave your server. - `Origin` matters: MetricRaven checks it against your site's allowed domains. A missing or wrong origin is refused with `domain_not_allowed`. - MetricRaven allows about 120 events per minute per visitor IP. Without `x-visitor-ip`, all of your visitors share the proxy's IP, so busy sites lose events as well as locations. - A proxy doesn't guarantee that every blocker will allow tracking. --- # Proxy with Laravel > Serve the MetricRaven script and events from a Laravel app with the HTTP client. Source: https://metricraven.com/docs/proxy-laravel Markdown: https://metricraven.com/docs/proxy-laravel.md Section: Proxy A small route file relays the script and events with Laravel's HTTP client. ## Set it up 1. **Create routes/metricraven.php** ```php title="routes/metricraven.php" isMethod('get'); $event = $kind === 'event' && $request->isMethod('post'); abort_unless($script || $event, 404); $client = Http::timeout(15)->withHeaders(array_filter([ 'Origin' => $request->header('Origin'), 'User-Agent' => $request->userAgent(), 'X-Visitor-IP' => $request->ip(), ])); $upstream = $script ? $client->get('https://metricraven.com/js/script.js') : $client ->withBody($request->getContent(), $request->header('Content-Type', 'text/plain')) ->post('https://metricraven.com/api/v1/collect'); return response($upstream->body(), $upstream->status()) ->header('Content-Type', $upstream->header('Content-Type') ?: 'application/octet-stream') ->header('Cache-Control', $script ? 'public, max-age=3600' : 'no-store'); })->whereIn('kind', ['script', 'event']); ``` 2. **Load it outside the web middleware group** In `bootstrap/app.php`, add the `then` callback to your existing `withRouting` call. The routes then skip sessions and CSRF checks, which the tracker can't send. ```php title="bootstrap/app.php" // bootstrap/app.php · Laravel 11 or later use Illuminate\Support\Facades\Route; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', commands: __DIR__.'/../routes/console.php', health: '/up', // Outside the "web" group: no session cookie and no CSRF check on the tracker routes. then: function () { Route::group([], base_path('routes/metricraven.php')); }, ) ->withMiddleware(function (Middleware $middleware) { // Behind a load balancer or CDN? Trust it so $request->ip() is the visitor. // $middleware->trustProxies(at: '*'); }) // ...the rest of your existing configuration ``` 3. **Add the script tag to your Blade layout** Paste it before `` in the layout every page extends, for example `resources/views/layouts/app.blade.php`. 4. **Point your script tag at your own paths** Replace your MetricRaven tag with this one. `src` loads the tracker from your domain and `data-api` sends its events there too. ```html title="Your website's shared layout" ``` > **Verify it works** > > Open your live site. In the browser's Network panel, `/a/script` loads the tracker and `/a/event` answers with `ok: true`. Nothing goes to `metricraven.com` from the browser. > > In **Traffic**, your own visit should show your real country and city. If every visit appears to come from your server's or host's location, the visitor IP isn't being forwarded. ## Notes - If a CDN or load balancer sits in front of this server, the connecting address is that proxy, not the visitor. Use the visitor IP header it sets (and only one it overwrites, so visitors can't fake it). - In Laravel, trust your load balancer with `$middleware->trustProxies()` so `$request->ip()` is the visitor. - On Laravel 10 or earlier, register the file in `RouteServiceProvider` without the `web` middleware instead. How the proxy works and which headers it must send: [Use your own domain](https://metricraven.com/docs/proxy). ## Details - The proxy forwards only `Content-Type`, `Origin`, `User-Agent` and the visitor's IP as `x-visitor-ip`. Your site's cookies and auth headers never leave your server. - `Origin` matters: MetricRaven checks it against your site's allowed domains. A missing or wrong origin is refused with `domain_not_allowed`. - MetricRaven allows about 120 events per minute per visitor IP. Without `x-visitor-ip`, all of your visitors share the proxy's IP, so busy sites lose events as well as locations. - A proxy doesn't guarantee that every blocker will allow tracking. --- # Proxy with Astro > Serve the MetricRaven script and events from an Astro server endpoint. Source: https://metricraven.com/docs/proxy-astro Markdown: https://metricraven.com/docs/proxy-astro.md Section: Proxy An on-demand Astro endpoint relays the script and events. It needs a server adapter. ## Set it up 1. **Make sure your project has an adapter** Server endpoints run only with an adapter such as `@astrojs/node`, `@astrojs/vercel`, `@astrojs/netlify` or `@astrojs/cloudflare`. Add one with `npx astro add node` (or your host's name). Fully static sites: use your host's recipe instead (see the notes). 2. **Add the endpoint** ```typescript title="src/pages/a/[kind].ts" // src/pages/a/[kind].ts · needs an Astro adapter (Node, Vercel, Netlify or Cloudflare) import type { APIRoute } from "astro"; export const prerender = false; export const ALL: APIRoute = async ({ params, request, clientAddress }) => { const script = params.kind === "script" && request.method === "GET"; const event = params.kind === "event" && request.method === "POST"; if (!script && !event) return new Response("Not found", { status: 404 }); const headers = new Headers({ "x-visitor-ip": clientAddress }); for (const name of ["content-type", "origin", "user-agent"]) { const value = request.headers.get(name); if (value) headers.set(name, value); } const upstream = await fetch(script ? "https://metricraven.com/js/script.js" : "https://metricraven.com/api/v1/collect", { method: request.method, headers, body: event ? await request.arrayBuffer() : undefined, signal: AbortSignal.timeout(15000), }); return new Response(upstream.body, { status: upstream.status, headers: { "Content-Type": upstream.headers.get("content-type") || "application/octet-stream", "Cache-Control": script ? "public, max-age=3600" : "no-store", }, }); }; ``` `clientAddress` is the visitor IP as your adapter reports it. `prerender = false` keeps this one route on demand even when the rest of the site is static. 3. **Update the tag in your base layout** Astro doesn't bundle a script tag that has attributes besides `src`, so the tag works as is. Adding `is:inline` makes that explicit. 4. **Point your script tag at your own paths** Replace your MetricRaven tag with this one. `src` loads the tracker from your domain and `data-api` sends its events there too. ```html title="Your website's shared layout" ``` > **Verify it works** > > Open your live site. In the browser's Network panel, `/a/script` loads the tracker and `/a/event` answers with `ok: true`. Nothing goes to `metricraven.com` from the browser. > > In **Traffic**, your own visit should show your real country and city. If every visit appears to come from your server's or host's location, the visitor IP isn't being forwarded. ## Notes - Static Astro builds have no server. Use the [Vercel](https://metricraven.com/docs/proxy-vercel), [Netlify](https://metricraven.com/docs/proxy-netlify) or [Cloudflare Workers](https://metricraven.com/docs/proxy-cloudflare-workers) recipe for your host. - With the Node adapter behind a load balancer, check that `clientAddress` is the visitor and not the balancer. How the proxy works and which headers it must send: [Use your own domain](https://metricraven.com/docs/proxy). ## Details - The proxy forwards only `Content-Type`, `Origin`, `User-Agent` and the visitor's IP as `x-visitor-ip`. Your site's cookies and auth headers never leave your server. - `Origin` matters: MetricRaven checks it against your site's allowed domains. A missing or wrong origin is refused with `domain_not_allowed`. - MetricRaven allows about 120 events per minute per visitor IP. Without `x-visitor-ip`, all of your visitors share the proxy's IP, so busy sites lose events as well as locations. - A proxy doesn't guarantee that every blocker will allow tracking. --- # Proxy with Vue or Nuxt > Proxy the MetricRaven script and events in the Vite dev server for Vue, or with a Nuxt server route. Source: https://metricraven.com/docs/proxy-vue-nuxt Markdown: https://metricraven.com/docs/proxy-vue-nuxt.md Section: Proxy Nuxt has a server, so a server route does the job. A Vite-built Vue app is static: proxy it on your host, and use the dev server proxy locally. ## Set it up 1. **Nuxt: add a server route** ```typescript title="server/routes/a/[kind].ts" // server/routes/a/[kind].ts · Nuxt 3 or 4 (Nitro server) export default defineEventHandler(async (event) => { const kind = getRouterParam(event, "kind"); const script = kind === "script" && event.method === "GET"; const isEvent = kind === "event" && event.method === "POST"; if (!script && !isEvent) throw createError({ statusCode: 404 }); const headers = new Headers(); for (const name of ["content-type", "origin", "user-agent"]) { const value = getRequestHeader(event, name); if (value) headers.set(name, value); } // Reads X-Forwarded-For: trust it only if your host overwrites it. const ip = getRequestIP(event, { xForwardedFor: true }); if (ip) headers.set("x-visitor-ip", ip); const upstream = await fetch(script ? "https://metricraven.com/js/script.js" : "https://metricraven.com/api/v1/collect", { method: event.method, headers, body: isEvent ? await readRawBody(event, false) : undefined, signal: AbortSignal.timeout(15000), }); return new Response(upstream.body, { status: upstream.status, headers: { "Content-Type": upstream.headers.get("content-type") || "application/octet-stream", "Cache-Control": script ? "public, max-age=3600" : "no-store", }, }); }); ``` Nuxt auto-imports the h3 helpers. Routes under `server/routes` have no `/api` prefix. 2. **Nuxt: update the script in nuxt.config.ts** In `app.head.script`, set `src: "/a/script"` and add `"data-api": "/a/event"`. 3. **Vue (Vite): proxy on your host in production** `vite build` outputs static files, so the production proxy lives on your host: [Netlify](https://metricraven.com/docs/proxy-netlify), [Vercel](https://metricraven.com/docs/proxy-vercel), [Cloudflare Workers](https://metricraven.com/docs/proxy-cloudflare-workers), [Nginx](https://metricraven.com/docs/proxy-nginx) or [Caddy](https://metricraven.com/docs/proxy-caddy). 4. **Vue (Vite), optional: the same paths in development** So the proxied tag also works on `npm run dev`, add the dev server proxy. It only runs locally, and local visits are tracked only with `data-allow-localhost`. ```typescript title="vite.config.ts" // vite.config.ts · the Vite dev server only (vite build output has no server) import { defineConfig, type ProxyOptions } from "vite"; const forwardVisitorIp: ProxyOptions["configure"] = (proxy) => proxy.on("proxyReq", (proxyReq, req) => { proxyReq.setHeader("x-visitor-ip", (req.socket.remoteAddress || "").replace(/^::ffff:/, "")); proxyReq.removeHeader("cookie"); }); export default defineConfig({ server: { proxy: { "/a/script": { target: "https://metricraven.com", changeOrigin: true, rewrite: () => "/js/script.js", configure: forwardVisitorIp, }, "/a/event": { target: "https://metricraven.com", changeOrigin: true, rewrite: () => "/api/v1/collect", configure: forwardVisitorIp, }, }, }, }); ``` 5. **Point your script tag at your own paths** Replace your MetricRaven tag with this one. `src` loads the tracker from your domain and `data-api` sends its events there too. ```html title="Your website's shared layout" ``` > **Verify it works** > > Open your live site. In the browser's Network panel, `/a/script` loads the tracker and `/a/event` answers with `ok: true`. Nothing goes to `metricraven.com` from the browser. > > In **Traffic**, your own visit should show your real country and city. If every visit appears to come from your server's or host's location, the visitor IP isn't being forwarded. ## Notes - Nuxt's `routeRules` proxy option can't add a per-visitor header, so it isn't enough on its own. Use the server route. How the proxy works and which headers it must send: [Use your own domain](https://metricraven.com/docs/proxy). ## Details - The proxy forwards only `Content-Type`, `Origin`, `User-Agent` and the visitor's IP as `x-visitor-ip`. Your site's cookies and auth headers never leave your server. - `Origin` matters: MetricRaven checks it against your site's allowed domains. A missing or wrong origin is refused with `domain_not_allowed`. - MetricRaven allows about 120 events per minute per visitor IP. Without `x-visitor-ip`, all of your visitors share the proxy's IP, so busy sites lose events as well as locations. - A proxy doesn't guarantee that every blocker will allow tracking. --- # Proxy with Nginx > Serve the MetricRaven script and events through Nginx with two location blocks. Source: https://metricraven.com/docs/proxy-nginx Markdown: https://metricraven.com/docs/proxy-nginx.md Section: Proxy Two exact-match location blocks relay the script and events, sending only the headers MetricRaven needs. ## Set it up 1. **Create the shared proxy settings** ```nginx title="/etc/nginx/snippets/metricraven-proxy.conf" # /etc/nginx/snippets/metricraven-proxy.conf proxy_ssl_server_name on; proxy_http_version 1.1; # Send only what the collector needs; your site's cookies stay on your server. proxy_pass_request_headers off; proxy_set_header Host metricraven.com; proxy_set_header Content-Type $content_type; proxy_set_header Origin $http_origin; proxy_set_header User-Agent $http_user_agent; # The visitor's IP. Behind a CDN or load balancer, configure the realip module first. proxy_set_header X-Visitor-IP $remote_addr; ``` 2. **Add the locations to your server block** ```nginx title="sites-available/example.com" # Inside your site's existing server { } block location = /a/script { limit_except GET { deny all; } proxy_pass https://metricraven.com/js/script.js; include /etc/nginx/snippets/metricraven-proxy.conf; proxy_hide_header Cache-Control; add_header Cache-Control "public, max-age=3600" always; } location = /a/event { limit_except POST { deny all; } proxy_pass https://metricraven.com/api/v1/collect; include /etc/nginx/snippets/metricraven-proxy.conf; proxy_hide_header Cache-Control; add_header Cache-Control "no-store" always; } ``` 3. **Test and reload** ```bash sudo nginx -t && sudo systemctl reload nginx ``` 4. **Point your script tag at your own paths** Replace your MetricRaven tag with this one. `src` loads the tracker from your domain and `data-api` sends its events there too. ```html title="Your website's shared layout" ``` > **Verify it works** > > Open your live site. In the browser's Network panel, `/a/script` loads the tracker and `/a/event` answers with `ok: true`. Nothing goes to `metricraven.com` from the browser. > > In **Traffic**, your own visit should show your real country and city. If every visit appears to come from your server's or host's location, the visitor IP isn't being forwarded. ## Notes - `proxy_ssl_server_name on` is required: MetricRaven is served over HTTPS behind a CDN that needs the hostname during the TLS handshake. - Behind a CDN or load balancer, `$remote_addr` is that proxy. Configure `set_real_ip_from` and `real_ip_header` (the realip module) so it becomes the visitor. - `proxy_pass_request_headers off` stops Nginx from forwarding your site's cookies. The headers set after it are the only ones sent. How the proxy works and which headers it must send: [Use your own domain](https://metricraven.com/docs/proxy). ## Details - The proxy forwards only `Content-Type`, `Origin`, `User-Agent` and the visitor's IP as `x-visitor-ip`. Your site's cookies and auth headers never leave your server. - `Origin` matters: MetricRaven checks it against your site's allowed domains. A missing or wrong origin is refused with `domain_not_allowed`. - MetricRaven allows about 120 events per minute per visitor IP. Without `x-visitor-ip`, all of your visitors share the proxy's IP, so busy sites lose events as well as locations. - A proxy doesn't guarantee that every blocker will allow tracking. --- # Proxy with Caddy > Serve the MetricRaven script and events through Caddy's reverse_proxy. Source: https://metricraven.com/docs/proxy-caddy Markdown: https://metricraven.com/docs/proxy-caddy.md Section: Proxy Two handle blocks rewrite your paths and reverse-proxy them to MetricRaven. ## Set it up 1. **Add a reusable snippet** ```text title="Caddyfile" # Top of the same Caddyfile, outside any site block (metricraven_upstream) { header_up Host {upstream_hostport} # The visitor's IP. Behind a CDN or load balancer, set trusted_proxies first. header_up X-Visitor-IP {client_ip} header_up -Cookie header_up -Authorization header_up -Referer } ``` 2. **Add the handlers to your site block** Replace `example.com` with your existing site address and keep your current directives below the two handlers. ```text title="Caddyfile" # Caddyfile · Caddy 2.7 or later example.com { @metricravenScript { method GET path /a/script } handle @metricravenScript { rewrite * /js/script.js reverse_proxy https://metricraven.com { import metricraven_upstream header_down Cache-Control "public, max-age=3600" } } @metricravenEvent { method POST path /a/event } handle @metricravenEvent { rewrite * /api/v1/collect reverse_proxy https://metricraven.com { import metricraven_upstream header_down Cache-Control "no-store" } } # ...your existing site configuration } ``` 3. **Reload Caddy** ```bash caddy reload --config /etc/caddy/Caddyfile ``` 4. **Point your script tag at your own paths** Replace your MetricRaven tag with this one. `src` loads the tracker from your domain and `data-api` sends its events there too. ```html title="Your website's shared layout" ``` > **Verify it works** > > Open your live site. In the browser's Network panel, `/a/script` loads the tracker and `/a/event` answers with `ok: true`. Nothing goes to `metricraven.com` from the browser. > > In **Traffic**, your own visit should show your real country and city. If every visit appears to come from your server's or host's location, the visitor IP isn't being forwarded. ## Notes - `{client_ip}` needs Caddy 2.7 or later. Behind a CDN or load balancer, set `trusted_proxies` in the global `servers` options so it is the visitor, not the proxy. - `header_up Host {upstream_hostport}` is required for an HTTPS upstream; without it the request reaches MetricRaven with your site's hostname and fails. How the proxy works and which headers it must send: [Use your own domain](https://metricraven.com/docs/proxy). ## Details - The proxy forwards only `Content-Type`, `Origin`, `User-Agent` and the visitor's IP as `x-visitor-ip`. Your site's cookies and auth headers never leave your server. - `Origin` matters: MetricRaven checks it against your site's allowed domains. A missing or wrong origin is refused with `domain_not_allowed`. - MetricRaven allows about 120 events per minute per visitor IP. Without `x-visitor-ip`, all of your visitors share the proxy's IP, so busy sites lose events as well as locations. - A proxy doesn't guarantee that every blocker will allow tracking. --- # Proxy with Cloudflare Workers > Serve the MetricRaven script and events from a Cloudflare Worker on your own domain. Source: https://metricraven.com/docs/proxy-cloudflare-workers Markdown: https://metricraven.com/docs/proxy-cloudflare-workers.md Section: Proxy If your domain is on Cloudflare, a Worker on one route serves the script and events and passes Cloudflare's own visitor location. ## Set it up 1. **Create the Worker** ```javascript title="src/index.js" // src/index.js · Cloudflare Worker on the route example.com/a/* export default { async fetch(request) { const { pathname } = new URL(request.url); const script = pathname === "/a/script" && request.method === "GET"; const event = pathname === "/a/event" && request.method === "POST"; if (!script && !event) return new Response("Not found", { status: 404 }); const headers = new Headers(); for (const name of ["content-type", "origin", "user-agent"]) { const value = request.headers.get(name); if (value) headers.set(name, value); } const ip = request.headers.get("cf-connecting-ip"); if (ip) headers.set("x-visitor-ip", ip); // Cloudflare's location for this visitor. Names are URL-encoded so accented cities survive. const cf = request.cf || {}; for (const [name, value] of [ ["x-visitor-country", cf.country], ["x-visitor-region", cf.region], ["x-visitor-city", cf.city], ["x-visitor-latitude", cf.latitude], ["x-visitor-longitude", cf.longitude], ]) { if (ip && value) headers.set(name, encodeURIComponent(value)); } const upstream = await fetch(script ? "https://metricraven.com/js/script.js" : "https://metricraven.com/api/v1/collect", { method: request.method, headers, body: event ? await request.arrayBuffer() : undefined, }); return new Response(upstream.body, { status: upstream.status, headers: { "Content-Type": upstream.headers.get("content-type") || "application/octet-stream", "Cache-Control": script ? "public, max-age=3600" : "no-store", }, }); }, }; ``` 2. **Route it to /a/\* on your domain** Replace both `example.com` values with your domain, then deploy: ```toml title="wrangler.toml" # wrangler.toml name = "metricraven-proxy" main = "src/index.js" compatibility_date = "2026-10-01" [[routes]] pattern = "example.com/a/*" zone_name = "example.com" ``` ```bash npx wrangler deploy ``` You can also add the route in the Cloudflare dashboard, under the Worker's **Settings → Domains & Routes**. 3. **Point your script tag at your own paths** Replace your MetricRaven tag with this one. `src` loads the tracker from your domain and `data-api` sends its events there too. ```html title="Your website's shared layout" ``` > **Verify it works** > > Open your live site. In the browser's Network panel, `/a/script` loads the tracker and `/a/event` answers with `ok: true`. Nothing goes to `metricraven.com` from the browser. > > In **Traffic**, your own visit should show your real country and city. If every visit appears to come from your server's or host's location, the visitor IP isn't being forwarded. ## Notes - The domain must be proxied through Cloudflare (orange cloud) for a Worker route to run on it. - `cf-connecting-ip` is set by Cloudflare and can't be faked by visitors. It must still be sent as `x-visitor-ip`: requests from your Worker reach MetricRaven with the Worker's address as the connecting IP. - If the route pattern is wider than `/a/script` and `/a/event`, the Worker answers other `/a/` paths with 404. Pick a prefix your site doesn't use. How the proxy works and which headers it must send: [Use your own domain](https://metricraven.com/docs/proxy). ## Details - The proxy forwards only `Content-Type`, `Origin`, `User-Agent` and the visitor's IP as `x-visitor-ip`. Your site's cookies and auth headers never leave your server. - `Origin` matters: MetricRaven checks it against your site's allowed domains. A missing or wrong origin is refused with `domain_not_allowed`. - MetricRaven allows about 120 events per minute per visitor IP. Without `x-visitor-ip`, all of your visitors share the proxy's IP, so busy sites lose events as well as locations. - A proxy doesn't guarantee that every blocker will allow tracking. --- # Proxy with Vercel > Use vercel.json rewrites and a Vercel Function to serve MetricRaven from your domain. Source: https://metricraven.com/docs/proxy-vercel Markdown: https://metricraven.com/docs/proxy-vercel.md Section: Proxy vercel.json rewrites your two paths to a small Vercel Function, which forwards the visitor's IP and location. ## Set it up 1. **Add the function** Works for any framework or a static site deployed to Vercel. On Next.js, use the [Next.js route handler](https://metricraven.com/docs/proxy-nextjs) instead. ```javascript title="api/metricraven.js" // api/metricraven.js · Vercel Function for any framework or a static site async function relay(request) { const kind = new URL(request.url).searchParams.get("kind"); const script = kind === "script" && request.method === "GET"; const event = kind === "event" && request.method === "POST"; if (!script && !event) return new Response("Not found", { status: 404 }); const headers = new Headers(); for (const name of ["content-type", "origin", "user-agent"]) { const value = request.headers.get(name); if (value) headers.set(name, value); } // Vercel overwrites x-forwarded-for with the visitor's IP, so it can't be spoofed. const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim(); if (ip) { headers.set("x-visitor-ip", ip); for (const [source, target] of [ ["x-vercel-ip-country", "x-visitor-country"], ["x-vercel-ip-city", "x-visitor-city"], ["x-vercel-ip-latitude", "x-visitor-latitude"], ["x-vercel-ip-longitude", "x-visitor-longitude"], ]) { const value = request.headers.get(source); if (value) headers.set(target, value); } } const upstream = await fetch(script ? "https://metricraven.com/js/script.js" : "https://metricraven.com/api/v1/collect", { method: request.method, headers, body: event ? await request.arrayBuffer() : undefined, signal: AbortSignal.timeout(15000), }); return new Response(upstream.body, { status: upstream.status, headers: { "Content-Type": upstream.headers.get("content-type") || "application/octet-stream", "Cache-Control": script ? "public, max-age=3600" : "no-store", }, }); } export { relay as GET, relay as POST }; ``` 2. **Add the rewrites** Merge these into your existing `vercel.json`, above any catch-all rewrite: ```json title="vercel.json" { "rewrites": [ { "source": "/a/script", "destination": "/api/metricraven?kind=script" }, { "source": "/a/event", "destination": "/api/metricraven?kind=event" } ] } ``` 3. **Deploy** Push or run `vercel deploy`. Rewrites and functions apply to preview deployments too. 4. **Point your script tag at your own paths** Replace your MetricRaven tag with this one. `src` loads the tracker from your domain and `data-api` sends its events there too. ```html title="Your website's shared layout" ``` > **Verify it works** > > Open your live site. In the browser's Network panel, `/a/script` loads the tracker and `/a/event` answers with `ok: true`. Nothing goes to `metricraven.com` from the browser. > > In **Traffic**, your own visit should show your real country and city. If every visit appears to come from your server's or host's location, the visitor IP isn't being forwarded. ## Notes - A rewrite straight to `https://metricraven.com/...` is not enough: it can't add `x-visitor-ip`, so every visit would appear to come from Vercel. Keep the function in the middle. - Vercel overwrites `x-forwarded-for` with the visitor's IP and provides `x-vercel-ip-*` location headers, which the function forwards. Region is left to MetricRaven's own lookup because Vercel sends a code rather than a name. How the proxy works and which headers it must send: [Use your own domain](https://metricraven.com/docs/proxy). ## Details - The proxy forwards only `Content-Type`, `Origin`, `User-Agent` and the visitor's IP as `x-visitor-ip`. Your site's cookies and auth headers never leave your server. - `Origin` matters: MetricRaven checks it against your site's allowed domains. A missing or wrong origin is refused with `domain_not_allowed`. - MetricRaven allows about 120 events per minute per visitor IP. Without `x-visitor-ip`, all of your visitors share the proxy's IP, so busy sites lose events as well as locations. - A proxy doesn't guarantee that every blocker will allow tracking. --- # Proxy with Netlify > Serve the MetricRaven script and events from a Netlify Function on your own domain. Source: https://metricraven.com/docs/proxy-netlify Markdown: https://metricraven.com/docs/proxy-netlify.md Section: Proxy A Netlify Function mounted on your two paths relays the script and events with Netlify's visitor IP and location. ## Set it up 1. **Add the function** ```javascript title="netlify/functions/metricraven.mjs" // netlify/functions/metricraven.mjs · Netlify Functions export default async (request, context) => { const { pathname } = new URL(request.url); const script = pathname === "/a/script" && request.method === "GET"; const event = pathname === "/a/event" && request.method === "POST"; if (!script && !event) return new Response("Not found", { status: 404 }); const headers = new Headers({ "x-visitor-ip": context.ip }); for (const name of ["content-type", "origin", "user-agent"]) { const value = request.headers.get(name); if (value) headers.set(name, value); } const geo = context.geo || {}; for (const [name, value] of [ ["x-visitor-country", geo.country?.code], ["x-visitor-region", geo.subdivision?.name], ["x-visitor-city", geo.city], ["x-visitor-latitude", geo.latitude], ["x-visitor-longitude", geo.longitude], ]) { if (value) headers.set(name, encodeURIComponent(value)); } const upstream = await fetch(script ? "https://metricraven.com/js/script.js" : "https://metricraven.com/api/v1/collect", { method: request.method, headers, body: event ? await request.arrayBuffer() : undefined, signal: AbortSignal.timeout(15000), }); return new Response(upstream.body, { status: upstream.status, headers: { "Content-Type": upstream.headers.get("content-type") || "application/octet-stream", "Cache-Control": script ? "public, max-age=3600" : "no-store", }, }); }; // Serve the function at these paths on your site's own domain. export const config = { path: ["/a/script", "/a/event"] }; ``` `config.path` serves the function at `/a/script` and `/a/event` directly, so it needs no `_redirects` lines. 2. **Deploy** Push to your connected repository or run `netlify deploy --prod`. 3. **Point your script tag at your own paths** Replace your MetricRaven tag with this one. `src` loads the tracker from your domain and `data-api` sends its events there too. ```html title="Your website's shared layout" ``` > **Verify it works** > > Open your live site. In the browser's Network panel, `/a/script` loads the tracker and `/a/event` answers with `ok: true`. Nothing goes to `metricraven.com` from the browser. > > In **Traffic**, your own visit should show your real country and city. If every visit appears to come from your server's or host's location, the visitor IP isn't being forwarded. ## Notes - A `_redirects` proxy rule such as `/a/event https://metricraven.com/api/v1/collect 200` is not enough on its own: it can't add `x-visitor-ip`, so every visit would appear to come from Netlify. How the proxy works and which headers it must send: [Use your own domain](https://metricraven.com/docs/proxy). ## Details - The proxy forwards only `Content-Type`, `Origin`, `User-Agent` and the visitor's IP as `x-visitor-ip`. Your site's cookies and auth headers never leave your server. - `Origin` matters: MetricRaven checks it against your site's allowed domains. A missing or wrong origin is refused with `domain_not_allowed`. - MetricRaven allows about 120 events per minute per visitor IP. Without `x-visitor-ip`, all of your visitors share the proxy's IP, so busy sites lose events as well as locations. - A proxy doesn't guarantee that every blocker will allow tracking. --- # Proxy with Firebase Hosting > Rewrite two Firebase Hosting paths to a Cloud Function that relays MetricRaven. Source: https://metricraven.com/docs/proxy-firebase Markdown: https://metricraven.com/docs/proxy-firebase.md Section: Proxy Firebase Hosting rewrites your two paths to a Cloud Function, which forwards the visitor's IP. ## Set it up 1. **Add the function** Cloud Functions need the Blaze (pay as you go) plan. Use Node.js 18 or later for the built-in `fetch`. ```javascript title="functions/index.js" // functions/index.js · Cloud Functions for Firebase (2nd gen) const { onRequest } = require("firebase-functions/v2/https"); exports.metricraven = onRequest({ region: "us-central1" }, async (req, res) => { const script = req.path === "/a/script" && req.method === "GET"; const event = req.path === "/a/event" && req.method === "POST"; if (!script && !event) return res.status(404).end(); const headers = {}; for (const name of ["content-type", "origin", "user-agent"]) { if (req.headers[name]) headers[name] = req.headers[name]; } // Hosting's CDN puts the visitor's address here; confirm it on your project (see below). const ip = req.headers["fastly-client-ip"] || req.headers["x-forwarded-for"]?.split(",")[0]?.trim(); if (ip) headers["x-visitor-ip"] = ip; try { const upstream = await fetch(script ? "https://metricraven.com/js/script.js" : "https://metricraven.com/api/v1/collect", { method: req.method, headers, body: event ? req.rawBody : undefined, signal: AbortSignal.timeout(15000), }); res .status(upstream.status) .set("Content-Type", upstream.headers.get("content-type") || "application/octet-stream") .set("Cache-Control", script ? "public, max-age=3600" : "no-store") .send(Buffer.from(await upstream.arrayBuffer())); } catch { res.status(502).end(); } }); ``` 2. **Rewrite the paths to it** Add the two rewrites to `firebase.json`, above any `**` catch-all, and keep the region the same as the function's: ```json title="firebase.json" { "hosting": { "public": "public", "rewrites": [ { "source": "/a/script", "function": { "functionId": "metricraven", "region": "us-central1" } }, { "source": "/a/event", "function": { "functionId": "metricraven", "region": "us-central1" } } ] } } ``` 3. **Deploy** ```bash firebase deploy --only functions,hosting ``` 4. **Point your script tag at your own paths** Replace your MetricRaven tag with this one. `src` loads the tracker from your domain and `data-api` sends its events there too. ```html title="Your website's shared layout" ``` > **Verify it works** > > Open your live site. In the browser's Network panel, `/a/script` loads the tracker and `/a/event` answers with `ok: true`. Nothing goes to `metricraven.com` from the browser. > > In **Traffic**, your own visit should show your real country and city. If every visit appears to come from your server's or host's location, the visitor IP isn't being forwarded. ## Notes - Firebase doesn't document which header carries the visitor IP through a Hosting rewrite. The function reads `fastly-client-ip`, then the first `x-forwarded-for` entry. After deploying, confirm your own visit shows your city; if not, log `req.headers` once and pick the header with your IP. - A Hosting rewrite with a `destination` URL can't forward to another domain with a per-visitor header, which is why a function is needed. How the proxy works and which headers it must send: [Use your own domain](https://metricraven.com/docs/proxy). ## Details - The proxy forwards only `Content-Type`, `Origin`, `User-Agent` and the visitor's IP as `x-visitor-ip`. Your site's cookies and auth headers never leave your server. - `Origin` matters: MetricRaven checks it against your site's allowed domains. A missing or wrong origin is refused with `domain_not_allowed`. - MetricRaven allows about 120 events per minute per visitor IP. Without `x-visitor-ip`, all of your visitors share the proxy's IP, so busy sites lose events as well as locations. - A proxy doesn't guarantee that every blocker will allow tracking. --- # Proxy with DigitalOcean App Platform > Route /a on App Platform to a small proxy service that forwards the visitor IP from do-connecting-ip. Source: https://metricraven.com/docs/proxy-digitalocean Markdown: https://metricraven.com/docs/proxy-digitalocean.md Section: Proxy App Platform can route a path prefix to its own component. Send /a to a small proxy service and read the visitor IP from App Platform's header. ## Set it up 1. **Already running a server component?** If your app is Express, Flask, FastAPI, Laravel or another server, add that stack's recipe to it directly and skip the separate service. Take the visitor IP from `do-connecting-ip` (see the next step). 2. **Create a proxy service** For a static site, deploy the [Express recipe](https://metricraven.com/docs/proxy-express) as a service component, reading App Platform's visitor IP header: ```javascript title="metricraven-proxy.js" // The Express recipe, reading App Platform's visitor IP header. // Replace the first line of relay() with: const headers = { "x-visitor-ip": req.get("do-connecting-ip") || req.ip }; ``` 3. **Route /a to the service** In the app spec, or under **Settings → Components** in the control panel, give the service the `/a` route and keep the path prefix so it receives `/a/script` and `/a/event`: ```yaml title=".do/app.yaml" # .do/app.yaml (excerpt) · route /a to a small proxy service services: - name: metricraven-proxy github: repo: your-org/metricraven-proxy branch: main run_command: node metricraven-proxy.js http_port: 8080 envs: - key: PORT value: "8080" ingress: rules: - match: path: prefix: /a component: name: metricraven-proxy preserve_path_prefix: true # ...your existing rules for the rest of the site ``` 4. **Point your script tag at your own paths** Replace your MetricRaven tag with this one. `src` loads the tracker from your domain and `data-api` sends its events there too. ```html title="Your website's shared layout" ``` > **Verify it works** > > Open your live site. In the browser's Network panel, `/a/script` loads the tracker and `/a/event` answers with `ok: true`. Nothing goes to `metricraven.com` from the browser. > > In **Traffic**, your own visit should show your real country and city. If every visit appears to come from your server's or host's location, the visitor IP isn't being forwarded. ## Notes - DigitalOcean documents `do-connecting-ip` as the client IP on App Platform; `x-forwarded-for` there holds the ingress address, so don't use it. How the proxy works and which headers it must send: [Use your own domain](https://metricraven.com/docs/proxy). ## Details - The proxy forwards only `Content-Type`, `Origin`, `User-Agent` and the visitor's IP as `x-visitor-ip`. Your site's cookies and auth headers never leave your server. - `Origin` matters: MetricRaven checks it against your site's allowed domains. A missing or wrong origin is refused with `domain_not_allowed`. - MetricRaven allows about 120 events per minute per visitor IP. Without `x-visitor-ip`, all of your visitors share the proxy's IP, so busy sites lose events as well as locations. - A proxy doesn't guarantee that every blocker will allow tracking. --- # API overview > Base URL, which key to use for each endpoint, the site parameter, response and error formats, rate limits, pagination and date ranges. Source: https://metricraven.com/docs/api-overview Markdown: https://metricraven.com/docs/api-overview.md Section: API reference The REST API sends events and payments from your server, reads every dashboard report, and manages chart notes, funnels, segments, settings, alerts and team members. Everything the dashboard, the [CLI](https://metricraven.com/docs/cli) and the [MCP server](https://metricraven.com/docs/mcp) show comes from these endpoints. ## Base URL Every path on these pages is relative to `https://metricraven.com/api/v1`. Send JSON request bodies with `Content-Type: application/json`; every response is JSON, except `204 No Content`. ```bash curl 'https://metricraven.com/api/v1/stats/traffic?range=all' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" ``` ## Which key to use Send a key as `Authorization: Bearer …`. How to create each one is in [Authentication and access tokens](https://metricraven.com/docs/api-authentication). | Credential | Use it for | | --- | --- | | Site secret key (ana_sec_…) | Your backend, for one website. The only key that sends events, identify calls, payments and crawler requests, and the only key besides the dashboard that deletes a person's data. Also reads every report and changes settings of its own website. | | Account access token (mr_…) | Scripts, the CLI and AI assistants. Acts as you across websites, limited to its scopes, its websites and your role. Required for /account, /websites, /members and notification preferences. Never sends events or payments. | | Share link token (?share=…) | Public and embedded dashboards. Read-only aggregate reports without names, emails or visitor IDs. Send it as the share query parameter instead of an Authorization header. | ## The site parameter - **Account tokens** must send exactly one `site=WEBSITE_ID` on every website endpoint. A missing, repeated or unknown `site`, or a website the token is limited away from, returns 401. - **Site secret keys** already identify their website, so `site` is optional. If you send it, it must be that key's website ID; any other value returns 401. - Website IDs look like `ana_pub_1a2b3c4d5e6f7a8b`. List yours with [GET /websites](https://metricraven.com/docs/account-api#list-websites). ## Access levels Each website endpoint needs one of four access levels. A credential that lacks it gets 401. | Level | Secret key | Account token | Share link | | --- | --- | --- | --- | | read: aggregate reports, funnels | Yes | read scope | Yes | | live: realtime, chart notes, commits | Yes | read scope | No | | private: people and private configuration | Yes | visitors scope | No | | write: changes | Yes | write scope, admin or owner role | No | Reports read with a share link, or with a token without the `visitors` scope, return `null` instead of customer names, emails, user IDs and visitor IDs. An account token on a website where you are a viewer can read but never write, whatever its scopes. ## Endpoints ### Send data (secret key only) | Request | Reference | | --- | --- | | POST /events | [Server-side goals](https://metricraven.com/docs/server-events) | | POST /identify | [Identify users](https://metricraven.com/docs/identify-users#server) | | POST /payments | [Payments API](https://metricraven.com/docs/payments-api) | | POST /crawls | [Crawler requests (below)](https://metricraven.com/docs/api-overview#crawls) | | POST /collect | The browser script and SDKs; no key, checked against your domains and app IDs. See [Proxy](https://metricraven.com/docs/proxy). | ### Reports | Request | Access | Reference | | --- | --- | --- | | GET /stats/traffic | read | [Traffic](https://metricraven.com/docs/reports-api#traffic) | | GET /stats/breakdown | read | [Breakdown](https://metricraven.com/docs/reports-api#breakdown) | | GET /stats/filter-values | read | [Filter values](https://metricraven.com/docs/reports-api#filter-values) | | GET /stats/conversions | read | [Conversions and funnels](https://metricraven.com/docs/reports-api#conversions) | | GET /stats/goal | read | [One goal](https://metricraven.com/docs/reports-api#goal) | | GET /stats/insights | read | [Buyer insights](https://metricraven.com/docs/reports-api#insights) | | GET /stats/revenue | read | [Revenue](https://metricraven.com/docs/reports-api#revenue) | | GET /stats/subscriptions | read | [Subscriptions](https://metricraven.com/docs/reports-api#subscriptions) | | GET /stats/search | read | [Search Console](https://metricraven.com/docs/reports-api#search) | | GET /stats/ads | read | [Ad spend and ROAS, every network](https://metricraven.com/docs/reports-api#ads) | | GET /stats/meta-ads | read | [Meta Ads only (alias)](https://metricraven.com/docs/reports-api#meta-ads) | | GET /stats/currencies | read | [Reporting currency and exchange rates](https://metricraven.com/docs/reports-api#endpoints) | | GET /stats/crawlers | read | [Crawlers](https://metricraven.com/docs/reports-api#crawlers) | | GET /stats/crawler-pages | read | [Crawled pages search](https://metricraven.com/docs/reports-api#crawler-pages) | | GET /stats/x-referrers | read | [X links](https://metricraven.com/docs/reports-api#x-referrers) | | GET /stats/mentions | read | [Social mentions](https://metricraven.com/docs/reports-api#mentions) | | GET /stats/ai-visibility | read | [AI visibility](https://metricraven.com/docs/reports-api#ai-visibility) | | GET /stats/overview | read | [Compact overview](https://metricraven.com/docs/reports-api#overview) | | GET /stats/live | live | [Realtime](https://metricraven.com/docs/reports-api#live) | | GET /stats/live/replay | live | [Realtime replay](https://metricraven.com/docs/reports-api#live-replay) | | GET /stats/live/value | live | [Value of visitors online](https://metricraven.com/docs/reports-api#live-value) | | GET /stats/visitors | private | [Visitor search](https://metricraven.com/docs/reports-api#visitors) | | GET /stats/journeys | private | [Journeys](https://metricraven.com/docs/reports-api#journeys) | ### Website configuration and people | Request | Access | Reference | | --- | --- | --- | | GET /notes | live | [Chart notes](https://metricraven.com/docs/site-api#notes) | | POST /notes, PUT /notes/ID, DELETE /notes/ID | write | [Chart notes](https://metricraven.com/docs/site-api#notes) | | GET /commits | live | [GitHub commits](https://metricraven.com/docs/site-api#commits) | | GET /funnels | read | [Funnels](https://metricraven.com/docs/site-api#funnels) | | POST /funnels, PUT /funnels/ID, DELETE /funnels/ID | write | [Funnels](https://metricraven.com/docs/site-api#funnels) | | GET /segments | private | [Segments](https://metricraven.com/docs/site-api#segments) | | POST /segments, PUT /segments/ID, DELETE /segments/ID | write | [Segments](https://metricraven.com/docs/site-api#segments) | | GET /site | private | [Settings](https://metricraven.com/docs/site-api#settings) | | PUT /site | write | [Settings](https://metricraven.com/docs/site-api#settings) | | GET, POST /site/keys; PATCH, DELETE /site/keys/ID; POST /site/keys/ID/roll | write, no secret key | [Secret keys](https://metricraven.com/docs/site-api#secret-keys) | | GET /goals/customizations | read, no share links | [Goal names and colours](https://metricraven.com/docs/goals-funnels#customize-goals) | | PUT, DELETE /goals/customizations | write | [Goal names and colours](https://metricraven.com/docs/goals-funnels#customize-goals) | | GET /visitors/VISITOR_ID | private | [Visitor profile](https://metricraven.com/docs/visitors-api#profile) | | DELETE /people | write, no tokens | [Erase a person](https://metricraven.com/docs/visitors-api#erase) | | DELETE /payments | write | [Delete payments](https://metricraven.com/docs/payments-api#delete) | | DELETE /events | write | [Delete goal events](https://metricraven.com/docs/server-events#delete) | | GET, PUT, PATCH /bot-traffic/settings; GET, POST, PUT, DELETE /bot-traffic/token; GET /bot-traffic/usage | read / write, account token | [Bot traffic settings](https://metricraven.com/docs/bot-traffic-settings#api) | | GET, POST, DELETE /managed-proxy; POST /managed-proxy/check; GET /managed-proxy/dns-provider | read / write, account token | [Managed proxy](https://metricraven.com/docs/managed-proxy#api) | | GET, POST /alerts; GET, PUT, DELETE /alerts/ID; GET /alerts/history | read / write, no share links | [Conversion alerts](https://metricraven.com/docs/goal-alerts#api) | | GET, PUT /notifications | token or dashboard only | [Email preferences](https://metricraven.com/docs/email-reports#api) | ### Account (account tokens only) | Request | Reference | | --- | --- | | GET, PATCH /account | [Account API](https://metricraven.com/docs/account-api#endpoints) | | GET, POST /websites | [Websites](https://metricraven.com/docs/account-api#list-websites) | | GET, PATCH, DELETE /websites/ID | [Update or delete a website](https://metricraven.com/docs/account-api#update-website) | | GET, POST /tokens; DELETE /tokens/ID | [Access tokens](https://metricraven.com/docs/account-api#tokens) | | GET, POST /members; PATCH, DELETE /members/USER_ID; POST, DELETE /members/invites/ID | [Team members](https://metricraven.com/docs/account-api#members) | | GET /health | Service status: ok, db and redis. No key. | ## Responses Most endpoints return the object itself: a report, or a list under a named key such as `{ "notes": [...] }` or `{ "funnels": [...] }`. Alerts, notification preferences and team members wrap their result as `{ "status": "success", "data": ... }`. Social mentions return `data`, `pagination` and `meta`. - Timestamps are ISO 8601 strings in UTC. Chart notes use calendar dates (YYYY-MM-DD). - Money is in major units of the report currency: 29 means $29.00. Rates and percentages are 0–100, rounded to two decimals. - A change between periods is a relative percentage, or `null` when the previous value is zero. - New fields can be added at any time; ignore those you don't know. ## Errors Errors have an HTTP status and a JSON body with a machine-readable `error` code. Many add a readable `message`; schema failures add `details` with the invalid fields. ```json title="400 response" { "error": "invalid_payload", "details": { "formErrors": [], "fieldErrors": { "currency": ["Invalid"] } } } ``` | Status | Common error codes | | --- | --- | | 400 | invalid_json, invalid_payload, invalid_range, invalid_breakdown_query, invalid_filter_query, invalid_visitor_query, invalid_goal, invalid_steps, invalid_hours, invalid_currency, invalid_note, invalid_funnel, invalid_segment, invalid_settings, invalid_domain, invalid_request. | | 401 unauthorized | Missing, mistyped, deleted or rotated key; missing or wrong site; a token without the scope; a website the token can't use; or a write your role doesn't allow. | | 402 | tracking_paused: the plan's monthly event allowance is used up, so new events and payments are refused until the cycle resets or you upgrade. website_limit and member_limit: plan limits on the account API. | | 403 | domain_not_allowed (crawler or browser event for a domain the website doesn't list), app_not_allowed, insufficient_scope or forbidden (account API), account_token_required (team endpoints called with a secret key), forbidden (account token on DELETE /people). | | 404 | not_found (note, funnel, segment, visitor or person), unknown_funnel, unknown_site (collect). | | 409 | note_limit, segment_limit, duplicate_segment_name, segment_changed (a stale segment revision: reload and retry). | | 429 | rate_limited or too_many_attempts; see rate limits below. | ## Rate limits | Endpoint | Limit | | --- | --- | | POST /collect | 120 requests a minute per visitor IP and website. Over it: 429 rate_limited with Retry-After: 60. | | POST /members (invitations) | 30 an hour per IP: 429 too_many_attempts. | | MCP (/api/mcp) | 240 requests a minute per token or site key. | | Report, configuration and server ingestion endpoints | No fixed request limit. Keep to a few concurrent requests per website. | Reports are cached for 60 seconds per website, range and parameter set, so polling faster than once a minute returns the same numbers. Add `fresh=1` to recompute. ## Pagination and list sizes Paginated endpoints take `offset` (rows to skip, default 0) and `limit` (1–100, default 50) and return `total`, `offset` and `nextOffset`. `nextOffset` is `null` on the last page. ```bash curl --get 'https://metricraven.com/api/v1/stats/breakdown' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" \ --data-urlencode 'range=all' \ --data-urlencode 'dimension=page' \ --data-urlencode 'limit=100' \ --data-urlencode 'offset=100' ``` | Endpoint | Paging | | --- | --- | | /stats/breakdown, /stats/filter-values, /stats/visitors | offset, limit (1–100), total, nextOffset. | | /stats/journeys | offset, limit (1–100, default 25) and total. | | /stats/mentions | offset, limit (1–1000, default 100) and pagination.hasMore. | | Lists inside reports | Top 50 per breakdown in /stats/traffic (countries are complete), top 50 crawler pages, 200 X links, 500 commits, 500 realtime visitors and 1,000 notes. Use /stats/breakdown for complete tables. | ## Date ranges and time zones | Parameter | Meaning | | --- | --- | | from, to | UTC instants in ISO 8601 with a Z suffix, for example 2026-10-01T00:00:00.000Z. Offsets such as +02:00 and plain dates are rejected. from is inclusive and to exclusive; from must be before to, at most 36,600 days apart. | | range=all | Instead of from and to: from the start of the day of the website's first recorded event, payment or crawl, to now. | | timezone | IANA zone such as Europe/Madrid (default UTC). Groups days, weeks and months, and the weekday/hour heatmaps, in that zone. It doesn't move from and to: send the UTC instants of your local midnight. | | granularity | auto, hour, day (default), week or month. Hourly becomes daily over 3 days, daily becomes weekly over 400 days and weekly becomes monthly over 4,000 days. The value used is returned in period.granularity. | Reports that compare periods use the window of the same length that ends at `from`. Chart notes, the subscriptions report and realtime endpoints take no range. ## Send crawler requests `POST /crawls` records one request by a crawler that your server or edge middleware handled. Crawlers rarely run JavaScript, so this is how AI and search bots reach [/stats/crawlers](https://metricraven.com/docs/reports-api#crawlers). Ready-made middleware is in [Bots and AI crawlers](https://metricraven.com/docs/bots-ai-crawlers). ```bash curl --request POST 'https://metricraven.com/api/v1/crawls' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" \ --header 'Content-Type: application/json' \ --data '{ "url": "https://example.com/pricing", "userAgent": "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", "status": 200 }' ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | url | string | Yes | Full URL that was requested, up to 2,000 characters. Its host must be one of the website's domains. | | userAgent | string | Yes | The request's User-Agent header, 1–1,000 characters. | | status | integer | No | HTTP status you answered with, 100–599. | | Response | Meaning | | --- | --- | | 200 { "ok": true, "bot": "GPTBot", "type": "ai-training" } | Recorded. | | 200 { "ok": true, "ignored": "not_a_crawler" } | Not a known crawler, for example a normal browser. Nothing is stored; you can send every request. | | 400 invalid_payload | Missing or invalid url, userAgent or status. | | 401 unauthorized | Not a site secret key. | | 403 domain_not_allowed | The URL's host isn't one of the website's domains. | Crawler types are `ai-training`, `ai-search`, `ai-assistant`, `search-engine`, `seo`, `social-preview` and `other`. Requests for robots.txt, llms.txt, llms-full.txt, sitemaps and Markdown files are also counted as special files. Crawler requests are metered separately from events and are never refused for plan limits. > **Warning:** Keys belong on your server. Never put a site secret key or an access token in browser code, a mobile app, a public repository or a URL. ## Details - Responses for people (/stats/visitors, /stats/journeys, /visitors/…, realtime, settings and segments) are sent with `Cache-Control: private, no-store`. - The dashboard calls the same endpoints with a session cookie. Cookie writes are accepted only from metricraven.com pages, so scripts should always use a key. - Demo websites accept reads only; ingestion endpoints return 401 for them. - Repeated payments with the same transaction ID are ignored, so a payment can be retried safely. Events and crawler requests are not deduplicated: retry them only when the request failed. --- # Authentication and access tokens > Choose between a site secret key and an account access token, and give each token only the access it needs. Source: https://metricraven.com/docs/api-authentication Markdown: https://metricraven.com/docs/api-authentication.md Section: API reference Every API, MCP and CLI request sends a key as `Authorization: Bearer …`. There are two kinds of key. | Key | Use it for | | --- | --- | | Site secret key (ana_sec_…) | Your server, for one website: sending events, payments and identify calls, reading that website's reports and deleting a person's data. Shown once when you add the website. | | Account access token (mr_…) | The CLI, AI assistants (MCP) and scripts that work across your websites. Acts as you, limited to the scopes and websites you choose. Create it in Settings → API & MCP. | ## Create an access token 1. **Open Settings → API & MCP** In the dashboard, click **API** in the top bar, or go to [metricraven.com/app/api](https://metricraven.com/app/api). 2. **Name it and choose its access** Use one token per tool, so you can delete one without breaking the others. 3. **Copy the token** It is shown once. Store it in your password manager or an environment variable such as `METRICRAVEN_TOKEN`. ## Scopes | Scope | Allows | | --- | --- | | read (always on) | All reports: traffic, breakdowns, revenue, goals, funnels, insights, Search Console, AI crawlers, realtime and chart notes. Customer names and emails are removed. | | visitors | Visitor search, journeys and profiles, names, emails and visitor IDs inside reports, saved segments and site settings (GET /segments and GET /site). | | write | Add websites and chart notes, and change settings, funnels and segments through the API. Tokens can never delete a website or a person's data. | A token can be limited to chosen websites. Only tokens for all websites can create new ones. If you lose access to a website, every token you created loses it too. ## Tokens and team roles A token acts as you, so on each website it can never do more than your [role](https://metricraven.com/docs/team-members#roles) allows. Both the scope and the role must allow a request: | Your role on the website | What your token can do there | | --- | --- | | Owner or admin | Everything its scopes allow. | | Viewer | Read reports, and visitor details with the visitors scope. Writes fail with 401, even with the write scope. | Roles are checked on every request, so a role change or removal applies to your existing tokens at once. `GET /api/v1/websites` includes your `role` on each website. ## Use a token Report endpoints need the website, passed as `site`. Find website IDs with [GET /api/v1/websites](https://metricraven.com/docs/account-api). ```bash curl 'https://metricraven.com/api/v1/stats/traffic?site=YOUR_SITE_ID&range=all' \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" ``` ## Errors | Status | Meaning | | --- | --- | | 401 unauthorized | Missing, deleted, replaced or mistyped key, a website the token can't use, a scope it doesn't have, or a write your role on the website doesn't allow. | | 402 | Your plan's limit was reached: website_limit, member_limit, or tracking_paused when the monthly event allowance is used up. | | 403 | insufficient_scope or forbidden when creating a website without the write scope or with a token limited to some websites; forbidden for an account token on DELETE /people; account_token_required when team endpoints get a site secret key. | | 429 | Too many requests, for example over 30 invitations an hour. Wait and retry; see rate limits in the API overview. | Which key each endpoint accepts, and the error format, are listed in the [API overview](https://metricraven.com/docs/api-overview#access). > **Warning:** Never put a site secret key or an access token in browser code, a public repository or a URL. ## Details - Only a hash of each key is stored. A lost token can't be shown again: delete it and create a new one. - Site secret keys can be rotated from Settings → Secret keys, keeping the old key working for up to 7 days, and a website can have named keys that are revoked one by one. [Rotate your site key](https://metricraven.com/docs/team-members#rotate-key). - Tokens don't expire. The list in Settings shows when each one was last used, so you can delete ones you no longer need. - Event, payment and identify ingestion always uses the site secret key, not account tokens. --- # Reports API > Every report endpoint with its parameters and response fields: traffic, breakdowns, conversions, goals, insights, revenue, subscriptions, search, crawlers, X links, mentions, realtime and visitors. Source: https://metricraven.com/docs/reports-api Markdown: https://metricraven.com/docs/reports-api.md Section: API reference Read every dashboard report from your backend with the site secret key, or with an [account access token](https://metricraven.com/docs/api-authentication) plus `site=WEBSITE_ID`. Paths are relative to `https://metricraven.com/api/v1`; keys, errors and paging are described in the [API overview](https://metricraven.com/docs/api-overview). ## Make your first request 1. **Set METRICRAVEN_SECRET_KEY on your server** Never call these endpoints from browser code. 2. **Request a report** Replace the dates with your window as UTC timestamps ending in `Z`. Or use `range=all` without dates for all retained history. ```bash curl --get 'https://metricraven.com/api/v1/stats/traffic' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" \ --data-urlencode 'from=2026-10-01T00:00:00.000Z' \ --data-urlencode 'to=2026-10-30T23:59:59.000Z' \ --data-urlencode 'granularity=day' \ --data-urlencode 'currency=USD' ``` ## Common parameters Every endpoint that takes a period accepts these query parameters. | Parameter | Meaning | | --- | --- | | from, to | UTC ISO 8601 timestamps with a Z suffix (offsets and plain dates are rejected); from is inclusive, to exclusive, at most 36,600 days apart. | | range=all | All retained history instead of from/to; the response adds period.allTime and period.coverage. | | granularity | auto, hour, day (default), week or month for series. Hourly becomes daily over 3 days, daily weekly over 400 days, weekly monthly over 4,000 days. | | currency | Optional ISO 4217 code for revenue. Defaults to the site's reporting currency (its setting, else its most common payment currency, else USD), returned in period.currency. Payments in other currencies are converted at the ECB reference rate of each payment's UTC day; period.fx lists the converted currencies and, in excluded, those with no rate that are left out of totals. | | timezone | IANA zone (default UTC) for calendar grouping, returned in period.timeZone. | | filters | Up to six visitor filters as a JSON array (see Filters). | | compareStart | Optional ISO timestamp with a Z suffix, before from and at most ten years earlier: every previous-period figure (overview.previous and change, change columns, subscription events) compares with the window of the same length starting there, returned in period.comparison. Omitted: the window right before the range. Invalid values return 400 invalid_range. | | fresh=1 | Recompute instead of reading the 60-second report cache. | | site | Website ID; required with an account token. | Most reports start with a `period` object: `from`, `to`, `timeZone`, `granularity`, and, where relevant, `currency` and the normalized `filters`. With `range=all` it also has `allTime: true` and `coverage`, the first recorded `events`, `payments` and `crawls` timestamps (or null). ## Endpoints | Path | Returns | Access | | --- | --- | --- | | /stats/traffic | Totals with previous-period comparison, series, every breakdown, heatmap and spikes. | read | | /stats/breakdown | One complete, searchable, paginated breakdown. | read | | /stats/filter-values | Searchable values for a filter dimension. | read | | /stats/conversions | Goals, a funnel and recent customers. | read | | /stats/goal | One goal: totals, series, properties and sources. | read | | /stats/insights | Buyer rates, cohort rankings, purchase timing and heatmap. | read | | /stats/revenue | Payments, renewals, refunds, plans, channels and lifetime value in period.currency. Recent payments add originalAmount, originalCurrency, fxRate and fxDate. | read | | /stats/revenue-segment | One plan or country of the revenue breakdown: composition over time, share, customers and payments. | read | | /stats/currencies | The reporting currency and its source (setting or payments), currencies with an ECB rate, currencies the site was paid in with counts, and latestRateDate. No date range. | read | | /stats/subscriptions | Live Stripe subscriptions and MRR by plan. | read | | /stats/search | Search Console queries and pages. | read | | /stats/meta-ads | Meta Ads spend next to attributed revenue, ROAS and cost per customer. | read | | /stats/crawlers | Bot and AI crawler requests. | read | | /stats/crawler-pages | Every crawled page matching a path search or wildcard, with totals. | read | | /stats/x-referrers | Visits and revenue from X by t.co link. | read | | /stats/mentions | Stored X and Reddit mentions. | read | | /stats/ai-visibility | What AI assistants answer to your tracked prompts. | read | | /stats/overview | A compact summary for a window. | read | | /stats/live | Visitors online now and recent activity. | live | | /stats/live/replay | The last hours in 15-minute windows. | live | | /stats/live/value | Revenue per visitor over 30 days. | live | | /stats/visitors | Search visitors in a period. | private | | /stats/journeys | People who paid or completed a goal. | private | **read** works with a secret key, a token with the read scope, or a share link. **live** excludes share links. **private** needs the secret key or a token with the visitors scope. ## Traffic `GET /stats/traffic` returns everything on the main dashboard in one response. Accepts filters. ```bash curl --get 'https://metricraven.com/api/v1/stats/traffic' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" \ --data-urlencode 'range=all' \ --data-urlencode 'granularity=month' \ --data-urlencode 'timezone=Europe/Madrid' ``` | Field | Meaning | | --- | --- | | capabilities | visitorExplorer and sessions: whether visitor search and session metrics are available for this website. | | overview.current, overview.previous | visitors, pageviews, sessions, bounceRate (session-based; visitor-based without sessions), visitorBounceRate, avgSessionDuration and avgTimeOnSite (seconds), pagesPerSession, payments, payingCustomers, payingVisitors, revenue, conversionRate (paying visitors ÷ visitors) and revenuePerVisitor. current also has visitorsNow. | | overview.change | Relative change for each metric in percent, or null when the previous value is 0. | | primaryGoal | null, or the website's primary goal: name, current and previous (conversions, visitors, rate) and change (conversions, rate). | | series[] | One row per bucket: date, visitors, pageviews, newVisitors, returningVisitors, bouncedVisitors, sessions, bounceRate, sessionDuration, engagementSeconds, avgTimeOnSite, payingVisitors, payingCustomers, payments, revenue, renewalRevenue, conversionRate and revenuePerVisitor. Rates are null in buckets without visitors. | | channels, referrers, utmSources, utmMediums, utmCampaigns, utmTerms, utmContents, refTags, linkRefs, linkSources, linkVias, adNetworks, visitTypes, pages, entryPages, hostnames, outboundLinks, browsers, os, devices, countries, regions, cities | Top 50 rows each (countries are complete) with name, visitors and pageviews; most add revenue and payments credited to the visitor's first touch. pages adds avgTime in seconds. | | heatmap[] | Visitors by day (1 = Monday … 7 = Sunday) and hour (0–23) in the report time zone; empty cells are left out. | | spikes[] | Unusual traffic days (see below). | ```json title="Response (abridged)" { "capabilities": { "visitorExplorer": true, "sessions": true }, "primaryGoal": null, "period": { "timeZone": "UTC", "from": "2026-10-01T00:00:00.000Z", "to": "2026-11-01T00:00:00.000Z", "granularity": "day", "currency": "USD", "filters": [] }, "overview": { "current": { "visitors": 1840, "pageviews": 5212, "sessions": 2390, "bounceRate": 41.2, "visitorBounceRate": 38.55, "avgSessionDuration": 142, "pagesPerSession": 2.18, "avgTimeOnSite": 96, "payments": 31, "payingCustomers": 29, "payingVisitors": 27, "revenue": 899, "conversionRate": 1.47, "revenuePerVisitor": 0.49, "visitorsNow": 4 }, "previous": { "visitors": 1510, "revenue": 720 }, "change": { "visitors": 21.85, "revenue": 24.86 } }, "series": [ { "date": "2026-10-01", "visitors": 61, "pageviews": 170, "revenue": 29, "conversionRate": 1.64 } ], "channels": [{ "name": "Organic Search", "visitors": 820, "pageviews": 2400, "revenue": 420, "payments": 14 }], "pages": [{ "name": "/pricing", "visitors": 610, "pageviews": 702, "avgTime": 48 }], "heatmap": [{ "day": 1, "hour": 9, "visitors": 37 }], "spikes": [] } ``` ### Traffic spikes Daily `/stats/traffic` responses include `spikes`: days where one source sent far more visitors than usual. Each item has `date`, `source` (X, Google, Reddit, a referrer host or utm_source), `domain`, `visitors`, `usual` (median of the previous 14 days) and `ratio`. A spike needs at least 10 visitors, 2.5× usual and 15% of the day's visitors. Other granularities return an empty list. ## Breakdown `GET /stats/breakdown` returns one complete table, for example every referrer or every page. Accepts filters. | Parameter | Required | Description | | --- | --- | --- | | dimension | Yes | One of the dimensions below. | | q | No | Case-insensitive search in the row name, up to 500 characters. | | sort | No | visitors (default), pageviews or revenue. | | offset, limit | No | Rows to skip (default 0) and page size 1–100 (default 50). | ```bash curl --get 'https://metricraven.com/api/v1/stats/breakdown' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" \ --data-urlencode 'range=all' \ --data-urlencode 'dimension=referrer' \ --data-urlencode 'sort=revenue' \ --data-urlencode 'limit=2' ``` ```json title="Response" { "dimension": "referrer", "rows": [ { "name": "news.ycombinator.com", "visitors": 410, "pageviews": 530, "revenue": 290 }, { "name": "google.com", "visitors": 820, "pageviews": 2400, "revenue": 210 } ], "total": 64, "offset": 0, "nextOffset": 2 } ``` ### Dimensions | dimension | Rows | | --- | --- | | channel | Marketing channel of the visitor's first pageview in the period: Paid, Email, Social, Organic Search, Referral, Campaign or Direct. | | referrer | Referring host, for example news.ycombinator.com. | | utmSource, utmMedium, utmCampaign, utmTerm, utmContent | Each UTM tag on its own. | | campaignTuple | Complete UTM combinations: the row name is a JSON array of source, medium, campaign, term and content, with null for absent tags. | | refTag | Primary referral tag from ?ref=, ?via= or ?source=. | | linkRef, linkSource, linkVia | The ref, source and via link parameters separately. | | adNetwork | Ad network whose click ID was on the landing URL: google, meta, microsoft, linkedin, tiktok or twitter. Breakdown only. | | page | Page path. | | entryPage | Path of the visitor's first pageview in the period. | | hostname | Recorded page hostname, useful with several domains or subdomains. | | outbound | Outbound link URLs clicked; pageviews counts clicks. Breakdown only, and can't be sorted by revenue. | | visitType | new (first pageview ever inside the period) or returning. | | visits | Visits in the period, one whole number with op gt (more than), lt (fewer than), is or is_not. A visit ends after 30 minutes without a pageview. | | country | ISO 3166-1 alpha-2 code, for example US. | | region, city | Region and city names. | | browser, os, device | Browser, operating system, and device type (desktop, mobile or tablet). | Invalid parameters return 400 `invalid_breakdown_query`. Goals have their own reports: `/stats/conversions` and `/stats/goal`. ## Filters Encode a JSON array as the `filters` query parameter. Each filter has a `dim`, an `op` and up to 20 `values`; a visitor must match every filter, and any of a filter's values. ```json title="Example filters parameter" [ { "dim": "country", "op": "is", "values": ["US", "GB"] }, { "dim": "page", "op": "not_contains", "values": ["/admin"] }, { "dim": "goal", "op": "is", "values": ["signup"] } ] ``` ```bash title="Filtered request" curl --get 'https://metricraven.com/api/v1/stats/traffic' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" \ --data-urlencode 'range=all' \ --data-urlencode 'filters=[{"dim":"channel","op":"is","values":["Organic Search"]}]' ``` | Part | Values | | --- | --- | | dim | channel, utmSource, utmMedium, utmCampaign, utmTerm, utmContent, campaignTuple, refTag, linkRef, linkSource, linkVia, goal, visitType, referrer, page, entryPage, hostname, country, region, city, browser, os, device, visits; payment dimensions plan, currency, paymentType, refund and provider. | | op | is (default), is_not, contains, not_contains. contains matches literal text, case-insensitively. visits takes gt, lt, is or is_not with one whole number. | | values | 1–20 non-empty strings of up to 500 characters (6,500 for campaignTuple). | - `goal` keeps visitors who completed that goal in the period; `visitType` takes `new` or `returning`; `channel` and `entryPage` use the visitor's first pageview in the period. - `campaignTuple` takes a campaign row's exact name and only `is` or `is_not`. - `visits` counts each visitor's visits in the period (a visit ends after 30 minutes without a pageview): `{"dim":"visits","op":"gt","values":["2"]}` keeps visitors who came back at least twice. - Negative operators remove whole visitors: a visitor with any pageview matching `is_not` is excluded. - Payment dimensions: `plan` (`Unknown` for payments without one), `currency` (the three-letter currency paid in), `paymentType` (`new` or `renewal`), `refund` (`none`, `partial` or `refunded`) and `provider`. currency, paymentType and refund take only `is` or `is_not`. `/stats/revenue` and `/stats/revenue-segment` test each payment; other reports keep the visitors who made a matching payment in the period. - An invalid filters parameter returns 400 invalid_range rather than broadening the report. ## Filter values `GET /stats/filter-values` searches the values of one filter dimension, with visitor counts, for building filter pickers. Parameters: `dimension` (any filter dimension, including `goal`), `q`, `offset` (up to 100,000) and `limit` (1–100, default 50). Filters on other dimensions apply; filters on the same dimension are ignored. Country search also matches country names. ```json title="Response" { "dimension": "country", "rows": [ { "value": "US", "visitors": 640 }, { "value": "DE", "visitors": 212 } ], "total": 58, "offset": 0, "nextOffset": 2 } ``` ## Conversions and funnels `GET /stats/conversions` returns goal totals, a funnel and recent customers. Accepts filters. | Parameter | Description | | --- | --- | | steps | Comma-separated funnel of 1–8 steps: visit, page:/path, event:goal_name, identify and payment. Default: visit,page:/pricing,identify,event:checkout_started,payment. | | funnel | ID of a saved funnel instead of steps (see Funnels API). Unknown IDs return 404 unknown_funnel. | ```bash curl --get 'https://metricraven.com/api/v1/stats/conversions' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" \ --data-urlencode 'range=all' \ --data-urlencode 'steps=visit,page:/pricing,event:signup,payment' ``` | Field | Meaning | | --- | --- | | totalVisitors | Visitors in the period. | | goals[] | Every goal: name, events, visitors who completed it and rate (% of visitors). | | goalSeries[] | date and goals, a map of goal name to events for the eight most-completed goals. | | funnel[] | One row per step: step (label), key, visitors who reached it in order, fromStart and fromPrevious (%), revenuePerVisitor, and the top topSources and topCountries (name, visitors). | | customers[] | Up to 100 recent payments: transactionId, createdAt, amount, currency, plan, email, name, visitorId, source, channel, landingPage, firstSeenAt and hoursToConvert. | ```json title="funnel item" { "step": "Paid", "key": "payment", "visitors": 27, "fromStart": 1.47, "fromPrevious": 22.5, "revenuePerVisitor": 33.3, "topSources": [{ "name": "google.com", "visitors": 12 }], "topCountries": [{ "name": "US", "visitors": 15 }] } ``` ## One goal `GET /stats/goal?name=signup` reports one goal. `name` is required: a letter followed by up to 99 letters, digits, `_ : . -`; otherwise 400 invalid_goal. Accepts filters. | Field | Meaning | | --- | --- | | totals | events, people who completed it, visitors in the period and rate (%). | | series[] | date, events and people per bucket. | | properties[] | Each custom property key with up to 10 values: { key, values: [{ value, events }] }. | | sources[] | Where people who completed it came from: name and visitors. | | recent[] | The latest 25 completions, newest first: person (an opaque per-person key), visitorId, email, name, image, completedAt and up to six props. Share links get null visitorId, email, name and image. | ## Buyer insights `GET /stats/insights` powers [Buyer insights](https://metricraven.com/docs/buyer-insights). Accepts filters. | Field | Meaning | | --- | --- | | overview | visitors, payingVisitors, purchaseRate, customers, payments, revenue, averageDailyVisitors and calendarDays. | | rankings | sources and countries with at least minimumVisitors visitors (name, visitors, payingVisitors, purchaseRate), plus sourceGroups and countryGroups counts. | | timing | First-observed purchase timing: firstObservedCustomers, withPageviews, withoutPageviews, medianDays, medianHours, medianVisits, firstVisitPercent, and days, visits, goals, dayHistogram and visitHistogram buckets (label, customers). | | revenueMix | sources, countries, devices, os and browsers with name, revenue, payments and customers. | | purchaseHeatmap[] | All 168 day (1 = Monday) and hour cells with customers, payments and revenue, in the report time zone. | | sourceChanges[] | name, current and previous visitors, change and percent (null without a previous value). | ## Revenue `GET /stats/revenue` reports payments in one `currency`. Refunds reduce revenue and free trials are excluded. Accepts filters: visit dimensions keep the payments (and refunds) of visitors who match them in the period, payment dimensions test each payment, and subscription events follow visit, plan and provider filters. `unfiltered` lists the sections the active filters could not narrow: `ltv` (lifetime value and top customers cover every payment ever made) and `lifecycle` (with currency, paymentType or refund filters). ```bash title="Renewal revenue from US visitors" curl --get 'https://metricraven.com/api/v1/stats/revenue' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" \ --data-urlencode 'range=30d' \ --data-urlencode 'filters=[{"dim":"country","op":"is","values":["US"]},{"dim":"paymentType","op":"is","values":["renewal"]}]' ``` | Field | Meaning | | --- | --- | | overview.current, previous, change | revenue, newRevenue, renewalRevenue, newPayments, renewals and payingCustomers; current also has refunds and refundedAmount. | | series[] | date, newRevenue, renewalRevenue, newPayments, renewals, payingCustomers, refunds and refundCount. | | recentPayments[] | customer (an opaque key), email, name, visitorId, plan, amount, createdAt, source, channel and country. | | byPlan, byChannel, bySource, byCountry | name, revenue, newPayments and renewals. Channel, source and country are each payer's first tracked visit; byCountry uses ISO codes and "Unknown". | | lifecycle | Subscription goals recorded from payment providers in the period: enabled (the site's Subscription goals switch) and goals[] with name (trial_started … payment_refunded, every goal listed, zeros included), events, customers, previous and change (% vs the previous period, null when it had none). | | ltv | Lifetime value: customers, average, median, total, averageRevenuePerMonth, byPlan (plan, customers, average, median, averageRevenuePerMonth, averageMonths) and topCustomers (customer, email, name, visitorId, plan, revenue, payments, firstPaymentAt, lastPaymentAt). | Share links get null email, name and visitorId; the public demo keeps visitorId so its synthetic customers open their journey. ## One plan or country `GET /stats/revenue-segment?dimension=plan&value=Pro` reports one row of the revenue breakdown. `dimension` is `plan` or `country` (the country of each payer's first tracked visit); `value` is the plan name, a two-letter country code, or `Unknown`. Anything else returns 400 invalid_segment. Accepts filters, applied as in `/stats/revenue`. | Field | Meaning | | --- | --- | | totals | The revenue overview fields for this segment, plus share (of the period's revenue, 0–1) and periodRevenue. | | previous, change | The same figures for the previous period and the % change. | | series[] | The revenue series for this segment. Refunds count when issued; by country, by the refunded visitor's first visit. | | customers[], customerCount | Top 50 customers by revenue in the period: customer, email, name, image, visitorId, plan, country, source, channel, revenue, payments, renewals, firstPaymentAt and lastPaymentAt. | | recentPayments[] | The latest 25 payments, as in /stats/revenue. | | breakdown[] | Countries of a plan, or plans of a country: name, revenue and payments. | ## Subscriptions `GET /stats/subscriptions?currency=USD` returns live Stripe subscription counts. It takes no period and is never cached. Invalid currencies return 400 invalid_currency. ```json title="Response" { "available": true, "currency": "USD", "asOf": "2026-11-02T08:00:00.000Z", "stale": false, "error": null, "byPlan": [ { "plan": "Pro", "active": 120, "trialing": 8, "pastDue": 2, "canceled": 14, "cancelling": 5, "unknownPricing": 0, "mrr": 3480 } ], "totals": { "active": 120, "trialing": 8, "pastDue": 2, "canceled": 14, "cancelling": 5, "unknownPricing": 0, "mrr": 3480, "arr": 41760 } } ``` Without Stripe, or during the first sync, the response is `{ "available": false, "currency", "reason", "syncing" }`. ## Search Console `GET /stats/search` returns Google Search Console data for the linked property. Add `path` to keep pages whose URL contains it. | Field | Meaning | | --- | --- | | configured | false with a reason (no_credentials, no_property, no_access or error) when no data can be read. | | property, period | The property, and the Search Console window used: from, to, timeZone (always America/Los_Angeles) and limited. | | totals | clicks, impressions, ctr and position. | | queries[] | Up to 250: query, clicks, impressions, ctr, position and estimated revenue. | | pages[] | Up to 250: path, url, clicks, impressions, ctr, position and revenue from organic-search first visits. | | rangeUnavailable | true when the window is outside the data Search Console keeps. | ## Ad spend (Meta Ads, Google Ads) `GET /stats/ads` returns ad spend for the connected ad accounts next to the ad visitors, customers, first-touch and assisted revenue matched to each row. `provider` picks a network (`meta`, `google`) or `all` (default): rows of every connected network, each with its `provider`, and combined totals (customers and assisted payments counted once). Add `level` (`campaign`, `adgroup` or `ad`; `adset` is accepted for adgroup; default campaign). Filters don't apply. An unknown provider returns 400 invalid_provider. See [Meta Ads](https://metricraven.com/docs/meta-ads) and [Google Ads](https://metricraven.com/docs/google-ads) for matching rules. `GET /stats/meta-ads` is an alias of `/stats/ads?provider=meta` with the same response shape (its `level` takes `campaign`, `adset` or `ad`). Add `campaign` (a campaign ID) for `campaign`: that campaign's totals, a `series` of every day with spend, impressions, linkClicks, revenue, payments and customers (revenue by the payment's day in the ad account's timezone), and its top 50 `customers` (customer, email, name, image, visitorId, plan, revenue, payments, firstPaymentAt, lastPaymentAt, adsetId (the ad group) and adId; people are redacted on share links). It is null for a campaign that was never synced; an invalid ID returns 400 invalid_campaign. | Field | Meaning | | --- | --- | | configured | false with a reason (not_connected or no_account) when there is nothing to report. | | provider, connected | The network asked for (or all) and every network with an ad account chosen. | | accounts[] | Per network: provider, id, name, currency, timeZone, days, sync, totals (with spendAccountCurrency), unattributed and unconvertedSpend. | | account | id, name, currency and timeZone of the (first) ad account. sample is true on the demo. | | period | from, to, currency, and days: the inclusive account days (ad account timezone) that were summed. A live rolling period has as many days as the dashboard period, today included (Last 7 days is 7 days). | | sync | status, lastSyncedAt, syncedThrough and error of the (first) account's daily sync. | | totals | spend, spendAccountCurrency (null when combined accounts use different currencies), impressions, clicks, linkClicks, visitors (ad visitors), customers, payments, revenue, assistedRevenue, roas, costPerCustomer and costPerClick. Campaign rows only: unattributed clicks are never included. | | rows[] | id, name, provider (campaignName and adGroupName for ad groups and ads; adsetName is the same, for Meta clients) and the same metrics, by spend. | | unattributed | visitors, customers, payments and revenue of visits with only a click ID (fbclid, gclid, gbraid, wbraid) or the network's UTM tags that match no synced campaign, with label and includedInTotals: false. They can include organic shares, so they are not in totals, ROAS or cost per customer. other repeats the numbers (deprecated). | | assistedRevenue | Per row: revenue in the period from customers who clicked the row before paying but whose first touch was elsewhere. Not in revenue or ROAS; rows don't add up to the total, which counts payments whose first touch was no campaign at all. | | unconvertedSpend | Account-currency spend left out because no exchange rate exists for its day yet (single network; see accounts[]). | ## Crawlers `GET /stats/crawlers` reports bot requests sent with [POST /crawls](https://metricraven.com/docs/api-overview#crawls). | Field | Meaning | | --- | --- | | total | Requests in the period. | | verification | verified (IP inside the crawler operator's published ranges) and unverified (outside them). Other requests had no IP or a crawler with no published ranges. | | byType[] | name (ai-training, ai-search, ai-assistant, search-engine, seo, social-preview, other) and requests. | | byBot[] | bot, provider, type, requests, verified, unverified, pages and lastSeenAt. | | pages[] | Top 50 paths: name, requests and bots (distinct crawlers). | | files[] | robots.txt, llms.txt, llms-full.txt, sitemap and markdown: name, requests and bots. | | statuses[] | status and requests. | | series[], botSeries[] | Per bucket: requests by type, and bots, a map of crawler to requests. | ### Search crawled pages `pages[]` above is the top 50. `GET /stats/crawler-pages` searches every crawled page in the period: `q` (required, up to 200 characters) is plain text that the path must contain, or a wildcard pattern over the whole path such as `/blog/*` or `*.pdf`. Matching ignores case, and robots.txt, llms.txt and the other files are left out. It takes the usual date parameters; a missing or too long `q` returns 400 invalid_search. ```bash curl --get 'https://metricraven.com/api/v1/stats/crawler-pages' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" \ --data-urlencode 'from=2026-10-03T00:00:00Z' \ --data-urlencode 'to=2026-11-02T00:00:00Z' \ --data-urlencode 'q=/blog/*' ``` ```json title="Response" { "period": { "timeZone": "UTC", "from": "2026-10-03T00:00:00.000Z", "to": "2026-11-02T00:00:00.000Z" }, "search": "/blog/*", "pages": [ { "name": "/blog/launch", "requests": 214, "bots": 6 }, { "name": "/blog/pricing-teardown", "requests": 87, "bots": 4 } ], "matched": { "requests": 301, "pages": 2 } } ``` `pages[]` lists up to 100 matching paths, most requested first, with `bots` as the number of distinct crawlers. `matched` counts every match, including pages past the first 100. ## X links `GET /stats/x-referrers` groups visits from X by the t.co link they arrived through. Accepts filters. ```json title="Response" { "locked": false, "rows": [ { "name": "https://t.co/AbC123xYz", "visitors": 84, "revenue": 58, "post": { "id": "1850000000000000000", "platform": "x", "url": "https://x.com/ana/status/1850000000000000000", "text": "We just launched…" }, "profile": null }, { "name": "t.co", "visitors": 31, "revenue": 0, "post": null, "profile": null } ] } ``` Up to 200 rows. `post` is the X post containing the link and `profile` the account whose bio links to it (username, name, image). Visits without a link code stay in one `t.co` or `x.com` row. Without the Growth plan `locked` is true and links aren't resolved. See [X link attribution](https://metricraven.com/docs/x-link-attribution). ## Social mentions `GET /stats/mentions` lists stored X and Reddit posts that mention the website's domain or tracked keywords (see [X and Reddit mentions](https://metricraven.com/docs/social-mentions)). It reads stored mentions only and never starts a search. | Parameter | Meaning | | --- | --- | | from, to | ISO timestamps (to is exclusive) or inclusive YYYY-MM-DD dates in timezone. Default: the last 30 days. | | range | A preset such as 7d, 30d, 90d, or all. Ignored when from and to are set. | | timezone | IANA zone for date-only bounds. Default UTC. | | q | Case-insensitive search in text, title, author name and handle, URL and subreddit (max 200 characters). | | platform | x or reddit; omit for both. | | limit, offset | Page size 1–1000 (default 100) and rows to skip. | | order | desc (newest first, default) or asc. | Each item in `data` has `id`, `platform`, `kind` (post or comment), `timestamp`, `text`, `title` (Reddit), `url`, `author` (name, username, image), `engagement` (X: likes, reposts, replies, quotes, views; Reddit: score, comments), `subreddit`, `matched` (the terms found) and `links` (t.co links to your site). `pagination` has limit, offset, total and hasMore; `meta` has the resolved from, to and timezone, `totalMentions` before search and platform filters, and `locked`. Without the Growth plan, `locked` is true, `data` is empty and `meta.message` and `meta.upgradeUrl` explain how to upgrade. Unknown or repeated parameters return HTTP 400. ```bash curl "https://metricraven.com/api/v1/stats/mentions?range=30d&platform=x&limit=20" \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" ``` ## AI visibility `GET /stats/ai-visibility` returns the results of the weekly checks that ask your tracked prompts to AI assistants (see [AI visibility](https://metricraven.com/docs/ai-visibility)). It reads stored checks only and never starts one. | Parameter | Meaning | | --- | --- | | from, to | ISO timestamps (to is exclusive) or inclusive YYYY-MM-DD dates in timezone, for the trend. Default: the last 90 days. | | range | A preset such as 30d, 90d, 6m, or all. Ignored when from and to are set. | | timezone | IANA zone for date-only bounds. Default UTC. | | prompt | A prompt id: adds detail with that prompt's latest answers, one per assistant. | | competitor, domain, page, model | At most one: a brand name, a cited domain, a cited page URL or an assistant id (chatgpt, claude, perplexity, gemini, grok, google_ai_mode, google_ai_overviews). Adds focus with that subject in detail. | | compareStart | ISO timestamp before the range start (at most ten years earlier): previousSummary becomes the latest check in [compareStart, compareStart + range length), and meta.comparison gives that window and its runId (null when no check fell in it). Omitted: the check before the latest. | | filters | A JSON array of up to six { dim, op, values } (op is or is_not, one filter per dim): model (assistant ids), prompt (prompt ids), group (prompt topics), competitor (answers naming that brand), mention (mentioned, not_mentioned or cited) and citedDomain (answers citing that domain). Every figure, the trend, models, leaderboard, prompts, citations, focus and detail are recomputed from the matching answers; meta.filters echoes them. Invalid filters return 400 invalid_request. | `focus` has `kind`, `key`, `label`, `domain`, `history` (each check in the period: runId, date, answers, hits, you and avgPosition, where hits are answers naming the brand, citing the domain or page, or for an assistant naming you) and `answers` (the latest check's answers involving it: promptId, modelId, status, present, position, cited, sentiment, subjectPosition, brands, sources, and for an assistant an excerpt of the stored answer). A brand or assistant lists every latest answer; a domain or page only those citing it. Two focus parameters return 400. `latestRun.summary` and `previousSummary` have `visibilityScore` (0–100), `presenceRate`, `avgPosition`, `topPositionRate`, `citedRate`, `consistency` (rates 0–1), `responses`, `failed` and, when Google showed no AI Overview for some searches, `noAnswer`. An answer's `status` is `ok`, `error` or `no_answer` (Google AI Overviews only: no AI answer was shown, which is left out of the scores rather than counted as not mentioned). `trend` lists each check in the period with its summary and `byModel` scores. `models` has per-assistant summaries, `competitors` every brand named ranked by visibility (yours has `isYou`), `prompts` each prompt with per-assistant `results` (present, position, cited) and `citations` the most cited domains and pages. With a secret key, member session or token, `credits` (used, allowance, resetsAt) and `status` (lastRunAt, nextRunAt, running) are filled in; share links get null. Without the Growth plan, `meta.locked` is true and the lists are empty. ```bash curl "https://metricraven.com/api/v1/stats/ai-visibility?range=90d" \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" ``` ## Compact overview `GET /stats/overview` is a small summary for scripts. It needs `from` and `to` (UTC, ending in Z) and ignores range, timezone, currency and filters. ```bash curl --get 'https://metricraven.com/api/v1/stats/overview' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" \ --data-urlencode 'from=2026-10-01T00:00:00Z' \ --data-urlencode 'to=2026-11-01T00:00:00Z' ``` ```json title="Response" { "from": "2026-10-01T00:00:00.000Z", "to": "2026-11-01T00:00:00.000Z", "visitors": 1840, "pageviews": 5212, "events": [{ "name": "signup", "count": 96 }], "payments": [{ "currency": "USD", "count": 31, "amount": 899 }], "revenueBySource": [{ "source": "google.com", "currency": "USD", "count": 12, "amount": 348 }] } ``` `events` lists the 20 most frequent goals. `payments` and `revenueBySource` group by currency and leave out fully refunded payments and free trials. `revenueBySource` credits each payment to the UTM source or referrer of the buyer's first pageview, Direct without either, or Unattributed without a visit. ## Realtime `GET /stats/live` takes no parameters besides `site` and is never cached; the dashboard polls it every few seconds. | Field | Meaning | | --- | --- | | visitorsNow | Visitors with a pageview or browser event in the last 5 minutes. | | visitorLimit, visitorsTruncated | At most 500 visitors are listed; visitorsTruncated is true when more are online. | | series[] | The last 30 minutes: minute (UTC, YYYY-MM-DDTHH:mm) and visitors. | | visitors[] | Newest first: visitorId, name, email, path, lastSeenAt, browser, os, deviceType, country, city, latitude, longitude (approximate, from the IP; null when unknown), source and channel of their first pageview in the last 24 hours. | | recent[] | Last 30 pageviews and events in 30 minutes: type, name, path, visitorId, createdAt, country and deviceType. | | recentPayments[] | Last 20 payments in 30 minutes: transactionId, visitorId, createdAt, amount (net of refunds), currency and renewal. | | recentGoals[] | Last 20 goals in 30 minutes: name, visitorId and createdAt. | ```json title="visitors item" { "visitorId": "0123456789abcdef0123456789abcdef", "name": null, "email": null, "path": "/pricing", "lastSeenAt": "2026-11-02T08:21:40.112Z", "browser": "Safari", "os": "iOS", "deviceType": "mobile", "country": "ES", "city": "Madrid", "latitude": 40.4, "longitude": -3.7, "source": "google.com", "channel": "Organic Search" } ``` Without the visitors scope, name and email are null. ### Realtime replay `GET /stats/live/replay?hours=8` returns the last 1–24 `hours` (default 8; otherwise 400 invalid_hours) as 15-minute windows aligned to UTC quarter hours. The response has `windowMinutes`, `hours`, `from`, `to`, `generatedAt` and `windows`. Each window has `start`, `end`, `visitors`, `points` (up to 300 locations rounded to 0.1°: latitude, longitude, country, visitors), `pointsTruncated` and up to eight `countries`, `devices` and `sources` (name, visitors). It never contains visitor IDs or paths. ### Value of visitors online `GET /stats/live/value?currency=USD` returns revenue per visitor over the last 30 days: `{ "currency": "USD", "days": 30, "revenue": 899, "visitors": 1840, "revenuePerVisitor": 0.4886 }`. `revenuePerVisitor` is unrounded, and null without revenue or visitors. Cached for an hour. ## Visitor search `GET /stats/visitors` lists the visitors with a pageview in the period, as in the [visitor explorer](https://metricraven.com/docs/visitor-explorer). It needs the secret key or a token with the visitors scope, and is never cached. | Parameter | Description | | --- | --- | | q | Case-insensitive search in visitor ID, name, email and user ID, up to 500 characters. | | payment | all (default), paid or unpaid. | | sort | latest (default, last pageview), earliest (first pageview) or pageviews. | | offset, limit | Paging; limit 1–100, default 50. | | visitedPage, completedGoal, referrer, utm_source, country, device, visits_gt, … | Search filters as plain parameters, joined with `filters`; the full list is in [Visitors API](https://metricraven.com/docs/visitors-api#search). | ```bash curl --get 'https://metricraven.com/api/v1/stats/visitors' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" \ --data-urlencode 'range=all' \ --data-urlencode 'payment=paid' \ --data-urlencode 'limit=1' ``` ```json title="Response" { "period": { "from": "2026-09-01T00:00:00.000Z", "to": "2026-11-02T08:23:49.635Z", "currency": "USD", "timeZone": "UTC" }, "rows": [ { "visitorId": "8204a043a56203e0040598deeea9d707", "name": "Ana", "email": "ana@example.com", "pageviews": 3, "firstPageviewAt": "2026-11-01T17:34:00.492Z", "lastPageviewAt": "2026-11-01T17:39:12.735Z", "entryPage": "/", "lastPage": "/pricing", "source": "google.com", "country": "US", "device": "desktop", "paid": true } ], "total": 27, "offset": 0, "nextOffset": 1, "generatedAt": "2026-11-02T08:23:49.689Z" } ``` Invalid parameters return 400 invalid_visitor_query. Open one visitor with [GET /visitors/VISITOR_ID](https://metricraven.com/docs/visitors-api#profile). ## Journeys `GET /stats/journeys` lists people who reached a goal in the period, with where they came from and how long it took. It needs the secret key or a token with the visitors scope. Accepts filters. | Parameter | Description | | --- | --- | | goal | payment (default) or event:GOAL_NAME. Anything else returns 400 invalid_goal. | | q | Search name, email, source, country or visitor ID, up to 100 characters. | | offset, limit | Paging; limit 1–100, default 25. | ```json title="Response" { "currency": "USD", "goal": "payment", "total": 27, "rows": [ { "visitorId": "0123456789abcdef0123456789abcdef", "name": "Ana", "email": "ana@example.com", "source": "google.com", "channel": "Organic Search", "country": "US", "firstSeenAt": "2026-10-01T00:00:00.000Z", "completedAt": "2026-10-02T02:00:00.000Z", "hoursToComplete": 26, "spent": 29 } ] } ``` Payments without a known visit have `visitorId`, `firstSeenAt` and `hoursToComplete` null and channel Unattributed. ## Saved segments Saved segments, funnels, chart notes and settings are documented in [Notes, funnels and site settings API](https://metricraven.com/docs/site-api#segments). ## Details - Long windows use coarser intervals to keep charts bounded. - Exact `from`/`to` timestamps stay instants; the timezone controls calendar grouping. Invalid zones are rejected. Non-UTC hourly keys include an offset, for example `2026-10-25T02:00+0200` and `2026-10-25T02:00+0100` are separate groups. Search Console always reports its own Pacific calendar window. - Traffic, breakdown, filter-value, insights, conversions, goal, journey and X link reports accept filters. Each filter supports up to 20 values; the full filters parameter is limited to 16,000 characters. - Existing `{"dim":"country","value":"US"}` filters still work. - A campaign row's name is the canonical JSON array of source, medium, campaign, term and content, with `null` for absent tags. Reuse that exact string as a `campaignTuple` filter value. Combination filters accept only `is` and `is_not`; matching all five tags on one pageview prevents combinations assembled from different visits. - `refTag` includes older combined records. All referral dimensions support the normal exact, negative and literal contains operators. - With a share link or a token without the visitors scope, people fields in reports (customers, recent payments, top customers) are null. --- # Notes, funnels and site settings API > Create chart notes, read GitHub commits, save funnels and segments, and read or change a website's settings through the API. Source: https://metricraven.com/docs/site-api Markdown: https://metricraven.com/docs/site-api.md Section: API reference These endpoints manage one website's configuration. Use the site secret key, or an [account token](https://metricraven.com/docs/api-authentication) with `site=WEBSITE_ID` (the examples use a token). Changes need the write scope and an admin or owner role; share links can't use them. | Request | Token scope | | --- | --- | | GET /notes, GET /commits, GET /funnels | read | | GET /segments, GET /site | visitors | | POST, PUT and DELETE on /notes, /funnels, /segments; PUT /site | write | | Every /site/keys request (admins and the owner; never the site secret key) | write | ## Chart notes Notes are short, dated annotations such as “Launched on Product Hunt”, shown on the traffic chart. They are private to the website's team. To add them in the dashboard, see [Chart notes](https://metricraven.com/docs/chart-notes). ### List notes `GET /notes` returns notes oldest first. Optional `from` and `to` are inclusive YYYY-MM-DD dates; invalid dates return 400 invalid_range. ```bash curl --request GET 'https://metricraven.com/api/v1/notes?site=WEBSITE_ID&from=2026-10-01&to=2026-10-31' \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" ``` ```json title="Response" { "notes": [ { "id": "note_c7924fbf9cef42447f69d344", "date": "2026-10-14", "text": "Launched on Product Hunt", "createdAt": "2026-10-14T09:12:03.045Z", "updatedAt": "2026-10-14T09:12:03.045Z" } ] } ``` ### Create, change and delete a note | Field | Type | Required | Description | | --- | --- | --- | --- | | date | string | Yes | Calendar date YYYY-MM-DD, from 2000-01-01 up to tomorrow, in your reporting time zone. | | text | string | Yes | 1–280 characters. Line breaks and repeated spaces become one space. | ```bash curl --request POST 'https://metricraven.com/api/v1/notes?site=WEBSITE_ID' \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" \ --header 'Content-Type: application/json' \ --data '{ "date": "2026-10-14", "text": "Launched on Product Hunt" }' ``` | Request | Result | | --- | --- | | POST /notes | 201 with the note. 400 invalid_note for a bad date or text; 409 note_limit at 1,000 notes. | | PUT /notes/NOTE_ID | The same body replaces date and text; returns the note, or 404 not_found. | | DELETE /notes/NOTE_ID | 204 No Content, or 404 not_found. | ## GitHub commits `GET /commits` lists commits from the website's [connected repository](https://metricraven.com/docs/github) in the period, newest first, up to 500. It takes the usual `from`/`to` or `range=all` and `timezone`, and optional `type` values (comma separated or repeated): feat, fix, perf, refactor, docs, style, test, build, ci, chore or revert. Unknown types are ignored. ```bash curl --request GET 'https://metricraven.com/api/v1/commits?site=WEBSITE_ID&range=all&type=feat,fix' \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" ``` ```json title="Response" { "commits": [ { "sha": "3541888878c4d303a8bc2ad61815f4e048f55a5d", "title": "feat: annual pricing", "message": "feat: annual pricing\n\nAdds a yearly toggle to /pricing.", "type": "feat", "author": { "name": "Ana", "login": "ana", "avatarUrl": "https://avatars.githubusercontent.com/u/1?v=4" }, "committedAt": "2026-10-09T06:20:11Z", "url": "https://github.com/acme/web/commit/3541888878c4d303a8bc2ad61815f4e048f55a5d", "branch": "main", "additions": 120, "deletions": 14 } ] } ``` `type` is null for messages without a conventional-commit prefix; `additions` and `deletions` are null until their statistics are imported. ## Funnels Saved funnels appear in the dashboard and can be reported with [/stats/conversions?funnel=FUNNEL_ID](https://metricraven.com/docs/reports-api#conversions). | Field | Type | Required | Description | | --- | --- | --- | --- | | name | string | Yes | 1–80 characters. | | steps | string | Yes | 1–8 comma-separated steps: visit, page:/path, event:goal_name, identify, payment. Steps count only in this order. | ```bash curl --request POST 'https://metricraven.com/api/v1/funnels?site=WEBSITE_ID' \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" \ --header 'Content-Type: application/json' \ --data '{ "name": "Pricing to paid", "steps": "visit,page:/pricing,event:signup,payment" }' ``` ```json title="Response (201)" { "id": "pricing-to-paid-3fa2b1", "name": "Pricing to paid", "steps": "visit,page:/pricing,event:signup,payment", "position": 0 } ``` | Request | Result | | --- | --- | | GET /funnels | { funnels: [...] } in display order (position). | | POST /funnels | 201 with the funnel. 400 invalid_funnel with details. | | PUT /funnels/FUNNEL_ID | Changes name, steps or both; returns the funnel, or 404 not_found. | | DELETE /funnels/FUNNEL_ID | { ok: true }, or 404 not_found. | ## Segments A segment is a named set of [report filters](https://metricraven.com/docs/reports-api#filters), as in [Save a segment](https://metricraven.com/docs/filters-segments#segments). Each one has a `revision`: send the current one when changing or deleting it, so two people can't overwrite each other. | Field | Type | Required | Description | | --- | --- | --- | --- | | name | string | Yes | 1–64 characters, unique per website ignoring case. | | filters | array | Yes | 1–6 report filters. | | revision | integer | PUT, DELETE | The segment's current revision. | ```bash curl --request POST 'https://metricraven.com/api/v1/segments?site=WEBSITE_ID' \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" \ --header 'Content-Type: application/json' \ --data '{ "name": "US organic", "filters": [ { "dim": "country", "op": "is", "values": [ "US" ] }, { "dim": "channel", "op": "is", "values": [ "Organic Search" ] } ] }' ``` ```json title="Response (201)" { "id": "seg_0f1e2d3c4b5a69788796a5b4c3d2e1f0", "name": "US organic", "filters": [ { "dim": "country", "op": "is", "values": ["US"] }, { "dim": "channel", "op": "is", "values": ["Organic Search"] } ], "revision": 1, "createdAt": "2026-11-02T08:30:00.000Z", "updatedAt": "2026-11-02T08:30:00.000Z" } ``` | Request | Result | | --- | --- | | GET /segments | { segments: [...] } sorted by name. | | POST /segments | 201 with the segment. 409 duplicate_segment_name, or segment_limit at 50 segments. | | PUT /segments/SEGMENT_ID | Body: revision, name and the full filters. Returns the segment with revision + 1. | | DELETE /segments/SEGMENT_ID | Body: revision. Returns { ok: true }. | A stale revision returns 409 segment_changed: read the list again and retry. An invalid body or ID returns 400 invalid_segment, and a missing segment 404 not_found. ## Site settings `GET /site` returns the website's settings. `PUT /site` changes any subset of them and returns the updated settings. ```json title="GET /site response" { "id": "ana_pub_1a2b3c4d5e6f7a8b", "name": "Acme", "domains": ["acme.com", "app.acme.com"], "appIds": ["com.acme.app"], "exclusions": { "paths": ["/admin/*"], "ips": ["203.0.113.7"], "countries": [], "hostnames": ["staging.acme.com"] }, "gscProperty": "sc-domain:acme.com", "appearance": null, "primaryGoal": "signup" } ``` | Field | Type | Description | | --- | --- | --- | | name | string | 1–80 characters. | | domains | string[] | 1–20 domains. URLs are reduced to their host and www. is removed; an invalid one returns 400 invalid_domain. | | appIds | string[] | Up to 20 native app bundle or package IDs, such as com.acme.app, accepted from the mobile SDKs. Stored lowercase. | | exclusions | object | All four lists, which replace the current ones: `paths` (up to 100, starting with /), `ips` (up to 100), `countries` (up to 250 two-letter codes) and `hostnames` (up to 50). See [Exclude visits](https://metricraven.com/docs/exclude-visits). | | gscProperty | string or null | Search Console property: sc-domain:acme.com or https://acme.com/ (with the trailing slash). null unlinks it. | | primaryGoal | string or null | Goal name shown as the headline KPI ([primary goal](https://metricraven.com/docs/goals-funnels#primary-goal)); null restores the default. | | appearance | object or null | Dashboard colors: `light` and `dark`, each with `background`, `surface` and eight `graphs` colors as #RRGGBB. null restores the default theme. See [Appearance](https://metricraven.com/docs/appearance). | ```bash curl --request PUT 'https://metricraven.com/api/v1/site?site=WEBSITE_ID' \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" \ --header 'Content-Type: application/json' \ --data '{ "primaryGoal": "signup", "gscProperty": "sc-domain:acme.com" }' ``` Fields you leave out are unchanged. Invalid JSON returns 400 invalid_json; invalid fields return 400 invalid_settings with `details`. ## Secret keys Besides its default secret key, a website can have up to 10 named keys, for example one per server or integration. Every key has the same access; named keys let you see where each is used and revoke one without touching the others. These endpoints need an account token with the write scope (or a dashboard session) of an admin or the owner. A site secret key can't call them, so a leaked key can't create more keys. | Request | Does | | --- | --- | | GET /site/keys | Lists the keys: id (default for the default key, else key_…), name, hint, createdAt, lastUsedAt, plus previous (replaced values still accepted) and maxNamedKeys. | | POST /site/keys | Body { name } (1–60 characters, unique). Returns 201 with key and secretKey, shown once. | | PATCH /site/keys/{id} | Body { name }: renames a key, the default key included. | | DELETE /site/keys/{id} | Revokes a named key at once. The default key returns 400 default_key: roll it instead. | | POST /site/keys/{id}/roll | Body { grace: none \| 24h \| 7d } (default none): a new value, returned once; the old value keeps working for the grace period. | ```bash curl --request POST 'https://metricraven.com/api/v1/site/keys?site=WEBSITE_ID' \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" \ --header 'Content-Type: application/json' \ --data '{ "name": "Billing worker" }' ``` ```json title="Response" { "key": { "id": "key_3f9a1c0b7d2e4a51", "name": "Billing worker", "hint": "ana_sec_Q2xv…", "createdAt": "2026-11-02T10:00:00.000Z", "lastUsedAt": null, "default": false }, "secretKey": "ana_sec_Q2xv…full key, shown once" } ``` ```bash curl --request POST 'https://metricraven.com/api/v1/site/keys/default/roll?site=WEBSITE_ID' \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" \ --header 'Content-Type: application/json' \ --data '{ "grace": "24h" }' ``` Errors: 400 too_many_keys at the limit, 409 duplicate_name, 404 not_found for an unknown ID, 403 forbidden with a site secret key, 429 too_many_attempts after 30 new or rolled keys in an hour. Only the default key can be used to add an existing website to another account. ## Details - Note dates are calendar days, not instants: a note dated 2026-10-14 shows on that day in any report time zone. - Funnel IDs are made from the name plus a random suffix and don't change when you rename the funnel. - Segment names are compared after Unicode normalization, ignoring case and repeated spaces; filters are stored normalized, with duplicate values removed. - The share link stays in the dashboard. Owners delete a website in Settings or with [DELETE /api/v1/websites/WEBSITE_ID](https://metricraven.com/docs/account-api#update-website). --- # Visitors and people API > Read one visitor's full profile and timeline, find visitors and journeys, and erase a person's data. Source: https://metricraven.com/docs/visitors-api Markdown: https://metricraven.com/docs/visitors-api.md Section: API reference These endpoints return or delete data about individual people, so share links can never use them. Read them with the site secret key or an [account token](https://metricraven.com/docs/api-authentication#scopes) with the visitors scope. | Request | Use it for | | --- | --- | | GET /stats/visitors | [Search visitors in a period](https://metricraven.com/docs/reports-api#visitors) | | GET /stats/journeys | [People who paid or completed a goal](https://metricraven.com/docs/reports-api#journeys) | | GET /visitors/VISITOR_ID | One person's profile and complete timeline (below). | | DELETE /people | Erase one person (below). | | DELETE /payments, DELETE /events | [Delete payments or goal events](https://metricraven.com/docs/payments-api#delete) | ## Search visitors `GET /stats/visitors` lists the period's visitors (see the [Reports API](https://metricraven.com/docs/reports-api#visitors) for the period, paging and response). Besides the JSON `filters`, it takes plain search parameters; each becomes one filter, all must match, and comma-separated values match any of them. At most six filters apply in total. | Parameter | Matches visitors who… | | --- | --- | | visitedPage (page) | Viewed this exact path, e.g. /pricing. | | visitedPageContains (pageContains) | Viewed a path containing this text, without case. | | entryPage | Started their first visit in the period on this path. | | completedGoal (goal) | Completed this goal in the period. | | referrer | Came from a referrer containing this text, e.g. google. | | channel | Arrived first through this channel: Direct, Organic Search, Social, Referral, Email, Paid… | | utm_source, utm_medium, utm_campaign, utm_term, utm_content | Had a pageview with these UTM tags. | | ref, source, via | Had a pageview with these link tags (?ref=, ?source=, ?via=). | | country, region, city | Were located there; country takes two-letter codes. | | browser, os, device, hostname | Used this browser, OS, device (desktop, mobile, tablet) or hostname. | | visitType | new (first pageview ever in the period) or returning. | | visits_gt, visits_lt, visits | Made more than, fewer than or exactly this many visits in the period. A visit ends after 30 minutes without a pageview. | ```bash curl 'https://metricraven.com/api/v1/stats/visitors?range=all&visitedPage=/pricing&completedGoal=signup&visits_gt=2' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" ``` The response's `filters` lists every filter that applied. An invalid value, or more than six filters, returns 400 invalid_visitor_query with a message. The same numeric filter works in every report's `filters` as `{"dim":"visits","op":"gt","values":["2"]}` (ops gt, lt, is, is_not). ## Visitor profile `GET /visitors/VISITOR_ID` returns everything known about one person: every visitor ID linked to the same user by [identify](https://metricraven.com/docs/identify-users), their visits, goals, sign-ins and payments. It takes no period and is never cached. ```bash curl 'https://metricraven.com/api/v1/visitors/0123456789abcdef0123456789abcdef' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" ``` ```json title="Response" { "visitorId": "0123456789abcdef0123456789abcdef", "visitorIds": ["0123456789abcdef0123456789abcdef", "fedcba9876543210fedcba98"], "user": { "userId": "user_42", "email": "ana@example.com", "name": "Ana", "traits": { "plan": "pro" }, "verified": true }, "summary": { "firstSeenAt": "2026-10-01T09:14:02.000Z", "lastSeenAt": "2026-10-03T18:40:11.000Z", "visits": 3, "pageviews": 9, "events": 2, "timeOnSite": 412, "spent": 29, "firstTouch": { "source": "google.com", "channel": "Organic Search", "landingPage": "/", "at": "2026-10-01T09:14:02.000Z" }, "device": { "browser": "Chrome", "os": "macOS", "deviceType": "desktop", "app": null }, "location": { "country": "US", "region": "California", "city": "San Francisco" } }, "timeline": [ { "type": "pageview", "at": "2026-10-01T09:14:02.000Z", "path": "/", "newVisit": true, "source": "google.com", "channel": "Organic Search" }, { "type": "event", "at": "2026-10-01T09:16:40.000Z", "name": "signup", "path": "/signup", "props": { "plan": "free" }, "newVisit": false }, { "type": "identify", "at": "2026-10-01T09:16:41.000Z", "email": "ana@example.com" }, { "type": "payment", "at": "2026-10-03T18:40:11.000Z", "amount": 29, "currency": "USD", "plan": "pro", "renewal": false, "refunded": false, "refundedAmount": 0 } ] } ``` | Field | Meaning | | --- | --- | | visitorIds | The requested ID plus every visitor ID identified as the same user. | | user | null for anonymous visitors. Otherwise userId, email, name, custom traits and verified: true when the identity came from your server, false when only the browser claimed it. | | summary | firstSeenAt, lastSeenAt, visits (a new visit after 30 minutes of inactivity), pageviews, events, timeOnSite (seconds visible), spent (net of refunds, all currencies added), firstTouch, device (app has id and version for native apps) and location of the latest located event. | | timeline[] | Oldest first. pageview (path, newVisit, and source and channel when it starts a visit), event (name, path, props, newVisit), identify (email) and payment (amount, currency, plan, renewal, refunded, refundedAmount). | Visitor IDs are 24 (daily, before consent) or 32 (persistent) lowercase hex characters; anything else returns 400 invalid_visitor_id. An ID with no events, payments or identity returns 404 not_found. ## Erase a person `DELETE /people` handles a deletion request: it deletes the person's events and identity links and keeps their payments in revenue totals without any link to them. It accepts only the site secret key or a signed-in admin; account tokens get 403 so an assistant can never erase data. Dashboard steps are in [Delete a person's data](https://metricraven.com/docs/delete-person-data). | Field | Type | Description | | --- | --- | --- | | user_id | string | Your application's user ID (1–200 characters). | | visitor_id | string | A visitor ID. | | email | string | An email sent with identify; matched without case to the user IDs it was identified as. | Send at least one. Every visitor ID linked to the user IDs found is erased too. ```bash curl --request DELETE 'https://metricraven.com/api/v1/people' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" \ --header 'Content-Type: application/json' \ --data '{ "email": "ana@example.com" }' ``` ```json title="Response" { "ok": true, "visitorIds": 2, "deleted": { "events": 41, "identities": 2 }, "anonymizedPayments": 1 } ``` | Status | Meaning | | --- | --- | | 400 invalid_request | No user_id, visitor_id or email, or an invalid one. | | 403 forbidden | Called with an account token. | | 404 not_found | Only an email was sent and nobody was identified with it. | > **Warning:** Erasure can't be undone. Payments stay, but their visitor, user and customer IDs are removed. ## Details - Reports read with a token without the visitors scope, or with a share link, return null for names, emails, user IDs and visitor IDs. - A browser identify is self-asserted until your server confirms it with POST /identify; verified tells them apart. --- # Delete a person's data > Erase one person's analytics events and identity records from the dashboard or the API. Source: https://metricraven.com/docs/delete-person-data Markdown: https://metricraven.com/docs/delete-person-data.md Section: API reference Remove a person's linked events and identity records, for example after a deletion request. ## From the dashboard 1. **Open Settings → Erase a person** Enter an email, your application's user ID or a visitor ID. 2. **Confirm the deletion** ## From your backend ```bash curl --request DELETE 'https://metricraven.com/api/v1/people' \ --header "Authorization: Bearer $METRICRAVEN_SECRET_KEY" \ --header 'Content-Type: application/json' \ --data '{ "user_id": "user_42" }' ``` ## Details - This deletes linked events and identity records. - Payments remain in revenue totals, with their personal links removed. - Request fields, the response and errors are in the [Visitors and people API](https://metricraven.com/docs/visitors-api#erase). --- # Account API > Manage websites, access tokens, team members and your profile, and check who an access token belongs to. Source: https://metricraven.com/docs/account-api Markdown: https://metricraven.com/docs/account-api.md Section: API reference Account endpoints work across websites and need an [account access token](https://metricraven.com/docs/api-authentication), not a site secret key. ## Endpoints | Request | Returns | | --- | --- | | GET /api/v1/account | The token's owner (email, name) and the token's name, scopes and websites (null means all). | | PATCH /api/v1/account | Changes your display name from JSON { name } (null clears it). Needs the write scope. Your email is changed in the dashboard. | | GET /api/v1/websites | Websites the token can use: id, name, domains, your role (owner, admin or viewer) and createdAt. | | POST /api/v1/websites | Creates a website from JSON with name and domain. Needs the write scope on a token for all websites. Returns the website and its secret key, once. | | GET /api/v1/websites/WEBSITE_ID | One website: id, name, domains, appIds, your role and createdAt. | | PATCH /api/v1/websites/WEBSITE_ID | Renames the website or replaces its domains (JSON name and/or domains). Write scope, admin or owner. | | DELETE /api/v1/websites/WEBSITE_ID?confirm=DOMAIN | Deletes the website and all of its data. Write scope and the owner only; confirm must be the website's domain, as in the dashboard. | | GET /api/v1/tokens | Your access tokens: id, name, hint, scopes, websites, createdAt, lastUsedAt, and current for the token you call with. | | POST /api/v1/tokens | Creates an access token from JSON name, scopes and siteIds. Returns it once. Never more access than the calling token. | | DELETE /api/v1/tokens/TOKEN_ID | Revokes a token at once. Only tokens no broader than the calling token. | | GET /api/v1/members?site=… | Members with id, email, name and role. Admins and the owner also get pending invitations and plan seats. | | POST /api/v1/members?site=… | Invites an email address as admin or viewer. Returns the invitation and its link (also emailed). | | PATCH /api/v1/members/USER_ID?site=… | Changes a member's role to admin or viewer. | | DELETE /api/v1/members/USER_ID?site=… | Removes a member. They and their tokens lose the website at once. | | POST /api/v1/members/invites/INVITE_ID?site=… | Resends a pending invitation with a new link and a fresh 7-day expiry. | | DELETE /api/v1/members/invites/INVITE_ID?site=… | Revokes a pending invitation. | | GET /api/v1/bot-traffic/settings?site=… | Crawler exclusions, bot token status and the catalog of known crawlers. See bot traffic settings. | | PUT · PATCH /api/v1/bot-traffic/settings?site=… | Replaces the exclusion lists, or changes them incrementally (disable/enable companies and agents). | | POST · PUT · DELETE /api/v1/bot-traffic/token?site=… | Creates, rotates or deletes the ingest-only bot token. The token is returned once. | | GET /api/v1/bot-traffic/usage?site=… | AI and bot requests used this cycle against the allowance. | ## List websites ```bash curl https://metricraven.com/api/v1/websites \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" ``` ```json title="Response" { "websites": [ { "id": "ana_pub_1a2b3c4d5e6f7a8b", "name": "Acme", "domains": ["acme.com"], "role": "owner", "createdAt": "2026-10-01T09:30:00.000Z" } ] } ``` ## Create a website ```bash curl --request POST https://metricraven.com/api/v1/websites \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" \ --header 'Content-Type: application/json' \ --data '{ "name": "Acme", "domain": "acme.com" }' ``` Add the tracking script with the returned `id` as `data-site-id`, and store `secretKey` on your server for events and payments. ## Update or delete a website ```bash curl --request PATCH https://metricraven.com/api/v1/websites/WEBSITE_ID \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" \ --header 'Content-Type: application/json' \ --data '{ "name": "Acme", "domains": ["acme.com", "acme.app"] }' ``` Deleting a website removes its pageviews, goals, payments, visitors, funnels, segments, notes, alerts and integrations for good, and every member loses access. Only the owner can do it, with a write token, and like in the dashboard you confirm with the website's domain. A site secret key can never delete a website, and apps connected with OAuth can't either. ```bash curl --request DELETE "https://metricraven.com/api/v1/websites/WEBSITE_ID?confirm=acme.com" \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" ``` Without the right `confirm` the API answers 400 confirmation_required with the value to send. The response lists how many stored records were removed per kind. ## Manage access tokens A token with the write scope can create and delete the account's tokens, for example to give a script or an agent its own narrower token. A new token can never have more than the token that creates it: only scopes the caller has, and if the caller is limited to some websites, only some of those (then `siteIds` is required). Without `scopes` the new token is read-only; without `siteIds` it gets the caller's websites. Tokens of apps connected with OAuth can list tokens but not create or delete them. ```bash curl --request POST https://metricraven.com/api/v1/tokens \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" \ --header 'Content-Type: application/json' \ --data '{ "name": "Weekly report script", "scopes": ["read"], "siteIds": ["WEBSITE_ID"] }' ``` ```json title="Response" { "token": { "id": "6ae82a480561949e45b88f60", "name": "Weekly report script", "hint": "mr_Q2xv…", "scopes": ["read"], "siteIds": ["WEBSITE_ID"], "createdAt": "2026-11-02T09:30:00.000Z", "lastUsedAt": null, "current": false }, "value": "mr_…" } ``` `value` is shown only here; MetricRaven stores a hash. Deleting works for tokens no broader than the calling token (a token may delete itself). Asking for more returns 403 forbidden with the reason; past 25 tokens, 400 too_many_tokens. ## Manage team members Member endpoints act as the token's owner, with their [role](https://metricraven.com/docs/team-members#roles) on the website: any member can list the team; inviting, changing roles and removing need the write scope and an admin or the owner. A site secret key can't manage members. Transferring ownership and leaving a website stay in the dashboard. ```bash curl --request POST "https://metricraven.com/api/v1/members?site=WEBSITE_ID" \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" \ --header 'Content-Type: application/json' \ --data '{ "email": "sam@acme.com", "role": "viewer" }' ``` ```json title="Response" { "status": "success", "data": { "invite": { "id": "6ae82a480561949e45b88f54", "email": "sam@acme.com", "role": "viewer", "expiresAt": "2026-11-09T06:14:32.273Z", "expired": false }, "url": "https://metricraven.com/invite/mri_…", "emailSent": true } } ``` ## Details - Inviting counts toward your plan's team seats; over the limit, the API returns 402 member_limit. - Creating a website counts toward your plan's website allowance; over it, the API returns 402 website_limit. - Read reports for a website with the [Reports API](https://metricraven.com/docs/reports-api), adding `site=WEBSITE_ID`. --- # API playground > Try the MetricRaven API from your browser: pick an endpoint, fill the parameters, send it with your key and see the response. Source: https://metricraven.com/docs/api-playground Markdown: https://metricraven.com/docs/api-playground.md Section: API reference Pick an endpoint, set its parameters and send it with an [account token](https://metricraven.com/docs/api-authentication) (`mr_…`) or a website's secret key (`ana_sec_…`). Signed in, you can also read reports for your own websites without a key. Every request goes to `/api/v1` on this site and returns live data. > **Interactive:** the request form runs on the [web version of this page](https://metricraven.com/docs/api-playground). Every endpoint, its credentials and scopes are listed in the [API overview](https://metricraven.com/docs/api-overview). ## Which key to use - **Account token**: reports, settings and account endpoints across your websites. Add the website ID; the token's scopes (read, visitors, write) decide what it can do. Team members and your email preferences act as you, so they need a token, never the secret key. - **Site secret key**: one website, including server tracking (events, payments, identify, crawlers) and deleting a person's data. - **Signed in**: read-only requests with your dashboard session. Writes always need a key and an explicit confirmation. ## Keeping your key safe - The key is kept in memory, or in this tab's sessionStorage if you tick *Keep for this tab*. It is sent only as an Authorization header and never added to the page URL or the code samples. - Don't share your screen or hand over the tab while a key is entered. Use Clear when you're done. - Prefer a read-only token for exploring; revoke it in Settings → API & MCP if it may have leaked. --- # Connect AI assistants with MCP > Connect Claude, ChatGPT, Cursor, VS Code, Codex and other MCP clients with OAuth and ask about your traffic, revenue and goals. Source: https://metricraven.com/docs/mcp Markdown: https://metricraven.com/docs/mcp.md Section: MCP MetricRaven runs a hosted MCP server, so an AI assistant can read your analytics and answer questions like “which channels brought paying customers last month?”. There is nothing to install. ```text title="Server URL" https://metricraven.com/api/mcp ``` It uses the Streamable HTTP transport. Most clients connect with OAuth: you add the URL, sign in to MetricRaven in your browser and choose what the assistant may see. Clients without OAuth can send an [access token](https://metricraven.com/docs/mcp#access-token) instead. ## Connect with OAuth 1. **Add the server URL to your client** Use the command or setting for your client below. No token or header is needed. 2. **Sign in and choose what to share** Your client opens MetricRaven in the browser. Sign in, then choose **Reports** (always on), **Visitor details** (names, emails and journeys) and **Write** (add websites and chart notes), and which websites the app may use. 3. **Allow** You're sent back to the app and it's connected. It appears under **Connected apps** in [Settings → API & MCP](https://metricraven.com/app/api), where you can disconnect it at any time. ### Claude Code ```bash claude mcp add --transport http metricraven https://metricraven.com/api/mcp ``` Then run `/mcp`, choose `metricraven` and **Authenticate**. Add `--scope user` to use it in every project. ### Claude Desktop and claude.ai Open **Settings → Connectors**, choose **Add custom connector**, paste the server URL and select **Connect**. ### ChatGPT With developer mode on, open **Settings → Apps & Connectors**, create a connector with the server URL and OAuth authentication, then connect it. See [ChatGPT app](https://metricraven.com/docs/chatgpt-app) for the steps and the interactive charts. ### Cursor ```json title="~/.cursor/mcp.json" { "mcpServers": { "metricraven": { "url": "https://metricraven.com/api/mcp" } } } ``` In **Settings → MCP**, select **Needs login** next to `metricraven`. ### VS Code ```json title=".vscode/mcp.json" { "servers": { "metricraven": { "type": "http", "url": "https://metricraven.com/api/mcp" } } } ``` Start the server from the file or the MCP view; VS Code asks you to sign in. ### Codex ```bash codex mcp add metricraven --url https://metricraven.com/api/mcp codex mcp login metricraven ``` ## Use an access token instead For scripts, CI, or clients without OAuth, create an [access token](https://metricraven.com/docs/api-authentication) in [Settings → API & MCP](https://metricraven.com/app/api) and send it as `Authorization: Bearer`. Keep it in an environment variable, for example `export METRICRAVEN_TOKEN=mr_…`. ### Claude Code ```bash claude mcp add --transport http metricraven https://metricraven.com/api/mcp \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" ``` ### Codex ```toml title="~/.codex/config.toml" [mcp_servers.metricraven] url = "https://metricraven.com/api/mcp" bearer_token_env_var = "METRICRAVEN_TOKEN" ``` ### Cursor ```json title="~/.cursor/mcp.json" { "mcpServers": { "metricraven": { "url": "https://metricraven.com/api/mcp", "headers": { "Authorization": "Bearer mr_YOUR_TOKEN" } } } } ``` ### Clients that only start local servers Use the `mcp-remote` bridge: ```json title="claude_desktop_config.json" { "mcpServers": { "metricraven": { "command": "npx", "args": ["-y", "mcp-remote", "https://metricraven.com/api/mcp", "--header", "Authorization: Bearer ${METRICRAVEN_TOKEN}"], "env": { "METRICRAVEN_TOKEN": "mr_YOUR_TOKEN" } } } } ``` ## Try it - “How did traffic and revenue change this month compared with last month?” - “Which referrers brought paying customers in the last 90 days?” - “Where do visitors drop off in my signup funnel?” - “Which AI crawlers read my docs this week?” - “Add a chart note for today: launched the new pricing page.” See every tool and its inputs in the [tool reference](https://metricraven.com/docs/mcp-tools). > **Note:** Assistants only see what you allowed when connecting, or what the token allows. Without Visitor details, reports never include names, emails or visitor IDs. ## Details - OAuth follows the MCP authorization spec: discovery through `/.well-known/oauth-protected-resource`, dynamic client registration, PKCE (S256) and tokens bound to `https://metricraven.com/api/mcp`. They don't work on the REST API. - OAuth access tokens last an hour and are refreshed automatically for up to 30 days of inactivity. Disconnecting an app in Settings stops it at once. - Apps may return to an https address, to `localhost` on any port, or to the `cursor://`, `vscode://` and `vscode-insiders://` app links. The consent screen shows where you'll be sent back. - App names on the consent screen are chosen by the app and aren't verified. Only allow apps you just added. - A site secret key also works, limited to its own website. Prefer OAuth or an access token, which you can limit and revoke. - The MCP server can't delete websites or people's data, or change billing. - Limit: 240 requests a minute per connection or token. --- # ChatGPT app > Ask ChatGPT about your MetricRaven traffic, revenue, goals and funnels, with interactive charts and access limited to the websites you approve. Source: https://metricraven.com/docs/chatgpt-app Markdown: https://metricraven.com/docs/chatgpt-app.md Section: MCP Ask ChatGPT about your analytics and see the answer as an interactive chart in the conversation: traffic and revenue over time, the channels and pages that convert, your funnels and who is on your site right now. > **Note:** The MetricRaven app is waiting for its listing in ChatGPT's app directory. Until it appears, connect it yourself with developer mode, as below. ## What you can ask - **Chart traffic and revenue.** “Chart my visitors and revenue for the last 30 days.” Switch between 7, 30 and 90 days on the chart, or open the website in MetricRaven. - **Compare channels.** “Which channels brought the most revenue this month?” Also pages, campaigns, referrers, countries, devices and browsers. - **Goals and funnels.** “Show my signup funnel and where visitors drop off.” - **Right now.** “How many people are on my site, and from where?” Name the website and the dates in your question when you have several. Revenue is credited to the visit that first brought each customer, and your plan's limits still apply. ## Connect MetricRaven 1. **Turn on developer mode** In ChatGPT, open **Settings → Apps** (or **Security and login**, depending on your plan) and turn on **Developer mode**. Workspace admins may need to allow it. 2. **Create the app** Add a new app, name it **MetricRaven**, enter this server URL and choose **OAuth**: ```text https://metricraven.com/api/mcp ``` 3. **Approve access** Sign in to MetricRaven, choose the websites and permissions to share and select **Allow**. Read is enough for every report. You never paste an API key. 4. **Ask a question** Start a new conversation, turn on MetricRaven from the tools menu and try one of the questions above. ## You choose what to share - ChatGPT can only use the websites and permissions you approve. MetricRaven checks your current membership and role on every request, so removing you from a website removes it from ChatGPT too. - **read** covers reports and charts. **visitors** adds individual visitor journeys. **write** allows chart notes, alerts, goal labels and team changes; ChatGPT explains and asks before making them. You can't delete websites or connect payment providers from ChatGPT. - The answers you ask for are shared with ChatGPT. Your password, dashboard session and API keys are not, and the chart receives only the report it shows, never your connection token. - Disconnect in MetricRaven under **Settings → API & MCP → Connected apps**. That stops new requests and token refreshes at once; it doesn't delete what ChatGPT already received. ## How it works The app is MetricRaven's [MCP server](https://metricraven.com/docs/mcp) with OAuth sign-in. Two tools, `show_traffic_chart` and `show_breakdown_chart`, return a chart that ChatGPT renders in the conversation; every other tool is listed in [MCP tools](https://metricraven.com/docs/mcp-tools). The chart is self-contained and loads nothing from other sites. Claude, Cursor and other MCP clients use the same server. ## Demo A short recording of connecting MetricRaven and charting traffic in ChatGPT will appear here with the directory listing. --- # MCP tool reference > Every tool the MetricRaven MCP server offers, its inputs and the scope it needs. Source: https://metricraven.com/docs/mcp-tools Markdown: https://metricraven.com/docs/mcp-tools.md Section: MCP Assistants pick these tools themselves. A tool only appears when the access you allowed when connecting, or your [token's scopes](https://metricraven.com/docs/api-authentication#scopes), include its scope. ## Common inputs | Input | Meaning | | --- | --- | | website_id | From list_websites. Optional when the connection can use only one website. | | period | today, yesterday, 24h, 7d, 30d (default), 90d, 6m, 1y, wtd, mtd or all. | | from, to | Calendar days, YYYY-MM-DD, both inclusive. Replace period. | | timezone | IANA name such as Europe/Madrid for day boundaries and grouping. Default UTC. | | filters | Up to six filters, for example [{"dim":"country","op":"is","values":["US"]}]. | | compare | What change figures compare against: previous (default), year (same period last year) or a YYYY-MM-DD start. The comparison keeps the period's length. | ## Reports (read scope) | Tool | Returns | | --- | --- | | list_websites | Websites the connection can use: id, name, domains. | | get_traffic | Visitors, pageviews, visits, bounce rate, session time, revenue, conversion rate and revenue per visitor vs the previous period, a time series, top channels, referrers, campaigns, pages, countries and devices, and spikes (days one source sent far more visitors than usual). Optional granularity. | | get_breakdown | A complete ranked table for one dimension (channel, referrer, utmSource, page, entryPage, country, city, browser, device, outbound…), with sort, q, limit and offset. | | show_traffic_chart | The same traffic report trimmed for a chart: visitors, revenue, conversion and bounce rate with change, a daily series and the top channels. Clients that support MCP Apps (ChatGPT, Claude) show it as an interactive chart. | | show_breakdown_chart | A ranked chart for channel, referrer, utmSource, utmCampaign, page, entryPage, country, device, browser or os by visitors or revenue (limit up to 20). Shown as a chart in MCP Apps clients. | | get_revenue | Payments, new and renewal revenue, refunds, MRR estimates, customers and lifetime value, by first-touch source. Accepts filters, including plan, currency, paymentType, refund and provider. | | get_conversions | Goal completions and rates, paying customers and funnel steps with drop-off. Each goal keeps its raw name and adds displayName, color, description and hidden. | | get_goal | One goal by name: completions over time, properties and sources, with its display settings. | | get_insights | Which channels, pages, countries and devices turn visitors into buyers, time to first purchase and source changes. | | get_search_keywords | Search Console queries and pages, when connected. | | get_ad_spend | Ad spend from every connected network (Meta Ads, Google Ads; provider), with ad visitors, customers, first-touch and assisted revenue, ROAS and cost per customer per campaign, ad group or ad (level), and combined totals. Click-ID-only visits are unattributed and not in totals. | | get_meta_ads | Meta Ads only (get_ad_spend with provider=meta), when connected. | | get_ai_crawlers | AI crawler and bot requests by crawler, purpose and page. | | get_realtime | Visitors online now with page, source, country and device. | | list_funnels | Saved funnels and their steps. | | list_notes | Chart notes in the period. | | list_commits | GitHub commits from the connected repository in the period, optionally filtered by type (feat, fix…). | | list_mentions | Stored X and Reddit posts that mention your domain or keywords (Growth): author, text, link and engagement. Inputs q, platform (x, reddit), order, limit (up to 1000), offset. | | get_ai_visibility | What AI assistants (ChatGPT, Claude, Perplexity, Gemini, Grok) answer to your tracked prompts (Growth): scores and trend, per-assistant results, the brands they name, per-prompt results and cited pages. Optional prompt_id for that prompt's latest answers, and ai_filters (assistant, prompt, topic, brand named, your mention, cited domain). | | get_x_referrers | Visitors and revenue from X by t.co link, with the X post (or link-in-bio profile) behind each link (Growth). Supports filters. | | list_goal_customizations | Goal display settings: display name, colour, internal description and hidden flag per raw goal name, plus the allowed colours. Optional goal. | | list_alerts | Conversion alerts: trigger, recipients, on/off and how often each fired, plus the goal names the site tracks. | | get_alert_history | Emails the alerts sent, newest first: instant or summary, conversions, status and errors. Optional alert_id and limit. | | list_members | Team members with email, name and role; for admins also pending invitations and plan seats. Account tokens only. | | get_bot_settings | Crawler companies and agents excluded from bot tracking, whether the bot token is required, the token's status and the catalog of known crawlers. Account tokens only. | | list_tokens | The account's access tokens: name, hint, scopes, websites and last use (never the values). Account tokens only; create and delete tokens in Settings or with the CLI. | | get_bot_usage | AI and bot requests used this cycle, the allowance, requests refused after it ran out, the reset date and websites that used them. Account tokens only. | | get_managed_proxy | The managed proxy subdomain, its status (waiting for DNS, issuing certificate, active, error), the DNS records to add and the script tag. Account tokens only. | ## Visitors (visitors scope) | Tool | Returns | | --- | --- | | search_visitors | Visitors in the period with source, location, pageviews and payments. Inputs q, payment (all, paid, unpaid), sort, limit, offset, plus search filters: visited_page, visited_page_contains, completed_goal, referrer, channel, utm_source, utm_medium, utm_campaign, country, device, browser, os, visits_gt and visits_lt (visits in the period), and filters. | | get_visitor | One visitor's profile and full journey: visits, pages, goals and payments. Input visitor_id. | ## Changes (write scope) | Tool | Does | | --- | --- | | create_note | Adds a note to the traffic chart. Inputs date (YYYY-MM-DD, up to tomorrow) and text (up to 280 characters). | | create_alert | Emails members on a goal (trigger goal + goal_name), any payment or a new customer. Optional name, subject, message, notify (me or members) and enabled. | | update_alert | Renames an alert, changes its goal, or turns it on or off. Input alert_id. | | delete_alert | Deletes an alert; its email history is kept. Input alert_id. | | update_goal_customization | Sets a goal's display_name, color, description or hidden; null clears a field. Input goal (the raw name). Admins and the owner. | | reset_goal_customization | Removes a goal's display settings. Input goal. | | invite_member | Emails an invitation to join as admin or viewer and returns its link. Inputs email and role. Admins and the owner. | | change_member_role | Makes a member an admin or a viewer. Inputs user_id (from list_members) and role. | | remove_member | Removes a member; they and their tokens lose access at once. Input user_id. | | revoke_invite | Revokes a pending invitation. Input invite_id. | | update_bot_settings | Excludes or re-includes crawler companies or agents from now on (disable_companies, enable_companies, disable_agents, enable_agents), or requires the bot token (reject_without_token). Incremental; needs confirm: true. Admins and the owner. | | create_website | Adds a website and returns its ID, secret key (once) and tracking script. Needs access to all websites. | | delete_payments | Deletes payments: transaction_id, or visitor_id / user_id, optionally with start and end, or every payment between start and end. Without confirm: true it only returns the count (a dry run), so the assistant can ask you first. Admins and the owner. | | delete_goal_events | Deletes goal events: name, visitor_id / user_id, optionally with start and end, or every goal event between start and end. Pageviews are never deleted. Same dry run and confirm: true. Admins and the owner. | ## Calling the server directly The server speaks JSON-RPC over HTTP POST. You rarely need this, but it helps when testing: ```bash curl https://metricraven.com/api/mcp \ --header "Authorization: Bearer $METRICRAVEN_TOKEN" \ --header 'Content-Type: application/json' \ --data '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_traffic","arguments":{"period":"7d"}}}' ``` ## Details - Results are JSON. Very long results are cut at about 120,000 characters; narrow the period or add filters. - Tools return the same data as the [Reports API](https://metricraven.com/docs/reports-api), so numbers match the dashboard. - Without the visitors scope, `get_realtime` and other reports leave out names, emails and visitor IDs. --- # MetricRaven CLI > Install the CLI, log in with an access token and read your analytics from the terminal. Source: https://metricraven.com/docs/cli Markdown: https://metricraven.com/docs/cli.md Section: CLI Traffic, revenue, goals, visitors and websites from your terminal. Output is readable by default and JSON with `--json`, so it also works for scripts and AI agents that run shell commands. ## Install and log in 1. **Create an access token** In [Settings → API & MCP](https://metricraven.com/app/api), create a token. Reports only is enough to start; see [scopes](https://metricraven.com/docs/api-authentication#scopes). 2. **Log in** ```bash npx @metricraven/cli login ``` Paste the token when asked; it isn't shown as you type. To install the `metricraven` command permanently, run `npm install -g @metricraven/cli`. Node.js 20 or later. 3. **Pick a website and run a report** ```bash metricraven websites list metricraven use ana_pub_1a2b3c4d5e6f7a8b metricraven analytics traffic --period 7d ``` ## Periods and filters | Flag | Meaning | | --- | --- | | --period | today, yesterday, 24h, 7d, 30d (default), 90d, 6m, 1y, wtd, mtd or all. | | --from, --to | Calendar days, YYYY-MM-DD, both inclusive. | | --timezone | IANA name, e.g. America/New_York, for day boundaries. Default UTC. | | --filter, -f | dimension:operator:value, repeatable. Operators is, is_not, contains, not_contains. | | --compare | What change figures compare against: previous (default, the equal-length period right before), year (same period last year) or a YYYY-MM-DD start. | ```bash metricraven analytics breakdown referrer --sort revenue --limit 20 metricraven analytics traffic -f country:is:US -f device:is:mobile --json | jq .overview ``` ## Scripts, CI and agents | Variable | Meaning | | --- | --- | | METRICRAVEN_TOKEN | Access token; overrides the saved one. | | METRICRAVEN_SITE | Default website ID. | | METRICRAVEN_API_BASE | API origin, default https://metricraven.com. | ```bash title="Log in from stdin" echo "$METRICRAVEN_TOKEN" | metricraven login ``` Global flags: `--site`, `--json`, `--verbose` (request URLs and status codes on stderr), `--help` and `--version`. All commands are in the [command reference](https://metricraven.com/docs/cli-commands). > **Warning:** Prefer the hidden prompt, stdin or `METRICRAVEN_TOKEN` over `--token`, which can end up in your shell history. ## Details - The token is saved to ~/.config/metricraven/config.json (or $XDG_CONFIG_HOME), readable only by you. - Without a default website, the CLI uses the only website the token can see, or asks you to pick one. - Money is shown in your reporting currency's main unit, as in the dashboard. --- # CLI command reference > Every metricraven command, its options and the access it needs. Source: https://metricraven.com/docs/cli-commands Markdown: https://metricraven.com/docs/cli-commands.md Section: CLI Run `metricraven --help` for the options of any command. Dated reports accept the [period and filter flags](https://metricraven.com/docs/cli#periods-filters). ## Account and websites | Command | Does | | --- | --- | | login / logout | Save the access token after checking it, or remove it. | | whoami | The account, the token's access and the websites it can use. | | websites list | Websites the token can use; \* marks the default. | | websites create --name --domain | Add a website (write access, token for all websites). Prints its ID, secret key (once) and tracking script. | | websites get [\] | One website's name, domains, app IDs and your role (default: the default website). | | tokens list | The account's access tokens with their access, websites and last use; \* marks the token you are using. | | use \ | Save the default website. | ## Analytics (read access) | Command | Shows | | --- | --- | | analytics traffic | Totals with change vs the previous period, and the top channels, pages and countries. | | analytics breakdown \ | A full table for channel, referrer, page, entryPage, utmSource, utmCampaign, country, city, browser, device, outbound and more. Options --sort visitors\|pageviews\|revenue, --limit, --offset, --q. | | analytics revenue | Payments, renewals, refunds, lifetime value and revenue by channel. | | analytics conversions | Goals and funnel steps, by display name. Hidden goals are left out of the table (--json keeps them, with hidden: true). | | analytics goal \ | One goal with its sources and properties. | | analytics insights | What turns visitors into buyers and how long they take. | | analytics search | Google Search Console queries (when connected). | | analytics ads [campaign\|adgroup\|ad] [--provider all\|meta\|google] | Ad spend from every connected network, attributed and assisted revenue, ROAS and cost per customer, with combined totals. | | analytics meta-ads [campaign\|adset\|ad] | Meta Ads spend, attributed revenue, ROAS and cost per customer (when connected). | | analytics crawlers | AI crawlers and bots. | | realtime | Visitors online now. | | funnels list | Saved funnels and steps. | | goals list [goal] | Goal display settings: display name, colour, hidden and description. | | goals colors | The colours a goal can use. | | notes list | Chart notes in the period. | | mentions | X and Reddit posts that mention the website (Growth). --platform x\|reddit, --q, --limit, --offset. | | ai-visibility | What AI assistants answer to your tracked prompts (Growth): scores, per-assistant results and the brands they name. --period for the trend (default 90d); --filter model:is:claude, mention:is:not_mentioned, citedDomain:is:example.com and the like. | | alerts list | Conversion alerts with their trigger, recipients and counts. | | alerts get \ | One alert: recipients, custom email, today/month/total counts and the last error. | | alerts history [--alert \] | Emails the alerts sent, newest first. | | members list | Team members and roles; for admins also pending invitations and seats used. | | bots settings | Excluded crawler companies and agents, the bot token's status and the known crawlers. | | bots usage | AI and bot requests used this cycle, the allowance and when it resets. | | proxy status | The managed proxy subdomain, the DNS record to add, its status and, once active, the script tag. | ## Visitors (visitor details access) | Command | Shows | | --- | --- | | visitors list | Visitors in the period. Options --q, --payment all\|paid\|unpaid, --sort latest\|earliest\|pageviews, --limit, --offset, and search filters: --page \, --page-contains \, --goal \, --referrer \, --channel, --source, --utm-source, --utm-medium, --utm-campaign, --country \, --device desktop\|mobile\|tablet, --browser, --os, --visits-gt \, --visits-lt \. | | visitors get \ | One visitor's summary and journey. | ## Changes (write access) | Command | Does | | --- | --- | | notes add --text [--date YYYY-MM-DD] | Adds a chart note, dated today by default. | | goals set \ | Changes how a goal is shown: --name \