Webhooks

Webhooks move conversions in and out of TrueMetriks. Take them in from any tool, and send yours back out to any URL with attribution attached. Here is how both work.

Set this up with AI

Copy this prompt into ChatGPT, Claude or whichever AI you use. It carries this whole guide, the pages it depends on, and a link to every video, so it can walk you through it one step at a time.

Open as text

Webhooks move conversions in and out of TrueMetriks. Open Settings > Integrations > Webhooks and you get two tabs: Receive webhooks brings conversions in, Send webhooks pushes them back out with attribution attached.

Receive webhooks

Bring conversions in from any tool - Zapier, a WordPress form, GoHighLevel, or your own backend. Each webhook gets its own secret URL. Paste that URL into the tool and it sends Leads, booked calls, Purchases or custom events into this site's analytics.

Step 1 - Create a receiving webhook

On the Receive webhooks tab, click Add receiving webhook.

Receive webhooks 3 webhooks

Get conversions INTO TrueMetriks. Each webhook gets its own secret URL - paste it into any tool that can send webhooks.

Lead From GHL Lead ad platforms ON
Store Purchases Purchase
⚠ Stripe is already reporting Purchases for this site - if the same sale also arrives through this webhook it may be counted twice. Matching purchases are paired automatically.

Webhook URL - paste this into the sending tool

https://q.demo-brand.example/api/webhooks/in/dd10d51c9d4fd534d7eb53c768c12d5d4d1b293e9a131b

Also fire to connected ad platforms

Sends matching events to Facebook / TikTok / GA4 / Google Ads exactly like a native integration. Analytics reporting always happens either way.

Recent deliveries (3)
Quiz Completed Hook QuizCompleted
On the Receive webhooks tab, click Add receiving webhook. Each webhook has its own secret URL and an optional Also fire to ad platforms toggle.

Name it after the tool that will send to it, then pick the event type:

  • Lead, Schedule, or Purchase for a standard conversion.
  • Custom event name for your own event (for example ApplicationSubmitted). Custom events show in analytics and can trigger sending webhooks.

You cannot change the event type later, because tools send to this URL expecting it. If you need a different type, create a second webhook. Click Create webhook.

Add receiving webhook

Name

e.g. Zapier leads

A label just for you - name it after the tool that will send to it.

Event type

Lead

Lead, Schedule, Purchase, or a custom event name. Fixed after creation.

Also fire to connected ad platforms

Sends matching events to Facebook / TikTok / GA4 / Google Ads exactly like a native integration. Analytics reporting always happens either way.

Name the webhook, pick the event type, then click Create webhook. The event type is fixed after creation.

Step 2 - Copy the URL into your tool

Copy the webhook URL and paste it into the tool that will send to it. That is the whole setup. The URL is the only credential, there are no API keys.

Step 3 - Send a payload

POST JSON to the URL (form-encoded works too). We read the common field names, and we find them inside nested payloads too, so most tools work with no setup at all:

curl -X POST 'https://q.your-domain/api/webhooks/in/YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","first_name":"Jane","value":49.99,"currency":"USD"}'

The fields we read are email, phone, first_name, last_name, value, currency and order_id. Anything else you send is kept as event properties. If it works you get HTTP 200 back, with a summary of what we found.

The event then shows in your analytics like any other integration. It only gets credited to an ad if we can tell which visitor it came from, and that part is worth getting right, so it has its own section next.

Make sure the conversion lands on the right visitor

When a conversion arrives by webhook, we have to work out which visitor it came from. If we cannot, the sale still counts and the money is still yours, but it lands on a person with no history, so no ad gets the credit.

Most of the time this takes care of itself. Nearly every tool sends us an email, and nearly every buyer types their email somewhere on your site, so the two line up and the conversion joins the right journey. Nothing for you to do. Capturing the email as early as you have it makes this even more reliable - see window.tmIdentify on the Custom install guide.

When email is not enough

Two things break the email match, and both are worth knowing about:

  • They never typed their email on your site. They browsed, then paid somewhere you do not control, so no visit of theirs carries that address. Nothing to match against.
  • They typed it two different ways. One spelling at a booking, another at checkout, and to us that is two different people.

