PruuvDocs

Webhook API

If you prefer to send conversion events from your server rather than the browser, you can POST directly to the Pruuv webhook endpoint. This is useful when you already have lead data in a CRM or backend system and want to forward it to Pruuv without adding JavaScript to your pages.

Important: Server-side integrations cannot automatically capture fbc / fbp (Meta cookies) or ttclid (TikTok click ID) — these can only be read from the browser at the moment of the ad click. For best Meta and TikTok match rates, install the Pixel SDK on your landing pages and call Pruuv.identify()when the visitor fills in your form. When your server then sends the lead with the same email or reference, or with the browser's visitor ID, Pruuv adds the browser's click IDs to it.

Authentication

Pruuv checks the token only on requests that come from a browser — any request with an Origin header, such as a Pruuv.track()call. Those must send the stage's token as Authorization: Bearer …, and must come from a domain in the Allowed domains list under Settings → Pixel SDK (when the list is not empty).

Requests from a server have no Origin header. Pruuv accepts them without checking a token: the stage ID in the URL is all they need, and only the hourly rate limit applies. So treat your stage IDs as private. Sending the token from your server anyway is fine.

Each stage has its own token. Find it in Settings → Funnel & Data source → Funnel: open the stage and look under Webhook endpoint. The app shows only its first 8 characters, so ask your Pruuv contact for the full token.

Endpoint

bash
POST https://ai.pruuv.com/api/webhooks/funnel/{stageId}
Content-Type: application/json
Authorization: Bearer YOUR_WEBHOOK_TOKEN    # checked on browser requests only

Example request

bash
curl -X POST https://ai.pruuv.com/api/webhooks/funnel/YOUR_STAGE_ID \
  -H "Authorization: Bearer YOUR_WEBHOOK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "reference":    "CRM-REF-001",
    "utm_source":   "google",
    "utm_medium":   "cpc",
    "utm_campaign": "brand-search",
    "gclid":        "EAIaIQobCh...",
    "revenue_value": 250,
    "currency":     "GBP"
  }'

Payload reference

All fields are optional except reference. An optional field may be omitted, sent as null, or sent as an empty string — all three mean “not provided” and are stored as no value. reference itself must be a non-blank string.

FieldTypeDescription
referencestringRequired. Unique identifier for this contact or conversion — CRM contact ID, deal ID, etc. Used for deduplication and enrichment merge.
item_referencestringSecondary reference — deal ID, claim number, order ID. Required for stages that enforce item-level dedup.
event_idstringThe event ID your browser 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 (a longer one is ignored and the event is still stored). Use a new ID for every event. Not the same as the event_id in the response, which is Pruuv’s own ID.
event_namestringLogical event label stored against the event — e.g. "Lead", "Sale", "Appointment".
utm_sourcestringUTM source parameter
utm_mediumstringUTM medium parameter
utm_campaignstringUTM campaign parameter
utm_contentstringUTM content parameter — used for ad-level attribution join (stores ad ID).
utm_termstringUTM term parameter — stores keyword for Google Search / Bing keyword-level attribution.
utm_matchtypestringKeyword match type — the {matchtype} ValueTrack code: b (broad), p (phrase), e (exact), a (AI Max). Captured by the Pruuv Pixel. Drives the match-type breakdown; AI Max (a) is shown on its own axis.
gclidstringGoogle click ID — what Google Conversion Upload matches on (for a lead dated from 22 Oct 2026, a hashed email or phone can stand in)
wbraidstringGoogle web-to-app click ID (iOS, privacy-preserving)
gbraidstringGoogle app-to-web click ID (iOS, privacy-preserving)
ttclidstringTikTok click ID — improves the TikTok Events API match; a hashed email or phone also works
msclkidstringMicrosoft/Bing click ID
fbcstringMeta _fbc cookie value — captured automatically by the Pruuv Pixel from fbclid
fbpstringMeta _fbp cookie value — set by the Pruuv Pixel (or by Meta’s own pixel)
li_fat_idstringLinkedIn click ID — captured by the Pruuv Pixel from the li_fat_id URL parameter. Stored with the event.
click_timestampISO datetimeTime of the original ad click. Optional — when present, Pruuv keeps the conversion time it reports to Google after the click.
email_hashstringSHA-256 of the trimmed, lowercased email address. Used for Meta CAPI audience matching. The Pruuv Pixel hashes this in-browser automatically.
phone_hashstringSHA-256 of the phone number in E.164 format. Used for Meta CAPI and TikTok audience matching. The Pruuv Pixel only removes spaces before hashing, so give it E.164.
pruuv_aidstringPruuv native attribution ID — captured from the URL by the Pruuv Pixel. Stored with the event; attribution does not use it yet.
pruuv_visitor_idstringThe visitor ID of the browser the lead’s form was sent from: Pruuv.visitorId(), carried by a hidden form field to your CRM. Pruuv tries that browser’s click IDs first. Letters, digits, dots, underscores and hyphens, up to 128 characters; another value is ignored and the event is still stored.
identity_user_idstringThe reference given to Pruuv.identify() — or the email, when there was no reference. Sent by the Pruuv Pixel with track(), and sent to Meta (hashed) as external_id.
revenue_valuenumberConversion value — sent to ad platforms as conversion value
currencystringISO 4217 currency code (e.g. GBP, USD, EUR)
data_timestampISO datetimeWhen the conversion occurred in your CRM. Defaults to now if omitted.
data_timezonestringIANA timezone name (e.g. "Europe/London") — only needed if data_timestamp has no UTC offset (e.g. "2026-08-05 14:30:00"). Pruuv resolves such naive timestamps using this field, falling back to the timezone set on your client’s Settings page, then UTC. Prefer sending an offset-aware ISO timestamp (with a trailing Z or +HH:MM) when your CRM supports it — then this field isn’t needed at all.
first_touchobjectFirst-touch attribution snapshot — same fields as the root level UTMs/click IDs. Send when the visitor's landing page visit differs from the conversion page.
genderstringDemographic — for reporting only
age_rangestringDemographic — for reporting only (e.g. "25-34")
location_countrystringDemographic — ISO 3166-1 alpha-2 country code
location_regionstringDemographic — region or state
job_titlestringDemographic — for reporting only
company_sizestringDemographic — for reporting only
industrystringDemographic — for reporting only
<your custom key>string | number | booleanAny custom attribute declared under Funnel → Custom attributes. Stored on the event — coerced to its declared type, trimmed, and lowercased when normalisation is on. Undeclared keys are kept only in the raw payload.
Custom attributes: send extra keys alongside the built-in fields — for example a car-finance CRM posting "credit_band": "B". Declare the key under Funnel → Custom attributes and Pruuv stores it on every event (here as credit_band: "b" with lowercase normalisation), ready for conversion value rules. Re-posting the same reference merges new attribute keys first-write-wins — an already-stored key is never overwritten.

