Custom Goals — Track every user action that matters

TL;DR: A goal is a discrete user action you want to count and attribute — a pricing CTA click, a trial signup, a webinar registration, a file download. Admaxxer offers three ways to fire one: a one-line JavaScript call (window.admx?.goal('Trial Started')), a zero-JavaScript data attribute (data-admx-goal="Trial Started"), or a server-side POST /api/event for offline conversions. Five reserved __admx_* goals fire automatically when you install script.plus.js — outbound link clicks, file downloads, form submits, video plays, and scroll depth at 50%, 75%, and 90%. Goals appear in your dashboard within 5 seconds, feed cohort LTV filters, and contribute to channel MMM.

What's a goal?

A goal is any discrete user action you want to count and attribute back to a traffic source, ad campaign, or cohort. Page views are noisy — goals are signal. Examples that customers track:

Each goal fire stores a goal_name, an anonymous_id (the visitor cookie set by the pixel), an optional user_id (after sign-in), an optional metadata object, and a server-side fired_at timestamp. Once stored, goals appear on /dashboard/analytics in the Goals card, drive cohort LTV filters in the Reports surface, and contribute as conversion events to the Marketing Mix Model.

Three ways to install — pick what fits your stack

Admaxxer offers three install paths so the goal fires from the layer that owns the action. SPA frameworks (React, Vue, Svelte) prefer the JS API. Static sites and Webflow/Framer/HubSpot prefer the data-attribute. CRM-driven and offline conversions go through the server-side API.

Path 1 — JavaScript API (recommended for SPAs)

Once the pixel is installed (see install docs), window.admx exposes a goal() method. Fire a goal with one line:

window.admx?.goal('Pricing CTA Click');

Attach metadata as the second argument:

window.admx?.goal('Trial Started', {
  plan: 'pro',
  source: 'hero',
  experiment: 'B'
});

The optional-chaining (?.) is intentional — if the pixel hasn't loaded yet (slow connection, ad-blocker), the call no-ops instead of throwing. We recommend wrapping all production goal calls this way.

Path 2 — Data attribute (recommended for static sites)

Add data-admx-goal="Goal Name" to any clickable element. The pixel auto-binds a click handler at boot and on any DOM mutation, so you don't need to wire JavaScript yourself. Works inside React, Vue, server-rendered HTML, and CMS templates equally well.

<button data-admx-goal="Buy Now">Buy now</button>
<a href="/pricing" data-admx-goal="Pricing Link Click">See pricing</a>

Pass metadata via data-admx-goal-meta as JSON:

<button
  data-admx-goal="Buy Now"
  data-admx-goal-meta='{"price":99,"plan":"pro"}'
>
  Buy now
</button>

Use single-quotes around the attribute and double-quotes inside the JSON — HTML attribute parsers expect this exact shape. Invalid JSON is silently dropped (the goal still fires, but without metadata).

Path 3 — Server-side API (for offline conversions and CRM goals)

Fire goals from your backend when the conversion happens off-browser — after a Stripe webhook, on a sales-call disposition, when a CRM moves a deal to "Closed Won". The endpoint is bearer-authenticated and accepts JSON:

POST https://admaxxer.com/api/event
Content-Type: application/json
Authorization: Bearer YOUR_WORKSPACE_API_KEY

{
  "name": "Webinar Registered",
  "anonymous_id": "u_abc123",
  "metadata": {
    "webinar": "2026-q2",
    "source": "email"
  }
}

The anonymous_id should match the visitor cookie set by the pixel on the user's prior browser session — that's what stitches the server-side goal back to the original ad click. If you have a logged-in user_id on hand, pass it too; Admaxxer indexes both. API keys are minted on /settings/api-keys and scoped to a single workspace.

Reserved goals fired automatically (Admaxxer-only)

When you install the upgraded pixel script.plus.js (instead of the basic script.js), Admaxxer fires five categories of reserved goals for you — no code, no data attributes. Datafast, Plausible, and Fathom do not have an equivalent. The reserved prefix is __admx_ and these goal names are lowercase by convention.

Reserved goalFires whenDefault metadata
__admx_outbound_click Any link to a different host than the current page { href, host, text }
__admx_file_download Any click on a link ending in .pdf, .zip, .dmg, .pkg, .exe, .csv, .xlsx, .docx, .pptx, or .mp4 { href, extension, filename }
__admx_form_submit Any <form> submit event (uncaptured by SPA framework) { formId, action, method }
__admx_video_play First play event on a <video> tag (deduped per session) { src, duration }
__admx_scroll_50 User scrolls past 50% of the page height (fired once per page view) { path }
__admx_scroll_75 User scrolls past 75% of the page height { path }
__admx_scroll_90 User scrolls past 90% of the page height — "consumed" the article { path }

Reserved goals are independent of any user-defined goals you fire. You'll see both in the Goals card on /dashboard/analytics — reserved goals are visually distinguished with a system-icon badge so you can filter them out at-a-glance. To enable, see the Pro tracking doc — in short, swap your <script src="https://admaxxer.com/js/script.js"> tag for script.plus.js and reload.

Naming convention — best practices

Goal names show up everywhere — the dashboard, cohort filters, MMM channel breakdowns, AI agent answers, exported CSVs. Pick names that are unambiguous, scan-able, and stable.

Metadata — what to attach, what to skip

Metadata is a free-form JSON object stored alongside the goal fire. Use it for anything you'll want to slice or filter on later.

Good metadata

Bad metadata — do not attach

Metadata limits

Where your goals appear

