Integrations
Send webhooks to LeadJourney
The complete guide to sending events to LeadJourney by webhook – the endpoint, both ways to authenticate, every field you can send (identifiers, revenue, names, custom fields), how repeated calls are handled, and how to test and debug a webhook that isn't landing.
A webhook is how your own system tells LeadJourney that something happened: a lead came in, a
call was booked, a deal was won. You send one POST request with a small JSON body, and
LeadJourney matches that event back to the ad click that created the lead.
This is the way to go when the milestone lives in your own software – a custom CRM, your own booking form, an in-house backend – and there's no native connector watching it for you.
In short
- Copy your Webhook URL and Postback Secret from Settings → Events → Conversion API.
- Send a
POSTwithContent-Type: application/json, the headerAuthorization: Bearer <your secret>, and a body containing the event type plus at least one identifier (click_id,emailorphone). - Check the result in the Events Manager and the API Postbacks log.
When you need a webhook
- You have a native connector for the tool (HubSpot, Pipedrive, Stripe, Calendly and others). Then you don't need any of this – connect it and map the events. See Apps.
- The milestone lives in your own system. Your CRM, your booking system, your backend: send the event yourself with a webhook. That's this article.
Before you start
Three things have to be in place:
- Tracking is live. Events are matched to the visitor who clicked your ad, so clicks have to be recorded first – see Install the tracking script.
- The event exists. The
typeyou send has to match an event in your Events Manager. Every workspace starts withLeadandWonClient. - You have your credentials. Open Settings → Events → Conversion API:
| Credential | What it is |
|---|---|
| Webhook URL | The endpoint you send to. It looks like https://api.leadjourney.io/api/v1/postback/YOUR-WORKSPACE-ID and already contains your workspace ID. |
| Postback Secret | Your authentication secret, starting with pb_…. Treat it like a password; you can regenerate it here if it ever leaks. |
The secret belongs on your server
Anyone with the Postback Secret can write conversions into your workspace. Keep it in a server-side environment variable – never in browser code, a mobile app or a public repository. If a page in the browser needs to trigger an event, let it call your backend, and your backend adds the header and forwards the call.
Anatomy of a request
Every event is one POST with a JSON body:
curl -X POST "https://api.leadjourney.io/api/v1/postback/YOUR-WORKSPACE-ID" \
-H "Authorization: Bearer YOUR_POSTBACK_SECRET" \
-H "Content-Type: application/json" \
-d '{
"type": "Lead",
"click_id": "8f3c2a1e-4b7d-4c1a-9e2f-6d5b3a7c9e10",
"email": "[email protected]"
}'That's the whole contract: URL + Authorization header + JSON body. Everything below is
detail around those three parts.
No custom headers? Use the token in the URL
Some tools can't send custom headers. In that case, append the secret as a token query
parameter instead – …/postback/YOUR-WORKSPACE-ID?token=YOUR_POSTBACK_SECRET – and keep the
body identical. Only use this when the header isn't possible: tokens in URLs are more likely to
end up in proxy and server logs.
What you can send
| Field | Type | Required | What it does |
|---|---|---|---|
type | String | Required | The event name, exactly as in your Events Manager (e.g. Lead, WonClient). |
click_id | String (UUID) | Conditional | The lead's click ID – the strongest match. Required if email and phone are absent. |
email | String | Conditional | The lead's email address. Required if click_id and phone are absent. |
phone | String | Conditional | The lead's phone number. Required if click_id and email are absent. |
revenue | Number | Optional | The value of the conversion. Required for WonClient. |
cash_collected | Number | Optional | What was actually collected, if you track that separately from booked revenue. |
first_name | String | Optional | The lead's first name. |
last_name | String | Optional | The lead's last name. |
custom_fields | Object | Optional | Your own fields, nested as key–value pairs. Create them first. |
Stick to these fields – they're the ones the Conversion API defines.
The event type
type has to match the event name in the Events Manager
character for character, including capitalisation. A typo doesn't create a new event; the call
simply won't land where you expect it.
At least one identifier
An event without an identifier can't be attributed. Send whichever of these you have – ideally all three:
click_id– the strongest match, because it points at one specific ad click. Your system only has it if you captured it when the lead was created. See Read and store the click ID.email– the safety net. It also matches leads whose click ID was lost, for example because the visitor switched devices.phone– useful for call-driven funnels. Send it in international format (+49170…), so the same person is recognised across systems.
Never hold back an event because the click ID is missing
A lead who came in organically, by phone or on another device may have no click ID. Send the
event with email and phone anyway – it's still recorded, and it can still be attributed.
See How events are matched to a
lead.
Revenue
Send revenue as a number, not a string, with a dot as the decimal separator: 1499.00,
not "1.499,00 €". Your workspace currency decides how
that number is displayed in reports.
revenueis required forWonClient– without it, the deal counts but contributes no revenue to your ROAS.cash_collectedis for businesses that book a contract value up front but collect in instalments. If both are the same, you can leave it out.- For a refund, send a negative
revenueto correct the total.
Names
first_name and last_name don't affect matching – they enrich the lead record so you recognise
who is behind a conversion in your reports.
Custom fields
Anything specific to your business – industry, plan, deal size, lead score – goes into
custom_fields. Create each field once under Settings → Events → Custom Fields, then send its
exact webhook key:
{
"type": "WonClient",
"email": "[email protected]",
"revenue": 1499.00,
"custom_fields": {
"industry": "Software",
"company_size": 100,
"deal_size": 1499.00
}
}The field has to exist before you send data for it, and its type has to match the value you send – a string for text fields, a number for number and currency fields. Custom fields then become Group By and Filter dimensions in your reports. See Custom fields.
Example payloads
The same endpoint takes every event – only the body changes.
{
"type": "Lead",
"click_id": "8f3c2a1e-4b7d-4c1a-9e2f-6d5b3a7c9e10",
"email": "[email protected]",
"phone": "+491701234567",
"first_name": "Anna",
"last_name": "Schmidt"
}{
"type": "BookedCall",
"click_id": "8f3c2a1e-4b7d-4c1a-9e2f-6d5b3a7c9e10",
"email": "[email protected]"
}The click ID is the one you stored on the contact when the lead was created – that's what keeps every later milestone on the same click.
{
"type": "WonClient",
"click_id": "8f3c2a1e-4b7d-4c1a-9e2f-6d5b3a7c9e10",
"email": "[email protected]",
"revenue": 1499.00,
"cash_collected": 499.00,
"custom_fields": {
"plan": "Pro"
}
}{
"type": "Refund",
"email": "[email protected]",
"revenue": -1499.00
}A negative revenue corrects the total. Create the Refund event in the Events Manager first.
Sending it from your code
Hook into the place where the state actually changes – the form handler, the service method that
closes a deal, your payment provider's webhook handler – and send one POST per milestone:
await fetch(`https://api.leadjourney.io/api/v1/postback/${process.env.LJ_WORKSPACE_ID}`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LJ_POSTBACK_SECRET}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
type: "WonClient",
click_id: deal.contact.ljClickId, // the click ID you stored on the contact
email: deal.contact.email,
revenue: deal.amount,
}),
signal: AbortSignal.timeout(10_000),
});$ch = curl_init("https://api.leadjourney.io/api/v1/postback/" . getenv("LJ_WORKSPACE_ID"));
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("LJ_POSTBACK_SECRET"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"type" => "WonClient",
"click_id" => $deal->contact->lj_click_id,
"email" => $deal->contact->email,
"revenue" => $deal->amount,
]),
]);
curl_exec($ch);
curl_close($ch);import os, requests
requests.post(
f"https://api.leadjourney.io/api/v1/postback/{os.environ['LJ_WORKSPACE_ID']}",
headers={
"Authorization": f"Bearer {os.environ['LJ_POSTBACK_SECRET']}",
"Content-Type": "application/json",
},
json={
"type": "WonClient",
"click_id": deal.contact.lj_click_id,
"email": deal.contact.email,
"revenue": deal.amount,
},
timeout=10,
)curl -X POST "https://api.leadjourney.io/api/v1/postback/$LJ_WORKSPACE_ID" \
-H "Authorization: Bearer $LJ_POSTBACK_SECRET" \
-H "Content-Type: application/json" \
-d '{
"type": "WonClient",
"click_id": "'"$CLICK_ID"'",
"email": "[email protected]",
"revenue": 1499.00
}'Where to fire it in your system
| Milestone | Where to hook in |
|---|---|
| Lead created | The handler of your form or booking route, right after you've saved the record |
| Stage changed (qualified, booked, showed up) | The service method or database trigger that changes the stage |
| Deal won, payment captured | Your payment provider's webhook handler, or the method that marks the deal as won |
| Refund | Wherever the refund is booked |
Store the click ID on the contact when the lead is created. Every later event then reads it back from your own database – that's what keeps the whole funnel on one click, even weeks later.
Reliability
- Don't block the user. Send the webhook from a background job or queue, with a timeout of a few seconds. A slow call must never delay a sign-up or a checkout.
- Retry on failure. Treat any non-2xx response as a failure and retry with a short backoff.
- Don't build your own dedupe logic. Use the event modes below – then a retried call won't inflate your numbers.
Repeated calls: duplicates and updates
How a second webhook for the same lead is handled is decided per event in the Events Manager, not in your code:
| Mode | What happens |
|---|---|
| Create new conversion | Every webhook is recorded as a new event – for repeat purchases, for example. |
| Ignore duplicate postbacks | Only the first event per lead is kept. Ideal for Lead, so a double submit doesn't count twice. |
| Edit conversion with new data | The existing event is updated – for a revenue figure that gets revised later. |
| Keep original data, increment payout | The original event stays and the new revenue is added on top (WonClient only). |
You set the mode when you create the event – see Events & the Events Manager.
Test and debug
Create a click to match against
Open your site from a tracked link – a test ad URL or a link from the UTM Builder – so there's a click for the event to attach to.
Send a test event
Fire one webhook (the cURL above works). In Settings → Events the event's Total count goes up, Last Received updates, and its status flips from Created to Active.
Inspect the raw call
Open the API Postbacks log. It shows each call's status code, the payload you sent, and whether it matched a click, an email or a phone number. This is where you debug a webhook that isn't landing.
When something doesn't arrive
| Symptom | Usual cause |
|---|---|
| Event stays on Created / Never received | Nothing is reaching LeadJourney: wrong URL, missing workspace ID, or the call never fires |
| Response says it isn't authorised | Wrong or regenerated Postback Secret, or the header is missing – check Authorization: Bearer pb_… |
| Call arrives but nothing shows up in reports | The type doesn't match the event name exactly, or no identifier was sent |
| Conversion recorded but not attributed | No click_id, and the email/phone doesn't match a tracked visitor – see Missing leads |
| Numbers too high | The event's mode is Create new conversion while your system sends retries – switch to Ignore duplicate postbacks |
FAQ
Related articles
Send events from a custom CRM
The developer walkthrough: map pipeline stages to events and fire them from your backend.
Events & the Events Manager
Create events, pick their mode and see how they're matched to a lead.
Read and store the click ID
Get the click ID out of the browser and onto your records.
Custom fields
Send your own data with an event and report on it.
API Postbacks log
Inspect every call: payload, status code and whether it matched.
Apps
Connect external apps so their events flow into LeadJourney as conversions, auto-attributed to the campaigns that drove them. CRMs like HubSpot, Salesforce and GoHighLevel, payments and shops, forms, booking tools, call tracking, webinars and a BigQuery export.
Send events from a custom CRM
A developer reference for the Conversion API. Fire a webhook (postback) from your own CRM or backend whenever a pipeline stage changes, and LeadJourney matches it back to the ad click. Includes the endpoint, authentication, the full payload reference and copy-ready example requests in cURL, Node.js, PHP and Python.