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.
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.
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.
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.
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).
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.
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 goal | Fires when | Default 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.
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.
pricing_cta_click or PricingCTAClick.__. The double-underscore prefix is reserved for Admaxxer's auto-fired reserved goals (e.g. __admx_outbound_click). The API rejects user-fired goals starting with __ with HTTP 400.goal_name varchar(100). Names longer than 100 chars are truncated server-side to the first 100 characters.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.
{ plan: 'pro' }{ experiment: 'B' }{ category: 'pricing' }{ source: 'hero' } vs { source: 'sticky_footer' }{ price: 99, quantity: 2 }Once a goal fires, you'll see it in four surfaces:
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.Custom goals are table stakes — everyone has them. The differentiation is in what happens after the goal fires.
| Capability | Admaxxer | Datafast | Plausible / 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 |
window.admx?.goal('Trial Started', { plan: 'pro' });
function PricingCTA() {
return (
<button
onClick={() => {
window.admx?.goal('Pricing CTA Click', {
plan: 'pro',
source: 'hero',
});
}}
>
Start free trial
</button>
);
}
<button
data-admx-goal="Buy Now"
data-admx-goal-meta='{"price":99,"plan":"pro"}'
>
Buy now — $99
</button>
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"}
}'
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' },
}),
});
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},
},
)
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).__, 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.__. User-defined goals starting with double-underscore are rejected; the prefix is reserved for Admaxxer's auto-fired goals.window.admx?.goal('X', { k: 'v' }) — not window.admx?.goal('X', '{"k":"v"}').data-admx-goal-meta value must be valid JSON. Use single-quotes around the attribute (data-admx-goal-meta='{"k":"v"}') so the inner double-quotes don't conflict. Invalid JSON is silently dropped.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.
__admx_*) not firingReserved 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.
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.
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)