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.

#A site comes first

A site is one shop or one landing page: a name, a domain, its own site key, and its own goals. Nothing can be measured until there is one, because a goal belongs to a site rather than to the account.

Two sites can both have a purchase, and they stay separate — which is the whole reason the structure exists. It also means the site picker decides what you are editing: deleting a goal while looking at the second shop deletes that shop's goal, not the one with the same name on the first.

Each site has a kind, and it decides what the whole screen offers you:

Kind The win What it takes to install
Lead capture A form submit, worth what you say it is One line of JavaScript
Store event capture An order, carrying its own value A signing key and a call from your own server

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.

Three views, from the toolbar: Goals — what is defined, and whether it has fired lately. Install — the snippet, with your key already in it. Live events — what has arrived in the last hour, and why anything was refused.

#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.

#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.

#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.

#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 createdAdd 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. 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 on this site has that key
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.