We do repair the obvious misspellings, but only so far. We fix a short, fixed list of provider typos, one way only: an email arriving as gmial.com is also tried as gmail.com. An email that arrives spelled correctly is never checked against misspellings, because that could merge two different people. And a typo before the @ (davd@ for david@) is not fixed at all.

So if your conversions keep landing with no visit attached, email alone is not reaching those buyers, and the next section is how you fix it.

The fix - send us the visitor id

This is optional, and most sites never need it. But when email is not landing, it is the one thing that always works, because there is nothing to match and nothing to guess. Everything else is us working out who someone probably is. This is us being told.

Every visitor we track carries a cookie on your own domain called _evid. Read it in the browser, drop it into a hidden field on your form or checkout, and send it in the payload alongside the email you already send.

You can name the field any of these: evid, _evid, tm_evid, visitor_id, tm_visitor_id, tm_id.

Add the hidden field to your form:

<input type="hidden" name="tm_evid" value="">

Then fill it from the cookie at the moment the form is submitted:

<script>
  document.addEventListener("submit", function (e) {
    var field = e.target.querySelector('input[name="tm_evid"]');
    if (!field) return;
    var match = document.cookie.match(/(?:^|;\s*)_evid=([^;]*)/);
    field.value = match ? decodeURIComponent(match[1]) : "";
  }, true);
</script>

Your payload then carries one extra line:

{
  "email": "[email protected]",
  "value": 49.99,
  "currency": "USD",
  "tm_evid": "ev_k3f9q2_a1b2c3d4"
}

Read the cookie in the browser. Never bake it into the page. If a CDN caches the page holding your hidden field, one visitor's _evid gets saved into the HTML and handed to everybody who loads it after them, and all of their conversions pile onto that one person. We trust the visitor id above everything else, so nothing later catches the mistake. The snippet above is safe because it reads the cookie as the form is sent, in that visitor's own browser.

Send the click id if you have it

fbclid and gclid sit in the landing-page URL, so you can put them in a hidden field the same way, with no cookie to read. Another option when the visitor id is awkward to get at.

You can name the field any of these: fbclid, gclid, click_id, tm_click_id.

What we try, in order

  • Visitor id - exact, no guessing.
  • Click id - fbclid or gclid, matched against the click ids we stored for that visitor.
  • Email - matched against the email attached to their visits.

We stop at the first one that finds a real visit. For most sites the email on its own gets there, which is why nothing above is required. Sending more than one is safe either way - they do not clash, the strongest one wins.

Why a conversion shows no visit

A conversion with no visit attached means nothing in that payload matched a visitor we had seen. It still counts and the money is still yours, but with no journey behind it there is no ad to credit.

Adding the visitor id fixes it from then on. It cannot go back and fix conversions that already arrived, so the sooner it is in, the sooner your attribution fills in.

Also fire to connected ad platforms

Each receiving webhook has an Also fire to connected ad platforms toggle, off by default. Turn it on and the events also go to Facebook, TikTok, GA4 and Google Ads, just like a native integration. Your analytics get them either way. Custom events work too: Call Booked reaches your ad platforms under that same name.

Send a custom event to Facebook as a Custom Conversion

Once a custom-named receiving webhook exists, its event name shows up in Facebook's Create custom event dialog, under a Custom webhook events group. Pick it there and we create a Facebook Custom Conversion with that name, so Facebook can optimize and report on it. We store it as an OTHER-type conversion, so it is never mistaken for a Lead or a Purchase, and it fires once per event with no duplicates. Facebook only gets the event while that webhook's Also fire to connected ad platforms toggle is on. See the Facebook integration guide for the dialog.

Duplicate protection

We count each conversion once, even when two tools report it:

  • Leads, Schedules and custom events: the same email and event type counts once per day, across every source. A webhook and a native integration (say Typeform) reporting the same lead will not double-count.
  • Purchases are checked on email, order and amount instead, so two genuinely different orders on the same day both count.

If a native integration already reports the same event type for this site, the webhook shows an amber warning as a reminder.

Recent deliveries

Every receiving webhook keeps a Recent deliveries log: the last 25 receipts, 5 per page. Each one shows what happened (accepted, deduped, parse error, oversized or ignored) plus a preview of the payload, so you can see exactly what arrived.

