Pixel API

Every attribute, option and field of p.js — including when window.proof does not exist, and how to queue calls that happen before the script loads.

p.js is a single self-executing script, served from https://yontosales.com/p.js. It has one job: recover the click identifier of a referred visitor, and expose a function you call when that visitor converts.

It has no dependencies, no build step, adds no global except window.proof, and never runs for a visitor a creator did not refer.

Installing#

Anywhere in <head> or <body>
<script async src="https://yontosales.com/p.js" data-proof="YOUR_TRACKING_CODE"></script>
ParameterTypeDescription
data-proofrequiredstringThe campaign's tracking code. Read from document.currentScript, so the attribute must be on the same <script> tag that loads p.js. Without it the script returns immediately and does nothing at all.
asyncboolean attributeOptional but recommended. p.js does no DOM work, so nothing waits on it — see queuing if your conversion can fire before it arrives.

The response is cached for 5 minutes (Cache-Control: public, max-age=300). That is deliberately short so a change ships quickly, but it does mean the file is re-fetched often rather than sitting in cache for a day.

How a visitor is recognised#

On load, the script looks for a click identifier in two places, in order:

  • ?ref= on the current URL — appended by the tracked link when it redirected the visitor to you;
  • otherwise the proof_ref cookie, written the first time this browser arrived with a ?ref.

When ?ref is present it is written to a first-party cookie on your domain, holding the click id and nothing else. No IP, no profile, no second key.

ParameterTypeDescription
Nameproof_refThe random click identifier.
Max-Age30 daysWritten only when a ?ref is actually present — that is, at the moment the visitor lands from the tracked link. It is not refreshed on later page views, so the window is a fixed 30 days from the click rather than a sliding one that never closes for a regular customer.
Path/Readable across your whole site.
SameSiteLaxSurvives the navigation from the creator's link to your store.
Secureon HTTPS onlyOmitted over plain http so local testing still works.
HttpOnlynoIt is set and read by JavaScript, so it cannot be.
Note.earlier versions stored this in localStorage, which has no expiry. If a browser still holds that value, p.js adopts it into the cookie once and then deletes the localStorage key — so nobody is left holding an identifier indefinitely. The migration's 30 days necessarily run from that visit, because there is no stored timestamp to date it from.

Non-referred visitors#

If no click id is found, the script stops: no network request, nothing stored, no identifier anywhere. Only visitors a creator actually referred are ever measured.

window.proof is still defined — as an inert function that accepts any arguments and does nothing. It is defined before anything else runs, so it exists even when data-proof is missing or the script hits an internal error.

Tip.this means an unguarded proof('conversion') on your thank-you page is safe for every visitor, however they arrived. You do not need a typeof check.

It was not always so. Until recently window.proof was defined only for referred visitors, so an unguarded call threw a ReferenceError on a merchant's page and took the rest of that script block with it — invisibly, because you test by clicking a tracked link, which makes you a referred visitor. If you wrote a guard against that, it is harmless and you can leave it:

No longer required, still fine
if (typeof proof === 'function') {
  proof('conversion', { value: 49.90, id: 'order-1234' });
}

proof('conversion', options)#

The first argument must be exactly the string 'conversion'. Any other command is ignored silently — there are no other commands.

js
proof('conversion', {
  value: 49.90,        // omit entirely for signup / lead campaigns
  currency: 'EUR',     // what the shopper actually PAID in
  id: 'order-1234'     // your order id, for idempotency
});
ParameterTypeDescription
valuenumberThe order total. Sent as order_value. Required for sale campaigns and ignored for signup/lead ones — see Conversion endpoint. Coerced with Number(), so "49.90" works and "£49.90" becomes NaN and is rejected by the server.
currencystringISO-4217 code of what the shopper actually paid in. Sent as currency. Omit it only on a single-currency site: without it the server falls back to the campaign's currency, which relabels a EUR sale as GBP on a GBP campaign.
idstringYour order identifier. Sent as external_order_id — note the option is id but the wire field is not. This is what makes the conversion idempotent; without it you get a 2-minute dedupe window instead.

What it sends#

A POST to /api/conversion with Content-Type: application/json and keepalive: true, so the request survives the page unloading immediately afterwards. The body always carries tracking_code and click_id; the three fields above are added only when you pass them.

Warning.the call is fire-and-forget. The promise is swallowed by an empty .catch(), the whole script is wrapped in a try/catch, and proof() returns undefined rather than a promise. You cannot await it, retry it, or find out that it failed. Check the campaign dashboard to confirm a conversion landed.

Calling before the script loads#

With async, a conversion on a fast page can fire before p.js arrives. The script drains a queue on load, so push to it from an inline stub:

Before the p.js tag
<script>
  // Must push to window.__proofQueue — that is the array p.js drains.
  window.__proofQueue = window.__proofQueue || [];
  window.proof = window.proof || function () {
    window.__proofQueue.push(arguments);
  };
</script>
Note.the array must be window.__proofQueue — that is the one p.js drains. An older comment in the p.js source showed a stub pushing to proof.q, which silently dropped every queued call; the comment and the code now agree on __proofQueue.

Queued calls are replayed with apply in the order they were pushed, once the real sender is installed — and therefore only for referred visitors. A queued call from a non-referred visitor is discarded rather than replayed, which is the correct outcome: there is nobody to attribute it to.

Single-page apps#

p.js runs once, on load. It reads ?ref from window.location.href at that moment, so a client-side route change that adds a ?ref later is not picked up. Make sure the first page load of the session is the one carrying the tracked link's parameter — which it will be if the visitor arrived by clicking the link.

window.proof persists for the life of the page, so later conversions in the same SPA session work without reloading.