Skip to content

Documentation

Attruly records what visitors do, resolves those events to people, and measures every channel, campaign, device and OS version against every milestone you define. Two minutes to install, and everything after that. If something here is wrong or missing, tell us and we will fix it.

Quickstart

Add one script tag. Pageviews start recording immediately, including on client-side route changes.

  1. Create a project

    Sign in, create a workspace, and add your first project. You get a public key (pk_live_…) and a secret key (sk_live_…). The secret is shown once.
  2. Add the snippet

    Paste this just before </head>.
    index.html
    <script defer
      src="https://www.attruly.com/a.js"
      data-key="pk_live_YOUR_KEY"></script>
  3. Load a page

    Open your site. The Install tab in Attruly turns green within a few seconds of the first event.
  4. Or verify without opening a browser

    Useful in CI, or when an agent is doing the install. A working key returns { "accepted": 1, "dropped": 0 }. A bad key returns 401.
    curl
    curl -X POST https://www.attruly.com/api/v1/track \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer pk_live_YOUR_KEY" \
      -d '{"event":"Install Check","anonymousId":"install-check"}'

Browser SDK

Under 4KB gzipped, no dependencies, and it never throws into your code.

Configuration

Everything is a data attribute on the script tag.

AttributeDefaultWhat it does
data-keyRequiredYour public key.
data-hosthttps://www.attruly.comIngest origin. Defaults to where the script was served from.
data-auto-pageviewtrueSet to false to send pageviews yourself.
data-session-timeout30Minutes of inactivity that end a visit. Match your project setting.
data-respect-dntfalseSet to true to honour the browser Do Not Track signal.
data-excludeComma-separated path prefixes that are never tracked.
data-debugfalseLog events to the console instead of sending them.

Methods

app.js
// Record something a person did.
attruly.track('Demo Booked', { plan: 'growth', seats: 12 })

// With money attached. Use a decimal amount, not cents.
attruly.track('Purchase', { orderId: 'A-1042' }, { revenue: 149.00, currency: 'USD' })

// Attach your own id. Everything this browser did before now merges into it.
attruly.identify('user_8412', { email: 'sam@acme.com', name: 'Sam Mokoena' })

// A manual pageview, when auto-pageview is off.
attruly.page({ section: 'pricing' })

// Forget this browser. Call on sign-out on a shared device.
attruly.reset()

What it captures automatically

  • Pageviews, including SPA route changes through the History API.
  • utm_source, utm_medium, utm_campaign, utm_term and utm_content.
  • Ad click ids: gclid, gbraid, wbraid, fbclid, msclkid, ttclid, li_fat_id. These are captured even when the campaign carried no UTM parameters at all, which is what makes a forgotten tracking template still attributable.
  • The referrer, unless it is one of your own domains.
  • Device, browser and operating system, parsed from the user agent.

Offline and closing tabs

Events queue in local storage and flush on reconnect. Every event carries a message id, so a retried delivery is stored once. On page hide the SDK falls back to sendBeacon, which is the only transport a browser guarantees to attempt while a tab is closing.

Server SDK

For the events a browser never sees: webhooks, backend purchases, refunds, CRM stage changes. These are the events that carry the money.

npm
npm install @attruly/node
server.ts
import { Attruly } from '@attruly/node'

const attruly = new Attruly({
  apiKey: process.env.ATTRULY_SECRET_KEY,
  // Buffer and send in batches. Call flush() before the process exits.
  batch: true,
})

// A Stripe webhook. Still attributed to the campaign that brought this
// person in, months earlier.
await attruly.track({
  userId: invoice.customer,
  event: 'Subscription Paid',
  revenue: invoice.amount_paid / 100,
  currency: invoice.currency.toUpperCase(),
  // Pass your own id so a retry is stored once.
  messageId: `invoice_${invoice.id}`,
})

await attruly.flush()

Link the browser session, or the conversion is unattributedImportant

A server event with only a userId we have never seen creates a new person with no browsing history, and the conversion is reported as having no source. Call identify at signup with the browser's anonymous id, or pass anonymousId alongside the userId. Read it in the browser with window.attruly.getAnonymousId().

Failure behaviour

Errors are reported through onError rather than thrown, so a checkout never fails because analytics is down. Transient failures are retried with exponential backoff and jitter. Set throwOnError: true to handle them yourself.

Identity

The hard part of attribution is not the arithmetic. It is knowing that the anonymous browser from March and the signed contract from May are the same person.

Attruly keeps an alias graph. Every identifier it has ever seen for a person — the browser's generated id, and your own user id — points at one profile.

  • Call identify() the moment you know who somebody is. Everything that browser did beforehand merges into them.
  • When the same person is identified on a second device, the two profiles merge. The identified profile survives, the earlier first touch is kept, and every conversion is re-credited in the background.
  • Reserved traits — email, name, phone — are promoted to searchable columns. Everything else is stored as JSON on the profile.

Goals and revenue

A goal turns a stream of events into the numerator of every rate in the product.

  • Event goals match on the event name, exactly or by pattern.
  • Pageview goals match on the path — /thank-you.
  • Revenue goals match any event carrying money.

