Attribution

Attribution & onsite tracking

Credit every sale to the marketing that earned it. Install the pixel, tell us who the visitor is, and send us the order — we join the ad click, the email, the browsing and the purchase into one journey.

How it fits together

Four things flow in, and they are independent — you can ship them in any order and each one improves the picture on its own.

The pixel

Reads UTMs and ad click IDs off the landing URL, and records browsing. This is where attribution actually comes from.

identify()

Turns an anonymous browser into a contact. Everything they did beforehand is attached to them at this moment.

Orders

The only thing that counts as revenue. Automatic on Shopify; one POST per sale everywhere else.

The rollup

Every 30 minutes, credits each paid order to the most recent marketing touch before it.

What we support

The pixel runs on any website. The difference between platforms is only ever how the order reaches us — we cannot see your sales unless something tells us about them.

ShopifyFully automatic

Everything automatic. Connect the store and orders, refunds and cancellations arrive by webhook. Add the pixel for browsing and attribution.

Next.js, Remix, Astro, Nuxt, Rails, Laravel, Django, WordPress — any stackPixel + a little code

Pixel works as-is: browsing, UTMs, ad click IDs and attribution are automatic. Orders need one server-side POST per sale (below).

Static sites, Webflow, Squarespace, FramerTracking only

Pixel works, so you get traffic sources and journeys. No orders unless something server-side can post them — a serverless function or your payment provider's webhook.

WooCommerce, Magento, BigCommerce, Stripe, PaddleNo connector

No direct connector today. The pixel works, and orders can be posted from your own backend using the events API.

Native mobile apps (iOS / Android)No connector

Not supported. The pixel is a browser script. Events can be posted server-side, but there is no in-app SDK and no anonymous stitching.

Shopify is the only turnkey commerce integration today

Everything else works, it simply needs your backend to post the order. If you use Stripe, that is a few lines inside the webhook handler you already have.

1. Install the pixel

Your public key is in Dashboard → Onsite Tracking. It is safe in page source: on its own it can only record browsing for contacts you already have.

html
<script async
  src="https://brainito.com/brainito-pixel.js"
  data-key="YOUR_PUBLIC_KEY"
  data-auto-pageview></script>

Put it before </head> — in theme.liquid on Shopify, or your root layout on Next.js. Optional attributes:

  • data-auto-pageview — send a page view on every page automatically.
  • data-capture-forms — read an email from forms the visitor submits, so you do not have to call identifyyourself. Only a field that looks like an email is read, and nothing else on the form.
  • data-anonymous — also track visitors before they identify, so a journey can include the ad click that brought them. Off by default. See consent.

Verify your domain

Until your domain is verified in Onsite Tracking (DNS record, meta tag or file), events from it may be rejected. Verification proves you control the domain, which is what stops someone else posting events with your key.

2. Identify your visitors

Nothing is attributed to a person until you tell us who they are. Call identify on login, on checkout, or when someone arrives from one of your emails.

javascript
// Call this as soon as you know who the visitor is.
brainito('identify', { email: 'jane@example.com' });

// Optional extras, merged rather than replaced:
brainito('identify', { email: 'jane@example.com', first_name: 'Jane' });

Call it as often as you like — later calls merge rather than replace, so an identify carrying only an email will not erase a name captured earlier. On logout, call brainito('reset'): it clears the identity, the browser id and the recorded campaign, so the next person to use that computer does not inherit the previous one's journey.

3. Track browsing

Optional, but it is what makes a journey readable and what powers Browse Abandonment. Five event names are accepted from the browser: viewed_product, added_to_cart, started_checkout, viewed_page, active_on_site.

javascript
brainito('track', 'viewed_product', {
  product_id: 'SKU-123',
  title: 'Blue Shoes',
  price: 49.0,
  url: window.location.href
});

brainito('track', 'added_to_cart', { product_id: 'SKU-123', quantity: 1 });

The pixel cannot record a purchase, by design

order_paid is not accepted from the browser. Your public key is visible in page source, so if the pixel could report revenue, anyone viewing source could invent it. Orders go server-side, with a secret key.

4. Send orders

Shopify users can skip this. Connect the store and orders, refunds and cancellations arrive automatically.

