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