Give a goal a default value so a lead form is worth something even though it never charges a card. When an event carries its own revenue, that wins.

Changing a goal does not rewrite history

Editing a matching rule affects new events only. An event that did not match yesterday was not a conversion yesterday, and silently rewriting last quarter would be worse than the inconsistency. Changing a goal's value does update its existing conversions, because that is a correction rather than a redefinition.

Funnels

A funnel is an ordered set of goals. Every segment is measured against all of its stages at once, which answers how far the people a campaign brought in actually got rather than how many of them converted once.

Create one from the Funnels screen once the project has at least two goals. A funnel takes between 2 and 8 steps. Each step points at a goal, can carry a shorter label for use inside this funnel, and can be marked as an outcome — the stage where money changes hands.

How the cohort is counted

  • A person enters the funnel the first time they ever convert on the first step. That entry fixes which reporting window they belong to; converting on the first step again later does not move them.
  • Later stages are counted as of now, or within the funnel's window of that person's entry. A cohort that entered this week has had less time to reach the last stage than one that entered three months ago, so a recent window always reads less converted than an older one.
  • Stages are ordered, not nested. Someone can reach the fourth stage without ever matching the third. The report shows that as a gain on the stage, with the count named, rather than clamping it down to the stage above.

The window

A funnel with no window measures every later stage as of now, which suits a signup-to-revenue journey that legitimately runs for months. Setting a window of 1 to 365 days restricts later stages to that many days after entry, which is what the question "did they finish checkout" needs — a purchase six weeks later is a different visit and a different intent.

Reading the report

The breakdown groups by any of the reporting dimensions, and each person's entry is credited to their first or last touch. The Funnels URL carries the whole selection — funnel, range, dimension and touch — so one view can be linked to directly.

Highest and lowest yield compare a segment's entry-to-stage rate against the whole cohort's rate at the same stage, and rank by how much traffic the gap covers. A segment with fewer than 10 entrants is never called good or bad, because a rate built on four signups is not a finding.

Editing a funnel does not rewrite history

A funnel is only an ordered list of goals, so changing one changes how existing conversions are read, never what they are. Reordering steps, renaming them or adding one takes effect on the next report, with nothing to backfill and no credit to recompute.

How attribution works

Every report takes a model. Switching model recomputes the whole page from the same underlying data.

Touches

A touch is recorded when a visit starts and carries an acquisition signal — a UTM parameter, an ad click id, or an external referrer. A visit with none of those records an explicit direct touch, which is what lets the product tell you honestly what share of your conversions it cannot attribute.

Reloading the same tagged landing page does not create a second touch. Only a genuinely different campaign fingerprint does.

Channels

Free-text sources are folded into 14 fixed channels, so Google / Paid, google / cpc and an untagged gclid all land in Paid Search.

Windows

A conversion looks back over the project's attribution window (90 days by default) and credits the touches inside it. Changing the window re-credits every conversion in the project, which happens in the background.

HTTP API

Every SDK is a thin wrapper over these three endpoints. You can call them directly.

Authentication

Send the key as a bearer token, in the X-Attruly-Key header, or as writeKey in the body. Public keys may only write events; secret keys may also read reports.

POST /api/v1/track

curl
curl -X POST https://www.attruly.com/api/v1/track \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -d '{
    "event": "Demo Booked",
    "userId": "user_8412",
    "properties": { "plan": "growth" },
    "revenue": 149.00,
    "currency": "USD",
    "messageId": "demo_8412_2026_06_15"
  }'

POST /api/v1/batch

Up to 100 events per request, under { "events": [...] }. This is what the browser SDK posts.

POST /api/v1/identify

Attaches your id to a person and merges their anonymous history into it. Takes userId, optionally anonymousId, and traits.

Responses

A successful call returns { "accepted": 1, "dropped": 0 }. Events deliberately not recorded — bot traffic, an excluded IP, an origin that is not on your allowlist, or an exhausted quota — come back as 200 with a reason, so your site never shows an error to its own visitors because of us. A malformed body or a bad key returns 400 or 401, because those mean a broken install.

Limits

AttributeDefaultWhat it does
Request body256 KBLarger requests are rejected.
Events per batch100Split larger batches.
Properties per event100Nested objects are not accepted.
Backdating7 daysOlder timestamps are clamped to the server clock.
Rate limit600 req/min per projectBatch requests: 120/min.

Privacy and compliance

Attruly is first-party by design. Your domain, your data, your database.

  • No cross-site tracking, no device fingerprinting, no third-party cookies.
  • The visitor id is randomly generated and first-party. It is meaningless on any other site, including other Attruly customers'.
  • IP addresses are used to resolve a country and apply your exclusion list, then discarded. The address is not stored on the visit.
  • The AI analysis reads aggregate figures only. No email address, name, IP address or individual profile is ever sent to a model provider.
  • Set data-respect-dnt="true" to honour the browser Do Not Track signal, and use data-auto-pageview="false" to gate tracking behind your own consent banner.

The full detail is in the privacy policy, including the sub-processor list and the data processing terms.