docs
Open the app →

Goals on your own site

Conversions on a page you own — a lead form or a store — recorded without a network in the middle, and a live view answering "is it working yet".

A postback reports a sale on somebody else's server. A goal is the other half: a lead form, a checkout, an add to cart, on a page you control.

A goal is a conversion with a name and no network behind it, so everything here lands in the same conversions table the rest of the product reads. It shows up in the same reports, with the same statuses, alongside network conversions.

utmcap.goal('purchase')Page match · /thank-youIs there a click id?the visitor must have arrived through UTMCAPyesnoDeduplicatedper click, or per order id — your choice pergoalRefusedand Live says whyApprovedcounts as revenuePendinguntil it is confirmed
A conversion on a page you own. Two ways to fire one, one thing both need — a click this tracker issued — and a refusal you can watch arrive.

#Stores and lead capture

The Measurement page has three tabs, because the two things it measures work differently:

Tab The win What it takes to install
Stores An order, carrying its own value A signing key and a call from your own server
Lead capture A form submit, worth what you say it is One line of JavaScript on each lander
Live events — What arrived from either in the last hour, and why anything was refused

They are not the same instructions with a word changed. A shop signs an order carrying line items; a lead page has no order, no items and no total, and handing it a signing key would be telling somebody to build machinery for a thing they do not have.

Stores shows one shop at a time, chosen from the dropdown at the top. A store is a real site: a name, a domain, its own site key and its own goals. Two stores can both have a purchase and they stay separate — the store in the dropdown decides which one you are editing. The dropdown lists every store with how it is doing — last fired, a week's orders, or that its snippet has never been seen — and ends with Add a store. Beside it, the chosen store's health: how many of its goals have fired, and when the last event arrived. Under it: Goals, Install (the snippet with its key already in it, and the signing key) and Products (what sold, from signed orders).

Lead capture works the same way for lead pages: a set of lead goals — Lead and Registration to start with — and nothing else to set up. There is no domain or revenue rule, because a lead campaign's pages are the landers in its Funnel. The first lead page is made for you; add more with Add a lead page in the dropdown when two offers need different goals or values. A lead campaign converts on the default lead page unless you choose another on its Measurement tab, where the choice appears once there is more than one. Under the chosen page: Goals, with how many leads the buyer accepted; Where it fires; and Install.

Where it fires lists every lander the campaigns using that lead page sent clicks to in the last seven days, and whether the tag reported a page view from it. A lander with clicks and no page view has no tag on it — every lead on that page is lost, and this is the one place that says so.

Which goals a conversion is checked against is decided by the visitor's click: the campaign it came from names the store, or the lead goals, it converts on. The key in the tag only decides for a visitor no campaign sent.

#Lead capture

The win is a form submit, and it has two moments. They are different numbers.

a lead has two momentsForm submittedthe tag fires thegoalPendingworth the fixedamount you setBuyer decidesaccepted, or aduplicateApprovedtheir postback,real payoutThe gap between the two is your acceptance rate, on the campaign’s Measurement tab.Nothing is double-counted: revenue only ever comes from the approved one.
A lead has two moments and they are different numbers: what your page reported, and what the buyer accepted. Revenue only ever comes from the second.

The submission is recorded the instant the form fires, as pending, worth the fixed amount you set on the goal. That is the figure that tells you whether a landing page works.

<script>
  document.querySelector('#quote-form').addEventListener('submit', function () {
    utmcap.goal('lead');
  });
</script>

On submit, before anything navigates away.

The acceptance comes from whoever buys the lead: a duplicate, a bad number, out of area, or good and billable. That arrives as their postback and is recorded approved, with the real payout. Give them the postback URL from the network's own page — that is the half they fill in.

Nothing is double-counted. Reports only ever take revenue from the approved one, so the pending row costs nothing while it waits.

No signing key is involved. One exists so that a shop can state an order total a browser could otherwise forge. A lead is worth what you decided it is worth, and nothing on the page can revise that.

The goals offered for a lead site:

Key Worth Counted Records as
lead a fixed amount you set once per visitor pending
registration nothing once per visitor approved

lead is offered as the primary goal, because it is the one the campaign should be judged on. registration is a step rather than a sale — worth counting, worth nothing.

A store campaign has no offer and no lander. Its one path redirects to the store chosen on its Measurement tab, with the click id on the address as cid=. So its tracking link works like any other campaign's: paste it into the ad, the click is recorded, the visitor lands on the store, and the tag there reads cid and joins the visit to that click. The link is built on the tracking domain chosen on the campaign's Details tab.

Where a platform refuses redirects, use the direct link instead — the store's own address with the tracking parameters on it, which the tag reads as utm_id. On Google Ads and Microsoft Ads the ad uses the Final URL, and a tracking template is offered as well when the tracking domain is on the store's own site.

The path takes the store's domain when the campaign is saved. After changing a store's domain, save its campaigns again.

