Integrations

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.

If you run a custom-coded or in-house CRM, there's no native connector to click – you send events yourself. The pattern is simple: define a trigger in your CRM (a new lead row, an opportunity moving to Won, a payment captured) and, when it fires, send an HTTP webhook – also called a postback – to LeadJourney's Conversion API. LeadJourney matches that event back to the ad click that created the lead, so a milestone in your pipeline becomes an attributed conversion.

This article is written to be handed to a developer. It has the exact endpoint, how authentication works, every payload field, and copy-ready requests in four languages.

Before you start

Two things have to be in place first:

  • Tracking is live. Events are matched back to the visitor that clicked your ad, so clicks have to be recorded first. See Install the tracking script (and the developer notes in Install on a custom-coded website).
  • At least one event exists. Create the events you want to send in the Events Manager – every workspace starts with Lead and WonClient. The event name is the type you send in the payload.

Get your credentials

Open Settings → Events → Conversion API. That tab holds the three values your integration needs:

CredentialWhat it is
Workspace IDYour unique workspace identifier. It's already baked into the Webhook URL (see below), so you rarely send it separately.
Postback SecretYour authentication secret (starts with pb_…). This is the "authorization code" – treat it like a password. You can Regenerate it here if it ever leaks.
Webhook URLThe complete endpoint events are sent to – it looks like https://api.leadjourney.io/api/v1/postback/YOUR-WORKSPACE-ID.

The secret stays on your server

The Postback Secret grants write access to your workspace's conversions. Keep it in a server-side environment variable – never put it in browser code, a mobile app, or a public repository. Your CRM/backend holds the secret and makes the call; the browser never sees it.

Anatomy of a request

Every event is a single POST with a JSON body, authenticated with your Postback Secret. Copy the exact Webhook URL from the Conversion API tab – it already contains your Workspace ID.

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": "YOUR_CLICK_ID",
    "email": "[email protected]",
    "first_name": "John",
    "last_name": "Doe"
  }'

That's the whole contract: URL + Authorization header + JSON body. Everything else on this page is detail around those three parts.

Authentication

There are two ways to send the Postback Secret. Both are equivalent – pick whichever your platform supports.

Send the secret as a Bearer token in the Authorization header. This is the recommended form for any custom code, since the secret never ends up in URLs or server logs.

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", "email": "[email protected]" }'

If your platform can't send custom headers (some CRMs and no-code tools, e.g. HubSpot workflows), pass the secret as a token query parameter instead. The body and everything else stay the same.

curl -X POST "https://api.leadjourney.io/api/v1/postback/YOUR-WORKSPACE-ID?token=YOUR_POSTBACK_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "type": "Lead", "email": "[email protected]" }'

Only use this when the header form isn't possible – tokens in URLs are more likely to show up in proxy and server logs.

Payload reference

The body is JSON. type is always required, and every event needs at least one identifier (click_id, email or phone) so LeadJourney can match it to a lead.

FieldRequiredDescription
typeRequiredEvent type name – must match an event in your Events Manager (e.g. Lead, WonClient).
click_idConditionalThe lead's click UUID. Required if email and phone are absent.
emailConditionalThe lead's email. Required if click_id and phone are absent.
phoneConditionalThe lead's phone number. Required if click_id and email are absent.
revenueOptionalRevenue value (required for WonClient events).
cash_collectedOptionalCash actually collected.
first_nameOptionalThe lead's first name.
last_nameOptionalThe lead's last name.
custom_fieldsOptionalAn object of your own custom fields (create them first).

Always send an identifier

Send a click_id, an email or a phone with every event. The click_id is the strongest – it ties the event to the exact campaign, ad set and ad. Without any identifier, LeadJourney has no way to connect the event to a click and it can't be attributed. See How events are matched to a lead.

Sending extra CRM data (custom fields)

To carry attributes like industry, deal size or plan, create the field first under Settings → Events → Custom Fields, then nest its exact key under custom_fields in the same webhook – no new endpoint:

{
  "type": "WonClient",
  "email": "[email protected]",
  "revenue": 1499.00,
  "custom_fields": {
    "industry": "Software",
    "company_size": 100,
    "deal_size": 1499.00
  }
}

See Custom Fields for the full walkthrough.

Define the trigger in your CRM

