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.
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, use the Pixel SDK on your landing pages and include the cookie values in your server-side payload.Authentication
All requests must include a Bearer token in the Authorization header. The token is the Webhook Token shown alongside each funnel stage in Connections > Funnel Webhooks.
Endpoint
POST https://app.pruuv.io/api/webhooks/funnel/{stageId}
Authorization: Bearer YOUR_WEBHOOK_TOKEN
Content-Type: application/jsonExample request
curl -X POST https://app.pruuv.io/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.
| Field | Type | Description |
|---|---|---|
| reference | string | Required. Unique identifier for this contact or conversion — CRM contact ID, deal ID, etc. Used for deduplication and enrichment merge. |
| item_reference | string | Secondary reference — deal ID, claim number, order ID. Required for stages that enforce item-level dedup. |
| event_name | string | Logical event label stored against the event — e.g. "Lead", "Sale", "Appointment". |
| utm_source | string | UTM source parameter |
| utm_medium | string | UTM medium parameter |
| utm_campaign | string | UTM campaign parameter |
| utm_content | string | UTM content parameter — used for ad-level attribution join (stores ad ID). |
| utm_term | string | UTM term parameter — stores keyword for Google Search / Bing keyword-level attribution. |
| utm_matchtype | string | Keyword 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. |
| gclid | string | Google click ID — required for Google Conversion Upload API |
| wbraid | string | Google web-to-app click ID (iOS, privacy-preserving) |
| gbraid | string | Google app-to-web click ID (iOS, privacy-preserving) |
| ttclid | string | TikTok click ID — required for TikTok Events API |
| msclkid | string | Microsoft/Bing click ID |
| fbc | string | Meta _fbc cookie value — captured automatically by the Pruuv Pixel from fbclid |
| fbp | string | Meta _fbp cookie value — generated by the Pruuv Pixel on first visit |
| click_timestamp | ISO datetime | Time of the original ad click. Required for Google Conversion Upload API (conversion must postdate the click). |
| email_hash | string | SHA-256 of the lowercased email address. Used for Meta CAPI audience matching. The Pruuv Pixel hashes this in-browser automatically. |
| phone_hash | string | SHA-256 of the phone number in E.164 format. Used for Meta CAPI and TikTok audience matching. |
| pruuv_aid | string | Pruuv native attribution ID — captured from URL parameter by the pixel. Used for ad-level attribution without relying on platform click IDs. |
| identity_user_id | string | Visitor identity from Pruuv.identify() — enables cross-session matching. |
| revenue_value | number | Conversion value — sent to ad platforms as conversion value |
| currency | string | ISO 4217 currency code (e.g. GBP, USD, EUR) |
| data_timestamp | ISO datetime | When the conversion occurred in your CRM. Defaults to now if omitted. |
| data_timezone | string | IANA timezone name (e.g. "Europe/London") — used to interpret naive timestamps from CRMs that send local time. |
| first_touch | object | First-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. |
| gender | string | Demographic — for reporting only |
| age_range | string | Demographic — for reporting only (e.g. "25-34") |
| location_country | string | Demographic — ISO 3166-1 alpha-2 country code |
| location_region | string | Demographic — region or state |
| job_title | string | Demographic — for reporting only |
| company_size | string | Demographic — for reporting only |
| industry | string | Demographic — for reporting only |
| <your custom key> | string | number | boolean | Any 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. |
"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
| Status | Body | Meaning |
|---|---|---|
| 200 | { received: true, event_id: "..." } | Event accepted and stored |
| 200 | { received: true, duplicate: true } | Duplicate — same reference at same stage within 60 seconds |
| 400 | { error: "Validation failed", details: {...} } | Invalid payload — check field types |
| 401 | { error: "Unauthorized" } | Missing or incorrect bearer token |
| 404 | { error: "Stage not found" } | stageId does not exist |
| 429 | { error: "Rate limit exceeded" } | More than 1,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.
Enrichable fields: fbc, fbp, gclid, wbraid, gbraid, ttclid, msclkid, click_timestamp, email_hash, phone_hash, user_agent, revenue_value, currency, identity_user_id, first_touch.
Integration paths compared
| Path | Google (gclid) | Meta (fbc/fbp) | TikTok (ttclid) | Effort |
|---|---|---|---|---|
| Pruuv Pixel (recommended) | Auto-captured | Auto-captured | Auto-captured | Paste snippet + call track() |
| Backend webhook | Must capture yourself (hidden field) | Requires JS on landing page | Requires JS on landing page | Self-implement capture + POST |
| CRM pull (coming soon) | Available if CRM stores gclid | Rarely stored in CRMs | Rarely stored in CRMs | CRM config only |