#Store event capture

The win is an order, and four events describe the funnel in front of it. They are named after the events the rest of the industry already uses — the same words GA4 and Meta's pixel use — because somebody instrumenting a checkout is usually pasting alongside one of those, and matching names is one less thing to get wrong.

Key Name Worth Counted Records as
view_content Product viewed nothing once per visitor approved
add_to_cart Add to cart nothing once per visitor approved
checkout Checkout started nothing once per visitor approved
purchase Purchase from the order once per order approved

The three steps are one line of JavaScript each, at the moment they happen:

utmcap.goal('view_content');
utmcap.goal('add_to_cart');
utmcap.goal('checkout');

purchase is the one that carries money, and money is where the two lanes diverge.

#Why an order is signed

the public lane — the site key is in your page sourceYour pagePOST /api/v1/goalCountedA value sent from hererecorded, and marked unverifiedOrder lines from heredropped — only a signed request carries themthe signed lane — your own server, holding the signing keyYour serverPOST + signatureTrustedThe amount is believed, and the order lines are kept
Both lanes record the conversion. Only the signed one is believed about money — the site key is in the page source of your own website, so anyone who views source can post with it.

The site key is public. It is in the page source of your own website and it has to be — the tag is served to a browser. Anyone who views source can post with it. That is fine for counting a step and not fine for stating an amount.

A value sent from a browser is recorded — refusing it would lose the conversion along with the figure — but it is marked unverified, and the screens say so. Order lines sent from a browser are dropped.

An order signed by your own server is trusted, and only a signed request may carry product lines.

#Posting an order from your server

Create the signing key on the Install view. It is shown once and never again: replace it if you lose it, and any server still signing with the old one stops being trusted immediately.

const crypto = require('crypto');

const body = JSON.stringify({
  k: 'YOUR_SITE_KEY',
  cid: clickId,            // the _utmcap_cid cookie, carried through your checkout
  goal: 'purchase',
  order: order.id,         // makes a refresh count once
  value: order.total,
  currency: 'USD',
  items: order.lines.map(l => ({
    sku: l.sku, name: l.title, category: l.category,
    quantity: l.qty, price: l.unitPrice, total: l.lineTotal,
  })),
});

await fetch('https://app.utmcap.com/api/v1/goal', {
  method: 'POST',
  headers: {
    'content-type': 'text/plain',
    'x-utmcap-signature': crypto.createHmac('sha256', SIGNING_KEY)
      .update(body).digest('hex'),
  },
  body,
});

The same shape in any language: an HMAC-SHA256 of the exact bytes you send, hex-encoded, in x-utmcap-signature. Sign the serialised body rather than rebuilding it — the same object serialised again with its keys in a different order is a different signature.

cid is the visitor's click id, and your checkout has to carry it through from the page they landed on. The tag keeps it in the _utmcap_cid cookie; how it reaches your server is your plumbing — a hidden field, a session value, an order attribute.

#What the order lines buy you

The items on a signed order feed What has been selling on the Install view: SKU, name, category, units, orders and revenue over the last 30 days. Unsigned orders carry no lines, so that table is only ever as complete as your signed lane.

#The two ways a goal fires

Kind Fires when
Event Your page calls utmcap.goal('…')
Page match The visitor reaches a path you name, such as /thank-you

Page match needs no code, which is why it is there — but it records whatever reaching that URL means, including somebody who bookmarked it. On a checkout worth optimising, use the event.

It is a prefix match on the path, because a thank-you page almost always carries an order id or a query string on the end, and an exact match would work when you tested it and not in production. The longest match wins, so /checkout/complete beats /checkout when both are defined.

A page match cannot count once per order — a URL states no order id — so the app refuses that combination when you save the goal, rather than accepting it and refusing every conversion later.

#Triggers: firing a goal without code

In Tag Manager → Triggers, a trigger fires one of your goals when something happens on the page, with no code to write. It always fires a goal, so the goal's own worth and counting rule still apply.

Trigger Fires when
Click Somebody presses the element you picked
Form submit A form on the page is sent
Comes into view The element scrolls onto the screen
Time on page The visitor has stayed a number of seconds, while still on the page
Leaves the page The visitor goes, at the moment they go, if they stayed a number of seconds first
About to leave The mouse heads for the top of the window: the tabs, the back button, the address bar

#Time on page, Leaves the page and About to leave

The first two both ask for a number of seconds, so they look alike. The difference is when they fire. With 30 seconds set:

The visitor… Time on page Leaves the page
stays 2 minutes, then closes the tab fires at 0:30, while they read fires at 2:00, as they close it
leaves after 10 seconds does not fire does not fire
stays and keeps the tab open fires at 0:30 fires only when they finally go
  • Time on page says they stayed. Use it for an engaged-visitor goal, or to fire a pixel for people worth retargeting.
  • Leaves the page says they read it, then went. On a landing page that is the honest sign it did not convince them. It works on phones too, because it watches the tab being closed or hidden.
  • About to leave is the moment somebody reaches for the address bar, the one to use for a last-chance message. It is desktop only: a phone has no mouse pointer, so it never fires there.