This is the part that's unique to a custom-coded system. In a native connector LeadJourney watches the CRM for you; here you decide the moments and fire the call.

Capture the click ID when the lead is created

When a visitor arrives from a tracked link, the tracking script stores their click ID in the browser at localStorage.getItem('clickId'). Save that value onto the lead record the moment the lead is created – send it along with your sign-up/lead form as a hidden field, or read it in your form handler.

Persisting the click ID on the lead is what lets every later event for that lead (qualified, booked, won) include the same click_id, so the whole funnel stitches to one click. If you can't capture it, fall back to email / phone – LeadJourney matches on those too.

Map each pipeline stage to an event

Pick the milestones in your pipeline and decide which LeadJourney event each one sends. For a sales-led funnel that's typically:

Your CRM triggerEvent type to sendExtra fields
New lead row inserted / form submittedLeademail, name
Lead qualified by salesQualifiedLead
Call/demo bookedBookedCall
Prospect attended the callShowedUp
Deal marked Won / payment capturedWonClientrevenue (and cash_collected)

The type must match an event you created in the Events Manager. See Common events by business type for SaaS, lead-gen and e-commerce templates.

Fire the webhook from your backend

Hook into wherever that state change happens in your code – a database trigger, an ORM callback, the service method that closes a deal, a payment webhook handler – and send one POST per milestone. Keep the secret in an environment variable.

curl -X POST "https://api.leadjourney.io/api/v1/postback/YOUR-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
  }'
await fetch("https://api.leadjourney.io/api/v1/postback/YOUR-WORKSPACE-ID", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.LJ_POSTBACK_SECRET}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    type: "WonClient",
    click_id: lead.clickId,   // the click ID you stored when the lead was created
    email: lead.email,
    revenue: deal.amount,
  }),
});
$ch = curl_init("https://api.leadjourney.io/api/v1/postback/YOUR-WORKSPACE-ID");
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer " . getenv("LJ_POSTBACK_SECRET"),
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "type"     => "WonClient",
        "click_id" => $lead->click_id,
        "email"    => $lead->email,
        "revenue"  => $deal->amount,
    ]),
]);
curl_exec($ch);
curl_close($ch);
import os, requests

requests.post(
    "https://api.leadjourney.io/api/v1/postback/YOUR-WORKSPACE-ID",
    headers={
        "Authorization": f"Bearer {os.environ['LJ_POSTBACK_SECRET']}",
        "Content-Type": "application/json",
    },
    json={
        "type": "WonClient",
        "click_id": lead.click_id,
        "email": lead.email,
        "revenue": deal.amount,
    },
    timeout=10,
)

Handling duplicates and updates

Don't try to dedupe in your own code – let the Events Manager do it. Each event has a mode that decides how repeated postbacks for the same lead are handled:

  • Ignore duplicate postbacks – keep only the first (e.g. one Lead per person, even if your trigger fires twice).
  • Edit conversion with new data – update the existing event when a value changes (e.g. a deal's revenue gets revised).
  • Create new conversion – record every postback (e.g. repeat purchases).

Set this per event when you create it – see the mode table in Events & the Events Manager. It means your CRM can safely resend without inflating your numbers.

Error handling & retries

  • Send the body as application/json and read the HTTP status. A 2xx means the event was accepted; treat any non-2xx as a failure and retry with a short backoff (your CRM may fire before the network is ready).
  • Use a request timeout so a slow call never blocks the action that triggered it. Fire the postback from a background job or queue if you can.
  • Prefer the event modes above over your own dedupe logic, so a retried call doesn't create a duplicate conversion.

Test & verify

Create a click to match against

Open your site from a tracked link – one carrying lj_* parameters, e.g. a test ad URL or a link built with the UTM Builder – so a click ID is stored and there's something for the event to attach to.

Send a test event

Fire one postback (the cURL above is fine). Within moments, 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 to see each call's status code, the raw payload you sent, and whether it matched a click, email or phone. This is where you debug a postback that isn't landing.

Event stuck on “Created” / “Never”?

The events aren't reaching LeadJourney. Check the Webhook URL (does it include your workspace ID?), that the Authorization: Bearer … secret is correct and current, that the JSON type matches the event name exactly, and that you sent an identifier. The API Postbacks log shows the exact failure reason.

FAQ

On this page