Once a goal fires, you'll see it in four surfaces:

  1. Goals card on /dashboard/analytics — total fires, unique-visitor count, and a sparkline of the last 7/30/90 days. Click any goal name to drill into per-fire detail.
  2. The p_goals_summary Tinybird pipe — aggregates fires per goal per day, exposed via GET /api/v1/analytics/goals. Use this for custom dashboards, weekly emails, and external reporting.
  3. Cohort filter in Reports — "show me the LTV of users who fired Goal X" or "show me cohort retention of users who registered for Webinar Y". Goals become first-class cohort dimensions alongside acquisition source and first-visit date.
  4. MMM channel attribution — the Marketing Mix Model treats goal fires as conversion events alongside revenue. Channels that drive high-value goals (e.g. "Demo Requested") get credited in the channel decomposition even when revenue happens later.

How this compares to Datafast and other lightweight analytics

Custom goals are table stakes — everyone has them. The differentiation is in what happens after the goal fires.

CapabilityAdmaxxerDatafastPlausible / Fathom
JS API for goal fires Yes — window.admx.goal() Yes Yes
Data-attribute auto-fire Yes — data-admx-goal Yes — data-fast-goal Partial
Server-side goal API Yes — POST /api/event bearer-authed, fully documented Limited — thin documentation No
Reserved auto-fire goals (outbound click, file download, scroll depth, video, form) Yes — 7 reserved goals when you install script.plus.js No No
Cohort LTV filtering by goal ("show LTV of users who fired Goal X") Yes No No
MMM channel contribution by goal Yes — goal fires feed the Marketing Mix Model No No
AI agent reads goals natively Yes — the Maxxer AI agent answers "which channel drove the most Webinar Registered last week" without you writing SQL No No
Storage backend Tinybird (ClickHouse) — scales linearly, no row-count limits ClickHouse internally ClickHouse internally

Runnable code examples

JavaScript — minimal goal fire

window.admx?.goal('Trial Started', { plan: 'pro' });

JavaScript — goal fire from a React click handler

function PricingCTA() {
  return (
    <button
      onClick={() => {
        window.admx?.goal('Pricing CTA Click', {
          plan: 'pro',
          source: 'hero',
        });
      }}
    >
      Start free trial
    </button>
  );
}

HTML data attribute — zero JavaScript

<button
  data-admx-goal="Buy Now"
  data-admx-goal-meta='{"price":99,"plan":"pro"}'
>
  Buy now — $99
</button>

Server-side — curl

curl -X POST https://admaxxer.com/api/event \
  -H 'Authorization: Bearer YOUR_WORKSPACE_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Webinar Registered",
    "anonymous_id": "u_abc123",
    "metadata": {"webinar": "2026-q2"}
  }'

Server-side — Node.js fetch

await fetch('https://admaxxer.com/api/event', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer YOUR_WORKSPACE_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Webinar Registered',
    anonymous_id: 'u_abc123',
    metadata: { webinar: '2026-q2', source: 'email' },
  }),
});

Server-side — Python requests

import requests

requests.post(
    'https://admaxxer.com/api/event',
    headers={
        'Authorization': 'Bearer YOUR_WORKSPACE_API_KEY',
        'Content-Type': 'application/json',
    },
    json={
        'name': 'Trial Converted to Paid',
        'anonymous_id': 'u_abc123',
        'metadata': {'plan': 'pro', 'mrr': 99},
    },
)

Troubleshooting

Goal not appearing on /dashboard/analytics

  1. Verify the network call. Open DevTools, Network tab, filter by event. Click the goal-firing element. You should see POST https://admaxxer.com/api/event with status 200. No request? The pixel never loaded — check the install (install docs) and CSP (CSP guide).
  2. Check the response. A 200 means it stored. A 400 means the goal name was invalid (started with __, exceeded 100 chars, or contained an illegal character). A 401 means your bearer token is wrong (server-side path only). A 413 means the metadata payload exceeded 8 KB.
  3. Wait 5–10 seconds. Goals land in Tinybird in near-real-time, but the dashboard's Goals card is cached for 30 seconds. Hard-reload (Cmd+Shift+R) and look again.
  4. Verify the goal name doesn't start with __. User-defined goals starting with double-underscore are rejected; the prefix is reserved for Admaxxer's auto-fired goals.
  5. Confirm the workspace. Goals are workspace-scoped — if you fired the goal from a page that loaded the pixel for a different workspace's site ID, it landed in that other workspace.

Metadata missing on the goal

CSP is blocking /api/event

If your site uses a Content Security Policy that disallows POSTs to admaxxer.com, goals will fire from the JS path but the network request will be blocked — you'll see Refused to connect in the console. Add https://admaxxer.com to the connect-src directive. See the CSP troubleshooting guide for framework-specific examples.

Reserved goals (__admx_*) not firing

Reserved goals only fire when you install script.plus.js, not the basic script.js. Check your <script src=> attribute and swap it. See the pro-tracking doc for the upgrade path.

Goal firing twice for one click

Common cause: the same element has both data-admx-goal and a JavaScript onClick handler that calls window.admx.goal(). Pick one. The data-attribute path auto-binds at boot, so adding the JS call on top double-fires.

API reference summary

For the full request/response contract of POST /api/event, error codes, rate limits, and example responses, see the Developer (REST API) docs. The same endpoint serves both pageviews and goal fires — the presence of a name field promotes the call from a pageview to a goal fire.

Install the pixel · Pro tracking (script.plus.js) · Dashboard analytics · UTM best practices · CSP troubleshooting · Developer (REST API)