Rotating or removing a webhook

There is no regenerate button, on purpose. To change a URL, delete the webhook and create a new one, which gets a new URL. Deleting one stops its URL working straight away. Payloads over 256 KB are rejected.

Send webhooks

Push conversions out of TrueMetriks to any URL - a CRM, GoHighLevel, or your own backend. Every Lead, Schedule and Purchase goes to your URL with the person's first and last touch, sessions and paid channel attached. Leads reach GoHighLevel with the whole journey on them.

Step 1 - Create a sending webhook

Switch to the Send webhooks tab and click Add sending webhook.

Send webhooks 2 webhooks, 1 paused

Push conversions OUT of TrueMetriks. Every event is sent to your URL enriched with the person's first and last touch, sessions and paid channel.

GoHighLevel contacts active LeadSchedule

Webhook URL - we send events here

https://services.leadconnectorhq.com/hooks/aBcD1234
Recent deliveries (2)
Today 14:33:29 delivered Lead - [email protected], attempt 1 212 ms payload
Today 09:13:56 delivered Lead - [email protected], attempt 1 188 ms payload
Backend CRM sync paused Purchase
⚠ Paused automatically after 20 consecutive failures. Last error: 503 Service Unavailable. Fix the receiver, then unpause.
On the Send webhooks tab, click Add sending webhook. A dead URL auto-pauses and shows an Unpause button once you have fixed the receiver.

Fill in the dialog:

  • Name - a label for you (for example "GoHighLevel contacts").
  • Webhook URL - where we POST the payload. Must be https.
  • Send this event - pick ONE event from the dropdown: Lead, Schedule, Purchase, or one of your custom events (your Facebook Custom Conversions and custom-named receiving webhooks).

One sending webhook sends one event. To send another event to the same place, create a second one.

Click Create webhook.

Add sending webhook

Name

e.g. GoHighLevel contacts

Webhook URL

https://your-tool.com/webhook

Must be https. We POST a JSON payload to this URL for every matching event.

Send this event

Lead

Lead, Schedule, Purchase, or one of your custom events (your Facebook Custom Conversions and custom-named receiving webhooks).

Name it, paste your https URL, pick the one event to send, then click Create webhook.

Step 2 - Copy the signing secret

We show a signing secret once, right after you create the webhook. Copy it now, you will not see it again.

Sending webhook created

Every webhook we send is signed with this secret so your receiver can verify it came from TrueMetriks. Signature verification is optional.

Signing secret

whs_9d4e2a7c1f60b83e5a2d9c47f1b0e6a3

⚠  Copy it now - you will not see this secret again.

The signing secret shows once. Copy it now to verify the X-TrueMetriks-Signature header on each delivery.

Every delivery is signed so your receiver can check it came from us. The signature rides in an X-TrueMetriks-Signature: t=<unix>,v1=<hmac-sha256> header, built from the request body and your signing secret. Checking it is optional - receivers that do not check signatures (GoHighLevel inbound webhook triggers, for example) can ignore the header.

Step 3 - Know the timing

A delivery fires about 75 seconds after the event arrives. The wait is on purpose: identity and attribution settle first, so the payload carries the full journey. To test the wiring without waiting, click Send test and a sample payload arrives in about 15 to 30 seconds.

The test uses this webhook's real event name (Call Booked, not a generic Test), so your receiver sees exactly what a live delivery looks like. It is still marked with "test": true at the top level (and event.properties.test: true) so nothing downstream counts it as a real conversion, and in the Recent deliveries log the row reads Test: Call Booked. On a test send contact is null and attribution is sample data.

The payload

Every delivery is a JSON POST. event.type is Lead, Schedule or Purchase for standard events, and Custom for your own, so your receiver can branch on it:

{
  "api_version": "2026-07-09",
  "webhook": { "destination_id": "wdst_...", "attempt": 1, "sent_at": "2026-07-11T10:17:27.718Z" },
  "site": { "id": 1, "domain": "example.com" },
  "event": { "type": "Purchase", "name": "Purchase", "id": "evt_...", "time": "...", "value": 49.99, "currency": "USD", "source": "webhook", "properties": {} },
  "contact": { "email": "[email protected]", "phone": null, "first_name": "Jane", "last_name": null },
  "attribution": {
    "first_touch": { "source": "google", "medium": "cpc", "campaign": "brand-search", "channel": "Paid Search", "referrer": "https://www.google.com/", "landing_url": "example.com/landing" },
    "last_touch": { "source": "facebook", "medium": "paid", "campaign": "retargeting", "channel": "Paid Social", "referrer": "https://facebook.com/", "landing_url": "example.com/offer" },
    "touches": 7, "sessions": 3, "days_to_convert": 7, "paid": true, "paid_channel": "Paid Social"
  }
}

On real events contact carries the person's actual email, phone, and name, and attribution their real journey. On test sends contact is null and attribution is sample data.

A custom event delivers the same shape, with event.type set to Custom and event.name set to your event name:

{
  "api_version": "2026-07-09",
  "webhook": { "destination_id": "wdst_...", "attempt": 1, "sent_at": "2026-07-17T10:17:27.718Z" },
  "site": { "id": 1, "domain": "example.com" },
  "event": { "type": "Custom", "name": "Call Booked", "id": "evt_...", "time": "...", "source": "webhook", "properties": {} },
  "contact": { "email": "[email protected]", "phone": null, "first_name": "Jane", "last_name": null },
  "attribution": {
    "first_touch": { "source": "google", "medium": "cpc", "campaign": "brand-search", "channel": "Paid Search" },
    "last_touch": { "source": "facebook", "medium": "paid", "campaign": "retargeting", "channel": "Paid Social" },
    "touches": 4, "sessions": 2, "days_to_convert": 1, "paid": true, "paid_channel": "Paid Social"
  }
}

A test delivery of that same webhook looks identical but adds a top-level "test": true and event.properties.test: true, so your receiver can drop it.

Reliability

  • Stable IDs, so you can drop repeats. Each delivery carries an X-TrueMetriks-Delivery: dlv_<id> header and an event.id field, so your receiver can spot a retry. The X-TrueMetriks-Event header carries the event name.
  • Automatic retries, spaced further apart each time, when a delivery fails.
  • Dead URLs pause themselves. A URL that keeps failing is paused for you. The card shows why, with an Unpause button once your receiver is fixed.
  • Recent deliveries shows every attempt (delivered, queued, sending or failed) with the HTTP code.

Frequently asked questions

How does TrueMetriks know which visitor a webhook conversion belongs to?

It tries the visitor id first, then a click id, then the email address, and stops at the first one that finds a real visit. For most sites the email is enough, because buyers type it on the page. If conversions keep arriving with no visit attached, add the visitor id from the _evid cookie in a hidden field.

Do I need API keys to receive webhooks?

No. Create a receiving webhook, copy its secret URL, and paste that URL into the sending tool. The URL is the only credential.

What payload format does a receiving webhook accept?

POST JSON (form-encoded also works). TrueMetriks reads a simple schema (email, phone, first_name, last_name, value, currency, order_id) and auto-maps most other or nested payloads by key, so most tools work with no payload configuration.

How does TrueMetriks stop the same conversion counting twice?

The same email and event type within one UTC day counts once, across every source, so a webhook and a native integration reporting the same lead do not double-count. Purchases dedup on email, order, and amount instead, so two genuinely different orders both count.

Can a sending webhook include attribution?

Yes. Every sent payload carries the person's first and last touch, channel, sessions, and days to convert, so a CRM like GoHighLevel receives the full journey with each conversion.

How do I verify a sent webhook came from TrueMetriks?

Each delivery is signed with an X-TrueMetriks-Signature header (t=,v1=) using the signing secret shown once at creation. Receivers that do not verify signatures can simply ignore it.

Can one sending webhook send more than one event?

No. A sending webhook sends exactly one event - Lead, Schedule, Purchase, or one of your custom events. To send another event to the same place, create a second sending webhook.

When does a sending webhook fire?

About 75 seconds after the event is ingested, so identity and attribution settle first. Send test delivers a sample payload within about 15 to 30 seconds.

Need help with this?

Reach us instantly on WhatsApp, or send us a message and we will get back to you. On workdays we usually respond in less than 24 hours.

or send us a message