Pixel SDK
The Pruuv Pixel is a small script for your website (about 16 KB, or 6 KB compressed). It saves the ad click IDs and UTM tags a visitor arrives with, records page views, and links the visitor to their email when they fill in a form. Pruuv uses this to match your leads to the ads that brought them, and to send better-matched conversions back to the ad platforms.
Which calls do you need?
- Your CRM sends leads to Pruuv (most set-ups): add the tag, and call
Pruuv.identify()when the visitor gives their email. You do not needtrack(). Your CRM must also send each lead’s UTM tags, because the pixel does not add them (see What the pixel saves from the URL). - Pruuv gets no leads from your CRM: also call
Pruuv.track()when the conversion happens. - You only want page views: the tag is enough.
Quick start
1. Add the tag to every page
Copy the tag from Settings → Pixel SDK in Pruuv. It already contains your site key. Only agency users can open this page. If it shows Enable web pixel, an agency admin must click it first. If you cannot open the page, ask your Pruuv contact for the tag. Paste the tag just before the closing </body> tag on every page, and always on the landing pages your ads link to:
<script async src="https://ai.pruuv.com/pixel.min.js"
data-site-key="YOUR_SITE_KEY"
onload="Pruuv.page()"></script>data-site-keyis your site key. It is public, so it is safe in your page.onload="Pruuv.page()"records a page view once the script has loaded. The script isasync, soPruuvdoes not exist before then. Remove the attribute if you do not want page views.- Load the script from
https://ai.pruuv.com. The pixel sends its data to the host it was loaded from.
If your Content-Security-Policy blocks inline code, load the pixel from your own script file instead. Your policy must allow https://ai.pruuv.com in both script-src and connect-src.
var script = document.createElement('script');
script.src = 'https://ai.pruuv.com/pixel.min.js';
script.async = true;
script.dataset.siteKey = 'YOUR_SITE_KEY';
script.onload = function () { window.Pruuv.page(); };
document.head.appendChild(script);2. List your domains
In Settings → Pixel SDK (agency admins only), add every domain the tag runs on under Allowed domains (one per line), then click Save domains. A domain also covers its subdomains: example.com allows www.example.com and apply.example.com. The same list applies to track().
identify() calls from any site that uses your site key, and track() calls from any site that has your stage token.3. Identify the visitor
Call Pruuv.identify() as soon as you know the visitor’s email, usually in your form’s submit handler. The first argument must be the email. If you know your CRM’s ID for this lead, pass it as reference.
Pruuv.identify('jane@example.com', {
reference: 'crm-contact-12345', // your CRM's ID for this lead (recommended)
phone: '+441234567890' // optional: used by later track() calls
});- Call it before your CRM sends the lead to Pruuv, so the click IDs are ready when the lead arrives. Do not wait for your CRM’s reply.
- The lead your CRM sends must carry the same email, the same reference or the visitor ID. That is how Pruuv links them: see How Pruuv links the pixel to your CRM.
- It needs the site key. Without one, it logs a warning and sends nothing.
4. Send the conversion from the browser (optional)
You only need this when your CRM does not send the lead to Pruuv. Call Pruuv.track() when the conversion happens, for example after your form is sent:
Pruuv.track('YOUR_STAGE_ID', 'YOUR_STAGE_TOKEN', {
reference: 'crm-contact-12345',
value: 250,
currency: 'GBP'
});Each funnel stage has its own stage ID and token. To report a later stage, call track() again with that stage’s pair.
Where to find your keys
| Value | Where to find it | Is it secret? |
|---|---|---|
| Site key | Settings → Pixel SDK (agency users only). Click Show, then Copy key — or copy the whole tag. | No. It is public by design. Your Allowed domains list is what protects it. |
| Stage ID | Settings → Funnel & Data source → Funnel. Open the stage: the ID is at the top of the panel, and at the end of the stage’s webhook URL. | Not a password, but keep it private: a server can post events to a stage with only its ID (see Webhook API). |
| Stage token | The same stage panel, under Webhook endpoint. The app shows only its first 8 characters, so ask your Pruuv contact for the full token. | Not really. It sits in your page’s code, where any visitor can read it. Your Allowed domains list is what protects browser requests. |
Tag settings
Set these as attributes on the tag, or in window.PruuvConfig before the script loads. When both set a value, the tag attribute wins. For consent mode, either one turns it on. Add the tag only once per page.
| Tag attribute | window.PruuvConfig | What it does |
|---|---|---|
| data-site-key | siteKey | Your public site key. page() and identify() need it. track() does not. |
| data-consent="required" | requireConsent: true | Turns on consent mode: nothing is saved or sent until you call Pruuv.consent(true). |
| data-api-host | apiHost | Where the pixel sends its data, for example https://ai.pruuv.com. By default this is the host the script was loaded from, so set it only if you host pixel.min.js yourself. |
<script>
window.PruuvConfig = { siteKey: 'YOUR_SITE_KEY', requireConsent: true };
</script>
<script async src="https://ai.pruuv.com/pixel.min.js"></script>API reference
The script adds one object, window.Pruuv, with seven methods. It only exists once the script has loaded, so write window.Pruuv?.identify(…) in code that might run earlier. The methods catch network errors and never report the server’s answer, so check it in the Network tab (see Troubleshooting).
Pruuv.page()
Records a page view. It takes no arguments and needs the site key. In a single-page app, call it after each route change (see Single-page apps). Each call sends:
| Field | Where it comes from |
|---|---|
| host, path | The page’s host name, and its path including the query string. |
| referrer | document.referrer, when there is one. |
| anonymous_id | The visitor ID in the _pruuv_vid cookie. |
| session_id | The session ID in the _pruuv_sid cookie. |
| utm_source, utm_medium, utm_campaign, utm_content, utm_term | The last-touch UTM tags saved in _pruuv_session. |
| gclid, wbraid, gbraid, ttclid, msclkid, li_fat_id, pruuv_aid, fbc, fbp | The saved click IDs, so a page view carries its ad source. |
| user_agent, screen_width, screen_height, viewport_width, viewport_height, device_pixel_ratio | The browser, and its screen and window size. |
| occurred_at | The time of the call, from the browser. |
| ip_address | Added by Pruuv from the request. Never read from the page. |
| geo_city, geo_region, geo_country | Added by Pruuv from the CDN’s IP lookup. Used only to fill the city, state or country your CRM left empty, for Meta’s match keys, and always hashed before it is sent. |
Page views go to /api/pixel/collect. Pruuv uses them for the checks in Data Health → Pixel. For visitors you identify, it also uses them to fill in the click IDs, browser details, location and page URL their leads are missing. Page views themselves are never sent to the ad platforms.
Pruuv.identify(email, options)
| Parameter | Type | What it does |
|---|---|---|
| string (required) | The visitor’s email address. Lowercased, trimmed and hashed (SHA-256) before it is sent. Never pass a CRM ID here. | |
| options.reference | string | Your CRM’s ID for this person. Sent lowercased and trimmed, so a lead that arrives with this ID instead of an email still matches. Recommended. If identify() runs after your CRM sends the lead, this ID only matches when your CRM sends it in lowercase, with no spaces around it. |
| options.phone | string | Not sent by identify(). Kept for later track() calls, which hash it. Use E.164 format, for example +441234567890. |
| options.event_id | string | The event ID your pixel used for this lead: the eventID you passed to fbq(). Pruuv sends it to Meta and TikTok as the lead’s event ID, so they count the lead once instead of twice. Sent exactly as given. identify() only covers the first stage of your funnel; for a later event, send event_id with track() or your webhook post. See Stop Meta and TikTok counting an event twice. |
| options.<any other key> | string | Kept in the _pruuv_identity cookie only. Never sent. |
identify() sends the email hash, the reference and the event ID, together with the click IDs saved in this browser, the first-touch data and the browser’s user agent. Pruuv adds the IP address and approximate location. It keeps one record per visitor ID (the _pruuv_vid cookie): a later call updates the values it carries, but never clears a click ID saved earlier.
It also saves the email and options in the _pruuv_identity cookie, so later track() calls can use them. It returns nothing, so do not await it. Call it once per form, not on every page.
Pruuv.track(stageId, token, options)
| Parameter | Type | What it does |
|---|---|---|
| stageId | string (required) | The ID of the funnel stage this conversion belongs to. |
| token | string (required) | That stage’s webhook token. A token only works for its own stage. |
| options.reference | string | Your ID for this conversion, such as a CRM contact or deal ID. See “How the reference is chosen” below. |
| options.order_id | string | Used as the reference when reference is not set. |
| options.item_reference | string | A second ID, such as an order or claim ID. Required when the stage has Item reference turned on. |
| options.event_id | string | The event ID your pixel fired this event with, for example the eventID you passed to fbq(). Pruuv sends it to Meta and TikTok as the event ID, so they count the event once. Sent exactly as given, up to 256 characters. Use a new ID for every event. See Stop Meta and TikTok counting an event twice. |
| options.email | string | Lowercased, trimmed and hashed (SHA-256), then sent as email_hash. Defaults to the email given to identify(). |
| options.phone | string | Spaces removed, then hashed (SHA-256) and sent as phone_hash. Defaults to the phone given to identify(). Use E.164 format. |
| options.value | number | The conversion value, sent as revenue_value. Must be 0 or more. |
| options.currency | string | A 3-letter ISO 4217 code, such as GBP. Any other length is rejected. |
| options.event_name | string | A label stored with the event, such as Lead or Sale. |
| options.<any other key> | string | number | boolean | Sent as you give it, and replaces any value the pixel captured under the same name (for example gclid). Fields the Webhook API knows are handled as described there. Keys declared as custom attributes are stored on the event. Other keys are kept only in the stored copy of the request. |
Along with your options, track() sends the saved UTM tags (including utm_matchtype), the click IDs and click time, the first-touch data when it differs, the browser’s user agent, and the page URL without its query string (event_source_url). It also sends identity_user_id: the reference given to identify(), or the email when there was no reference.
How the reference is chosen. track() uses the first of these that is set: options.reference → options.order_id → the reference given to identify() → the email given to identify() → pruuv_<timestamp>.
pruuv_… reference, so your CRM cannot match it and repeated calls are not caught as duplicates.Duplicates. Pruuv keeps one event per stage and reference (and item_reference, when set). This is permanent, not a time window. A repeat call returns duplicate: true. It never creates a second event. It sends nothing itself, and never re-sends a conversion that was already sent. It only fills in fields the stored event is missing, such as a click ID, email hash, browser details, value or currency. It never adds UTM tags, so it cannot get the event matched to an ad. If the event has not been sent yet, the new fields go with it when it is. Give each real conversion its own reference, or use item_reference for a second order from the same person.
Responses. track() posts to /api/webhooks/funnel/{stageId}. The pixel does not read the answer, so check it in the Network tab or in Data Health → Live:
| Status | Body | Meaning |
|---|---|---|
| 200 | { received: true, event_id, status: "accepted" } | Stored as a new event. |
| 200 | { received: true, duplicate: true, status: "duplicate" } | This stage already has an event with this reference. Nothing changed. |
| 200 | { received: true, duplicate: true, status: "enriched" } | Already stored. Pruuv filled in fields the stored event was missing. If it was already sent, it is not sent again. If not, the new fields go with it when it is sent. |
| 400 | { error: "Validation failed", details } | The body is invalid: for example, a currency that is not 3 letters, or no item_reference on a stage that needs one. |
| 401 | { error: "Unauthorized" } | The token is missing, or it is not this stage’s token. |
| 403 | { error: "Forbidden origin" } | The page’s domain is not in Allowed domains, or the pixel is disabled. |
| 404 | { error: "Stage not found" } | No stage has this ID. Pruuv logs nothing, so you only see this in the Network tab. |
| 429 | { error: "Rate limit exceeded" } | This stage has reached its limit of 1,000,000 requests an hour. |
| 500 | { error: "Internal error" } | Pruuv could not save the event, and it is lost. It does not show in Data Health → Live, so you only see it in the Network tab. One cause: a pruuv_aid in the URL that Pruuv did not give you. |
Pruuv.visitorId()
Returns the visitor ID, the value of the _pruuv_vid cookie, and creates the cookie if it does not exist yet. It creates no other cookie. It returns null before consent (in consent mode), for a bot, and on a page that is not served over https, where the browser cannot make the ID.
Put it in a hidden field of your form, and have your CRM send it with the lead as pruuv_visitor_id (see Webhook API). Pruuv then finds this browser’s click IDs even when the lead’s email differs from the one identify() saw.
Pruuv.consent(), Pruuv.optOut() and Pruuv.reset()
| Call | What it does |
|---|---|
| Pruuv.consent(true) | Saves the visitor’s consent for 90 days, saves the click data held from this page, and sends the waiting calls. Does nothing if the browser has Do Not Track turned on. |
| Pruuv.consent(false) | Saves the refusal for 90 days and drops the waiting calls. Cookies that already exist are kept. |
| Pruuv.optOut() | Saves the refusal, drops the waiting calls, and deletes the other seven cookies: _pruuv_session, _pruuv_first_touch, _pruuv_identity, _pruuv_vid, _pruuv_sid, _fbp and _fbc, and their saved copies. An _fbp or _fbc that Meta’s own pixel set for your whole domain is not deleted. |
| Pruuv.reset() | Forgets the identify() data and deletes the _pruuv_identity cookie. Use it when a user logs out. Click IDs and the visitor ID stay. |
Examples
A form whose leads your CRM sends to Pruuv
<form id="lead-form">
<input name="email" type="email" required>
<button type="submit">Get a quote</button>
</form>
<script>
document.getElementById('lead-form').addEventListener('submit', function () {
var email = this.querySelector('[name="email"]').value;
// Your CRM sends this lead to Pruuv with the same email
if (window.Pruuv) window.Pruuv.identify(email);
});
</script>If the page knows your CRM’s ID for the lead, pass it too: identify(email, { reference: leadId }).
A form that also reports the conversion
async function handleSubmit(data) {
window.Pruuv?.identify(data.email, { reference: data.leadId });
const result = await submitForm(data);
if (result.ok) {
window.Pruuv?.track('YOUR_STAGE_ID', 'YOUR_STAGE_TOKEN', {
reference: data.leadId,
value: 250,
currency: 'GBP'
});
}
}Stop Meta and TikTok counting an event twice
If you also run the Meta pixel (or the TikTok pixel) and fire an event on your site, the platform gets it twice: once from your pixel and once from Pruuv. Give both the same event ID and the platform keeps only one (Meta’s guide, TikTok’s guide):
function handleSubmit(email) {
var eventId = 'lead-' + crypto.randomUUID();
fbq('track', 'Lead', {}, { eventID: eventId });
window.Pruuv?.identify(email, { event_id: eventId });
}- The event names must match too. The platform only merges two events with the same event ID and the same name. Your pixel’s event (
Leadabove) must be the event that stage is mapped to in Pruuv. - Use a new event ID for every event.
identify()carries the ID of the lead itself (your first stage). For a later event, such as a signature, fire your pixel with a new ID and send that ID with the same event:Pruuv.track(stageId, token, { event_id: eventId }), orevent_idin your CRM’s webhook post for that stage. - One ID belongs to one lead. If the same ID reaches Pruuv on several events, only the earliest one sends it. The others go with Pruuv’s own ID.
- Claims and other items. If your site fires one event per claim with the claim number as its event ID, set that destination’s Event ID to Item reference in Pruuv. Pruuv then sends each event’s
item_referenceas the event ID. The text must be exactly the same. Two stages that use Item reference on one ad account need different event names. - Send the ID before Pruuv sends the event. The platform merges the two only within 48 hours, and Pruuv can only use an event ID that reaches it before the event is sent. So call
identify()in the submit handler, or sendevent_idin the same request as the event. - Pruuv never creates an event ID for you. Without one, events go with Pruuv’s own ID, as before.
Single-page apps
A route change inside a single-page app does not reload the page, so the tag’s onload fires only once. Call Pruuv.page() from your router after each route change. The pixel already re-reads the URL for UTM tags and click IDs on pushState, replaceState, popstate and hashchange. For the Next.js App Router:
'use client'
import { usePathname, useSearchParams } from 'next/navigation'
import { useEffect, useRef } from 'react'
export function usePruuvPageViews() {
const pathname = usePathname()
const searchParams = useSearchParams()
const isFirstPage = useRef(true)
useEffect(() => {
// The tag's onload already recorded the first page
if (isFirstPage.current) {
isFirstPage.current = false
return
}
window.Pruuv?.page()
}, [pathname, searchParams])
}Google Tag Manager
Paste the tag, exactly as copied from Pruuv, into a Custom HTML tag that fires on all pages. Then add a second Custom HTML tag for identify(), fired by your form-submit trigger:
<script>
if (window.Pruuv) {
window.Pruuv.identify('{{DLV - email}}', { reference: '{{DLV - lead_id}}' });
}
</script>How Pruuv links the pixel to your CRM
- A visitor clicks an ad and lands on your site. The pixel saves the click IDs.
- They fill in your form.
identify()sends their email hash and your reference to Pruuv, with the click IDs. - Your CRM sends the lead to Pruuv with the same email or reference, and with the lead’s UTM tags (see Webhook API).
- When the lead carries a visitor ID (
pruuv_visitor_id, see Pruuv.visitorId()), Pruuv tries that browser first. It skips the browser if it was last identified with another email, as on a shared computer. Then Pruuv finds the visitor by email hash, then by reference. It adds the click IDs, browser details and location the lead is missing. It never overwrites what your CRM sent.
This also works across devices. If the visitor clicked the ad on their phone and identify() ran there, a lead with the same email gets the phone’s click ID.
Pruuv does this when the lead arrives, and again every 30 minutes for leads up to 7 days old. So a late identify() still counts, if the lead has the same email, the same reference in lowercase, or the browser’s visitor ID. But Meta and TikTok may already have received the conversion without the click ID, which gives a weaker match. Call identify() first.
What the pixel saves from the URL
On every page load, and on every route change inside the page, the pixel reads the URL. It saves the UTM tags utm_source, utm_medium, utm_campaign, utm_content, utm_term and utm_matchtype, and sends them with track() calls (see Attribution). It does not add them to the leads your CRM sends. It also saves these click IDs:
| URL parameter | Platform | Saved in | What Pruuv uses it for |
|---|---|---|---|
| gclid, wbraid, gbraid | Google Ads | _pruuv_session | Matching the conversions Pruuv sends to Google Ads. |
| fbclid | Meta | _fbc, as fb.1.<time>.<fbclid> | Matching the conversions Pruuv sends to Meta’s Conversions API. |
| — (no URL parameter) | Meta | _fbp | Meta’s browser ID, used for matching. The pixel creates it, or reuses one that Meta’s own pixel set. |
| ttclid | TikTok | _pruuv_session | Matching the conversions Pruuv sends to TikTok’s Events API. |
| li_fat_id | _pruuv_session | Stored only. Sending conversions to LinkedIn is not available yet. | |
| msclkid | Microsoft Ads | _pruuv_session | Stored only. Pruuv does not send conversions to Microsoft Ads. |
| pruuv_aid | Pruuv | _pruuv_session | Stored only. Attribution does not use it yet. Use only a pruuv_aid that Pruuv gave you. Any other value makes that visitor’s track() calls fail, and stops Pruuv adding their click IDs and browser details to your leads. |
Click IDs are only in the URL of the first page after the ad click. So the tag must be on your landing pages, not only on the thank-you page.
utm_source must name the platform. For Meta and TikTok, the tags must carry the ad ID. For Google Search, the ad group ID. For Performance Max and other campaign-level Google types, the campaign ID and a utm_medium that names the type. A click ID, email or phone alone is not enough. If your CRM sends the lead, it must send these tags. Attribution lists the tags for each platform; Conversion sending has the full rules.First touch and last touch
| Signal | Cookie | How it behaves |
|---|---|---|
| Last touch | _pruuv_session | Replaced by the newest landing URL that carries any UTM tag or click ID. Values the new URL does not carry are dropped. Sent with every track() call. |
| First touch | _pruuv_first_touch | The first landing that carried a UTM tag or click ID. Written once and never changed. Sent as first_touch with track() and identify() when it differs from the last touch. |
Conversions use the last touch. The first touch is stored with the event.
Cookies
All cookies are first-party. The pixel sets them with JavaScript on the page’s own host (with no Domain attribute), with path=/ and SameSite=Lax.
| Cookie | What it holds | Lifetime asked for |
|---|---|---|
| _pruuv_session | The last-touch UTM tags and click IDs, and when they were seen. | 90 days |
| _pruuv_first_touch | The same data from the first tracked visit. Written once. | 90 days |
| _pruuv_identity | The email and options given to identify(), in plain text. | 90 days |
| _pruuv_consent | granted or denied. Written only by consent() or optOut(). | 90 days |
| _pruuv_vid | A random visitor ID. Pruuv.visitorId() returns it. | 90 days |
| _pruuv_sid | A random session ID. | Until the browser closes |
| _fbp | Meta’s browser ID. Reused if one already exists. | 90 days |
| _fbc | Meta’s click ID, built from fbclid. | 90 days |
_pruuv_vid, _pruuv_sid and _fbp are created by the first page(), identify() or track() call, not when the script loads. visitorId() creates _pruuv_vid only.
identify() already sent to Pruuv are not affected.Saved copies in localStorage
The pixel also saves a copy of _pruuv_session, _pruuv_first_touch, _pruuv_vid, _fbp and _fbc in the browser’s localStorage, under keys that start with _pruuv_mirror.. Each copy has the same expiry as its cookie. When one of those cookies is missing on page load and its copy has not expired, the pixel writes the cookie back from the copy, keeping the original expiry.
This keeps click IDs for visitors who convert a few days after the click. Safari deletes localStorage only after 7 days pass with no visit to the site, so each visit restarts that clock. A visitor who comes back after a longer gap has no saved click IDs.
- A cookie that exists always wins. If Meta’s own pixel has written a newer
_fbcor_fbp, the copy is updated to match it. - The copies are written and restored only when consent allows, and
optOut()deletes them. _pruuv_identity,_pruuv_sidand_pruuv_consentare never copied.
Cookies and localStorage both belong to one host. Data saved on www.example.com is not visible on apply.example.com, so a visitor who lands on one host and converts on another has no click IDs on the second.
Consent mode
By default the pixel does not wait for consent: it writes cookies and sends calls straight away. The tag you copy from Pruuv does not turn consent mode on. If the browser sends Do Not Track, the pixel writes no cookies and sends nothing, even after consent(true).
To wait for consent, add data-consent="required" to the tag (or set requireConsent: true). Then:
- Nothing is written or sent until you call
Pruuv.consent(true). Until then, the page’s click data and anypage(),identify()andtrack()calls wait in memory. - If the visitor opens another page before accepting, the waiting data is lost, including the landing page’s click IDs.
- The choice is saved in
_pruuv_consentfor 90 days and applies to later pages.
// The visitor accepts Pruuv.consent(true); // The visitor declines Pruuv.consent(false); // The visitor withdraws consent later: this also deletes the pixel's cookies Pruuv.optOut();
Security and limits
- The site key is public. Your Allowed domains list is what stops other websites from using it.
- A
track()call from a browser must carry the stage token and come from an allowed domain. The token is visible in your page, so the domain list is the real protection. - Requests from a server (with no
Originheader) need no token: see Webhook API → Authentication. Keep your stage IDs private. - Rotate site key (in Settings → Pixel SDK, agency admins only) stops
page()andidentify()at once on every tag that uses the old key. Update your tags straight after. It does not stoptrack(), which uses the stage token. To stop a stage’s browsertrack()calls, an agency admin can click Rotate secret on that stage. Server posts need no token, so they keep working. - Disable pixel (agency admins only) stops page views,
identify()calls and browsertrack()calls. - Each site key and each stage can receive 1,000,000 requests an hour. This is a safety limit, not a quota you should expect to reach.
Privacy
identify()andtrack()hash the email and phone (SHA-256) in the browser before sending them.- The
_pruuv_identitycookie keeps the email andidentify()options in plain text in the visitor’s browser for up to 90 days. Other scripts on your page can read it. - If you pass no reference, the email becomes the event’s reference and is sent and stored in plain text. Pass a reference to avoid this.
- Other fields you pass to
track()are sent as you give them. Pruuv hashes the personal fields it knows (see Webhook API), but keeps a copy of each request. - Page views store the visitor’s IP address, user agent, approximate location and the full page URL, including its query string. Do not put personal data in your URLs.
- The pixel only talks to Pruuv. It loads no other scripts and sets only first-party cookies. Pruuv sends a conversion only to the connected ad account the lead was matched to (see Conversion sending).
Browser support
- The page must use HTTPS (or run on
localhost). On plain HTTP, browsers block the crypto functions the pixel needs, so nothing is sent. - It needs a recent browser. It uses
crypto.randomUUID(), which arrived in Chrome and Edge 92, Firefox 95 and Safari 15.4. - It ignores bots and automated browsers. Playwright, Puppeteer and Selenium set
navigator.webdriver, so nothing is sent from them. Test in a normal browser window.
Troubleshooting
Check the requests
Open your browser’s developer tools, go to the Network tab and load the page. Look for these requests to https://ai.pruuv.com:
| Request | Sent by | Success |
|---|---|---|
| POST /api/pixel/collect | Pruuv.page() | 200 { received: true } |
| POST /api/pixel/identify | Pruuv.identify() | 200 { received: true } |
| POST /api/webhooks/funnel/<stage ID> | Pruuv.track() | 200, with status "accepted", "duplicate" or "enriched" |
Errors from page views and identify():
| Status | Body | Meaning |
|---|---|---|
| 400 | { error: "Validation failed", details } | The request is invalid. For identify(), this is usually a missing visitor ID because the browser blocks cookies. A site key in the wrong format, such as the YOUR_SITE_KEY placeholder, also gives 400. |
| 403 | { error: "Forbidden origin" } | The page’s host is not in Allowed domains. Subdomains of a listed domain are allowed. |
| 404 | { error: "Unknown or inactive site" } | The site key is wrong or was rotated, or the pixel is disabled. |
| 429 | { error: "Rate limit exceeded" } | This site key has reached its limit of 1,000,000 page views (or identified visitors) an hour. |
| 500 | { error: "Internal error" } | Pruuv could not save the request. |
For track() errors, see the responses under Pruuv.track().
No request at all
- The Console shows
[Pruuv] siteKey is required for page()(or foridentify()): the tag has nodata-site-key. - The Console shows
[Pruuv] stageId and token are required for track(): an argument is missing. - The Console shows
Pruuv is not defined: the call ran before the script loaded, or the script did not load. Usewindow.Pruuv?.and check the script URL. - The browser has Do Not Track turned on, or the visitor declined consent.
- Consent mode is on and
Pruuv.consent(true)has not been called yet. - The browser is automated, or looks like a bot.
- The page uses plain HTTP. The Console shows an error about
randomUUIDordigest.
Check in Pruuv
- Data Health → Pixel shows when each site last sent a page view, and how many CRM leads were linked to an
identify()call. - Data Health → Live shows
track()requests as they arrive, including most rejected ones (wrong token, domain not allowed, invalid body). - Page-view and
identify()errors are not shown in Pruuv. Use the Network tab.
Leads arrive without click IDs
- The tag must be on the landing pages your ads link to.
- Call
identify()on every form, before your CRM sends the lead, and make sure the lead carries the same email or reference. - The visitor moved to another host (cookies belong to one host), or came back after Safari removed the cookies.
- In consent mode, the visitor opened another page before accepting.
Low Meta match rate
- Call
identify()before your CRM sends the lead, so the lead gets thefbc,fbp, IP address and user agent. - Send the phone number with each lead: from your CRM, or in
track(). Use E.164 format. - Make sure the tag is on your landing pages, so
fbclidis saved as_fbc.