# Ad attribution

Keep the ad click that brought a customer all the way to the deal in your CRM and back to Google Ads

> Full REST + WebSocket API reference: https://api.happ.tools/reference

A visitor clicks your ad, reads a couple of pages, opens the chat, and leaves a request. Your CRM gets the deal — but with nothing on it that says which campaign paid for that visit. Happ now carries the click through the whole chain: page → chat → deal → back to the ad network.

Nothing to switch on. Any widget loaded with the current embed snippet starts collecting on its next page view.

## What gets collected [#what-gets-collected]

On the visitor's first page, the embed script reads the ad parameters out of the URL and stores them on your own domain:

| Group             | Parameters                                                                      |
| ----------------- | ------------------------------------------------------------------------------- |
| Google            | `gclid`, `gbraid`, `wbraid`                                                     |
| Meta              | `fbclid`                                                                        |
| TikTok, Microsoft | `ttclid`, `msclkid`                                                             |
| Campaign labels   | `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, `utm_id` |

It also keeps the landing page and the referrer.

Two touches are kept, not one:

* **first touch** — the campaign that introduced this person;
* **last touch** — what they clicked on the way back, which is what the ad networks attribute a conversion to.

The parameters survive navigation: a visitor can land on the ad URL, browse five pages with a clean address bar, and open the chat an hour later — the campaign is still there. A visit carrying no ad parameters never overwrites what was stored, so a customer who returns directly to convert stays attributed to the campaign that won them.

Conversations that arrive from a Click-to-Messenger or Click-to-Instagram ad carry their ad context the same way, with the ad id and creative title in place of a click id.

## Where it ends up [#where-it-ends-up]

### In the CRM record [#in-the-crm-record]

Every CRM integration that creates leads or deals writes the origin into the record's comment:

```
--- Happ attribution ---
channel: widget
gclid: Cj0KCQjw...
utm_source: google
utm_medium: cpc
utm_campaign: raiden-brand
landing: /catalog
captured: 2026-09-19T08:12:44.000Z
```

Key-value lines on purpose: a manager reads them, and a script, a spreadsheet formula or an n8n flow parses them without asking us for an export.

The assistant is never asked to pass any of this. A click id is a hundred opaque characters, and a model told to copy one will truncate it or invent a plausible one — a wrong click id is worse than none, because it gets uploaded as someone else's conversion. The platform attaches it server-side, out of the conversation entirely.

**NetHunt** additionally exposes every field as a mappable parameter — `attribution_gclid`, `attribution_utm_campaign`, `attribution_note` and the rest — so you can point each one at a column of your own folder in the integration settings.

### In your webhook [#in-your-webhook]

If you have the **Chat webhook** integration connected, every message body carries an `attribution` object, and a new event fires when a request is actually created:

```json
{
  "event": "lead.created",
  "eventId": "6f2a...:8821",
  "chatId": "6f2a1c84-...",
  "companyId": "8fb075ae-...",
  "toolName": "keepincrm_create",
  "entityId": "8821",
  "entityType": "leads",
  "phone": "+380731234567",
  "createdAt": "2026-09-19T08:14:02.000Z",
  "attribution": {
    "channel": "widget",
    "clickId": "Cj0KCQjw...",
    "clickNetwork": "google",
    "utmSource": "google",
    "utmMedium": "cpc",
    "utmCampaign": "raiden-brand",
    "landingPage": "/catalog",
    "referrer": "https://www.google.com/",
    "capturedAt": "2026-09-19T08:12:44.000Z",
    "note": "--- Happ attribution ---\nchannel: widget\ngclid: Cj0KCQjw..."
  }
}
```

`eventId` is stable across retries — dedupe on it. When the visitor converted on a later campaign than the one that introduced them, the first touch rides along under `attribution.first`.

`phone` is there for [Enhanced Conversions for Leads](https://support.google.com/google-ads/answer/13258081), which match on a hashed phone number and need no click id at all — the fallback for visitors whose `gclid` never made it into the URL.

Bodies without an `event` field are ordinary messages, as before.

### On the page, for GTM and GA4 [#on-the-page-for-gtm-and-ga4]

When a request is created, the widget pushes an event into `dataLayer` on the host page:

```js
{
  event: "happ_lead_created",
  happ_event_id: "6f2a...:8821",
  happ_chat_id: "6f2a1c84-...",
  happ_entity_id: "8821",
  happ_entity_type: "leads",
  happ_widget_id: "wgt_..."
}
```

This counts the request, not the chat opening — every curious visitor opens the chat, and a conversion defined that way teaches the bidding nothing. The event is deduped in the browser, and `happ_event_id` matches the webhook's `eventId`, so a server-side copy of the same conversion reconciles against it.

In GTM: create a **Custom Event** trigger on `happ_lead_created`, use it for your GA4 event or Google Ads conversion tag, and pass `happ_event_id` as the transaction/event id so the two sources are counted once.

## Closing the loop with Google Ads [#closing-the-loop-with-google-ads]

1. Take the deals your CRM marks as won.
2. For each one, read `gclid` out of the attribution note (or out of the column you mapped it to).
3. Upload them as [offline conversions](https://support.google.com/google-ads/answer/2998031) with the conversion value and time.

For deals with no click id — a visitor from organic search, an iOS visitor whose `gclid` never appeared — use Enhanced Conversions for Leads on the phone number instead.

## Limits [#limits]

* Parameters are stored in the browser's `localStorage` on your domain. A visitor who clears site data, switches devices or browses in a private window arrives as a fresh visit; the first touch already recorded on their conversation is not overwritten by it.
* A voice call started from the widget does not carry the attribution into the CRM yet — the chat does.
* The attribution reflects what was in the URL. If your ads land on a page that redirects and drops the query string, there is nothing to capture; check that your redirects preserve it.