Give these their own goal, worth nothing and not the one the campaign is judged on. Pointed at your Lead goal, every visitor who leaves would count as a lead.

#Page event: telling your own page

A trigger records a goal. To make your page do something when it fires — open a popup, show an offer — add a Page event in Tag Manager → Pixels. Give it a name and pick the goal it follows.

When that goal fires, the tag sends the event to your page. Your own code listens for it:

window.addEventListener('utmcap:open-demo', () => openPopup());

The event also carries the goal and the click id, in event.detail.goal and event.detail.clickId. Leave the goal empty and it fires on every page view.

Nothing is sent anywhere else, and the goal is recorded first either way. It only fires on pages that have the tag.

#What a goal is worth

Value Records
None The conversion, no revenue
Fixed The same amount every time
From the order Whatever the caller passed in value

Fixed suits a lead worth a known amount. From the order suits a basket, and is the only one that makes revenue reporting mean anything on a store — and the only one where the signed lane matters, because it is the only one where the caller states the number.

#Counting the same thing twice

Two dedupe modes, and the right one depends on what you are counting.

Per click gives every firing of one goal on one click the same conversion id, so a registration reported on every page load stays one conversion.

Per order uses the order's own id, so a customer buying twice counts twice and a refreshed confirmation page counts once.

An order goal that arrives with no id is refused, not counted per click. Quietly collapsing two genuine orders into one is a worse answer than saying the call was incomplete.

#Approved or pending

Per goal, and it decides whether the conversion reaches revenue at all.

Record as pending when something confirms it later — a lead your buyer qualifies, an order that can be cancelled. It stays out of Conversions, Revenue, CR, EPC, ROI and Profit until something approves it, exactly as a network conversion does. See the figures, exactly.

Record as approved when reaching the page is the conversion.

Conversions stuck as pending are usually this setting rather than a fault: a goal recorded as pending stays pending until something approves it, and if nothing ever does — no postback, no network answer — it stays there. Check the goal's own setting first, then whether the postback that would approve it is arriving. See the four states.

#Which goal the campaign is judged on

One goal per site can be primary, and it is the one a campaign's conversion count and ROI are read from. A shop counting view_content, add_to_cart, checkout and purchase has four conversion counts, and only one of them is the business.

It is never chosen for you, including by the button that adds the common goals. Quietly deciding it is how a campaign's conversion count changes without anybody having picked anything.

#Naming a goal

Lowercase letters, digits and underscores, starting with a letter, up to 64 characters. Narrow on purpose: a key ends up in a snippet somebody types by hand, in a URL, and in a column built for a small set of values — and a key differing from another by a space or a capital is a goal that silently never fires.

What you type is normalised when the goal is created — Add To Cart becomes add_to_cart — and never when one fires. Correcting an inbound key would make a snippet with a typo appear to work, right up until somebody fixed the typo and the numbers moved.

#Is it working?

Live events shows what has arrived in the last hour and — the part that matters — why anything was refused. Each row says whether it came from a store or a lead page and which campaign sent the visitor, and the view filters to either, or to refusals only. It refreshes itself every few seconds while you are on it, because somebody watching that view is triggering an event by hand, and making them press a button is making them miss it.

Fire the goal on your own site, watch the row appear, and you are done. If it does not appear, the row that should have been there says what was wrong:

Refused What happened
no_click_id Nothing identified the visitor — no cid, and no cookie
bad_click_id The value is not a click id this tracker issues
no_such_click A well-formed id that was never recorded here
wrong_tenant That click belongs to another account
unknown_goal No active goal with that key among the store's or lead goals the click's campaign converts on
missing_order_id A per-order goal arrived with no order

The endpoint itself answers 204 and never a reason, whatever happened. It is called by a browser on your website, so a reason in the response would be a way for anybody to probe which click ids and goals exist. The reason is not lost — it is put where the person who needs it can see it, which is this view.

One visitor cannot report more than 60 conversions a minute: the limit is per site key and address, so a browser stuck in a loop cannot fill your reports. If the counter itself is unavailable the call is let through — losing a real conversion is the worse failure.

#What a goal cannot do

It cannot attribute a conversion to a click the tracker never issued. The visitor has to have arrived through UTMCAP — a tracking URL, parallel tracking, or a page carrying the tag that was recognised.

Someone who typed your URL directly, on a page with no tag, has no click id, and a goal firing for them has nothing to attach to. It is refused rather than recorded against nobody, and Live says so.

Tracking tips by email

Practical tips on tracking, attribution and ad costs, from the people who build UTMCAP.

Unsubscribe any time. Privacy policy