Responses

StatusBodyMeaning
200{ received: true, event_id: "...", status: "accepted" }Event accepted and stored — event_id is Pruuv’s own ID for it
200{ received: true, duplicate: true, status: "duplicate" }Duplicate — this reference already exists at this stage (permanent, not a time window)
200{ received: true, duplicate: true, status: "enriched" }Duplicate, but it filled in fields the stored event was missing (see below)
400{ error: "Validation failed", details: {...} }Invalid payload — check field types, or a null/blank/missing reference
401{ error: "Unauthorized" }Browser request (with an Origin header) with a missing or incorrect token
403{ error: "Forbidden origin" }Browser request from a domain that is not in Allowed domains, or the Pixel SDK is disabled
404{ error: "Stage not found" }stageId does not exist
429{ error: "Rate limit exceeded" }More than 1,000,000 events/hour for this stage
500{ error: "Internal error" }Server error — contact support if persistent

Deduplication and enrichment

Events are deduplicated on the combination of stage_id + reference (+ item_reference if present) using a unique index — this is a permanent deduplication, not a time-window. Sending the same reference twice will never create two rows. This makes it safe to retry on network errors.

However, if the second event arrives with new data that the first event lacked — for example, a CRM posts UTMs first and the Pruuv Pixel fires moments later with fbc/gclid/ttclid — Pruuv performs an enrichment merge: it fills in any NULL fields on the existing row from the new payload, without overwriting anything already stored. The response is still { received: true, duplicate: true }, but the row now has both datasets. A merge never itself sends anything, and never re-sends a conversion that was already sent. If the lead has not been sent yet, the fields the merge added (a click ID or an email, say) are used when it is. A merge cannot help a lead get matched to an ad account, though: UTM parameters are not merged.

Enrichable fields: fbc, fbp, gclid, wbraid, gbraid, ttclid, msclkid, li_fat_id, click_timestamp, email_hash, phone_hash, the name, date-of-birth and postcode hashes, user_agent, event_source_url, the visitor's IP address, revenue_value, currency, identity_user_id, first_touch, event_id, pruuv_visitor_id. A repeat post that adds the visitor ID also fills in the click IDs and browser details of that browser.

To get the browser's click IDs onto the lead before the conversion is sent, call Pruuv.identify() in the browser before your server posts the lead. See Pixel SDK → How Pruuv links the pixel to your CRM.

Integration paths compared

PathGoogle (gclid)Meta (fbc/fbp)TikTok (ttclid)Effort
Pruuv Pixel (recommended)Auto-capturedAuto-capturedAuto-capturedPaste snippet + call identify() (and track() if no CRM sends the lead)
Backend webhookMust capture yourself (hidden field)Requires JS on landing pageRequires JS on landing pageSelf-implement capture + POST
CRM pull (coming soon)Available if CRM stores gclidRarely stored in CRMsRarely stored in CRMsCRM config only