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

  1. Copy your Webhook URL and Postback Secret from Settings → Events → Conversion API.
  2. Send a POST with Content-Type: application/json, the header Authorization: Bearer <your secret>, and a body containing the event type plus at least one identifier (click_id, email or phone).
  3. 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:

  1. 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.
  2. The event exists. The type you send has to match an event in your Events Manager. Every workspace starts with Lead and WonClient.
  3. You have your credentials. Open Settings → Events → Conversion API:
CredentialWhat it is
Webhook URLThe endpoint you send to. It looks like https://api.leadjourney.io/api/v1/postback/YOUR-WORKSPACE-ID and already contains your workspace ID.
Postback SecretYour 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

FieldTypeRequiredWhat it does
typeStringRequiredThe event name, exactly as in your Events Manager (e.g. Lead, WonClient).
click_idString (UUID)ConditionalThe lead's click ID – the strongest match. Required if email and phone are absent.
emailStringConditionalThe lead's email address. Required if click_id and phone are absent.
phoneStringConditionalThe lead's phone number. Required if click_id and email are absent.
revenueNumberOptionalThe value of the conversion. Required for WonClient.
cash_collectedNumberOptionalWhat was actually collected, if you track that separately from booked revenue.
first_nameStringOptionalThe lead's first name.
last_nameStringOptionalThe lead's last name.
custom_fieldsObjectOptionalYour 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.

  • revenue is required for WonClient – without it, the deal counts but contributes no revenue to your ROAS.
  • cash_collected is 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 revenue to 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

MilestoneWhere to hook in
Lead createdThe 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 capturedYour payment provider's webhook handler, or the method that marks the deal as won
RefundWherever 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:

ModeWhat happens
Create new conversionEvery webhook is recorded as a new event – for repeat purchases, for example.
Ignore duplicate postbacksOnly the first event per lead is kept. Ideal for Lead, so a double submit doesn't count twice.
Edit conversion with new dataThe existing event is updated – for a revenue figure that gets revised later.
Keep original data, increment payoutThe 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

SymptomUsual cause
Event stays on Created / Never receivedNothing is reaching LeadJourney: wrong URL, missing workspace ID, or the call never fires
Response says it isn't authorisedWrong or regenerated Postback Secret, or the header is missing – check Authorization: Bearer pb_…
Call arrives but nothing shows up in reportsThe type doesn't match the event name exactly, or no identifier was sent
Conversion recorded but not attributedNo click_id, and the email/phone doesn't match a tracked visitor – see Missing leads
Numbers too highThe event's mode is Create new conversion while your system sends retries – switch to Ignore duplicate postbacks

FAQ