On any other stack, post the sale from your backend. Create an API key with the events.write scope in Dashboard → API Keys.

typescript
// Server-side only. Run this where the payment confirms —
// a Stripe webhook handler, your checkout route, a queue worker.
await fetch("https://brainito.com/api/events", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BRAINITO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    email: "jane@example.com",       // links the sale to the contact
    event: "order_paid",
    value: 84.00,                    // the amount paid
    properties: {
      order_id: "1234",              // REQUIRED — refunds match on this
      currency: "GBP",               // REQUIRED — omitted means skipped
    },
    dedup_key: "stripe:order_paid:1234",  // REQUIRED — retries double revenue
  }),
});

Three fields that fail silently

Each of these is accepted without an error and produces a wrong number later.
  • No currency — the sale is skipped entirely. Revenue goes missing rather than being guessed, because a dashboard labelling GBP as dollars is worse than one that is short a row.
  • No dedup_key — payment providers retry webhooks. One sale becomes two and revenue doubles.
  • No order_id in properties — refunds are matched to the original order on it. Without it refunds never reverse and revenue stays permanently overstated.

Refunds

Send refunded_order with the amount refunded, not the order total. A partial refund reverses only that amount and leaves the rest credited. We never edit the original — a refund is recorded as its own negative entry, so the history stays auditable.

typescript
await fetch("https://brainito.com/api/events", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BRAINITO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    email: "jane@example.com",
    event: "refunded_order",
    value: 10.00,                    // the REFUNDED amount, not the order total
    properties: {
      order_id: "1234",              // must match the original order
      currency: "GBP",
    },
    dedup_key: "stripe:refund:re_abc123",
  }),
});

Tracking visitors before they identify themselves is off by default and governed by a setting in Dashboard → Onsite Tracking:

  • Only with consent (default) — nothing anonymous is recorded until your banner calls brainito('consent', true).
  • Always — record without waiting for a signal. Choose this only if you have a lawful basis.
  • Never — identified visitors only.
javascript
// Call from your own cookie banner.
brainito('consent', true);   // visitor accepted analytics
brainito('consent', false);  // withdrawn — also clears what we hold

navigator.doNotTrack is honoured as a refusal in every mode, and is never treated as agreement. You are the data controller for your visitors; anonymous records carry no personal data and are deleted after 30 days.

How credit is decided

A sale is credited to the most recent marketing touch in the 90 days before it — last-touch attribution. A touch is an ad click, an email or SMS click, a social click, or a landing carrying UTM parameters.

When there is no touch to credit, we say which kind of nothing it was rather than calling it all “direct”:

Shown asMeans
DirectA real touch, and it genuinely was direct traffic.
No recent touchWe have touches for this person, none within 90 days.
UnattributedNo touch at all — an imported contact, or a sale from before tracking started.

Results refresh every 30 minutes. Revenue is never summed across currencies — you pick one, because a single figure adding GBP to USD looks authoritative and means nothing.

Known limits

Worth reading before you compare our numbers with anyone else's.

  • Cross-device is not linked. Someone who browses on a phone and buys on a laptop is two separate journeys. We do not guess a connection between them.
  • Safari clears local storage after about seven days of no return visit. Long consideration cycles on Safari will lose the earlier part of the journey. The same applies to private windows and cleared browser data.
  • Attribution is not backdated. Sales from before you installed the pixel show as Unattributed, because the journey that produced them was never recorded.
  • Ad blockers block the pixel, as they do every analytics script. Orders posted from your server are unaffected, which is another reason they go server-side.
  • One model. Last-touch only today. Multi-touch models are not available.

Go-live checklist

Pixel installed and your domain verified in Onsite Tracking
identify() fires for logged-in users and after checkout
Shopify connected — or your backend posts order_paid
Every order carries currency, order_id and a dedup_key
Refunds post refunded_order with the refunded amount
Consent posture chosen, and your banner calls consent() if required
One test purchase appears on that contact's Journey tab within 30 minutes

The last line is the one that matters. Open the contact you tested with, and you should see the ad or email that brought them, the pages they viewed, and the order marked with the channel that earned it.