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
LeadandWonClient. The event name is thetypeyou send in the payload.
Get your credentials
Open Settings → Events → Conversion API. That tab holds the three values your integration needs:
| Credential | What it is |
|---|---|
| Workspace ID | Your unique workspace identifier. It's already baked into the Webhook URL (see below), so you rarely send it separately. |
| Postback Secret | Your 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 URL | The 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.
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.
| Field | Required | Description |
|---|---|---|
type | Required | Event type name – must match an event in your Events Manager (e.g. Lead, WonClient). |
click_id | Conditional | The lead's click UUID. Required if email and phone are absent. |
email | Conditional | The lead's email. Required if click_id and phone are absent. |
phone | Conditional | The lead's phone number. Required if click_id and email are absent. |
revenue | Optional | Revenue value (required for WonClient events). |
cash_collected | Optional | Cash actually collected. |
first_name | Optional | The lead's first name. |
last_name | Optional | The lead's last name. |
custom_fields | Optional | An 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 trigger | Event type to send | Extra fields |
|---|---|---|
| New lead row inserted / form submitted | Lead | email, name |
| Lead qualified by sales | QualifiedLead | – |
| Call/demo booked | BookedCall | – |
| Prospect attended the call | ShowedUp | – |
| Deal marked Won / payment captured | WonClient | revenue (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
Leadper 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
revenuegets 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/jsonand 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
Related articles
Events & the Events Manager
Create events, choose their mode, and find your Conversion API credentials.
Custom Fields
Send extra CRM data alongside each event and use it as a report dimension.
Track sign-ups in your app
A worked example, including carrying the click ID through Google/Microsoft SSO.
API Postbacks log
Inspect each call, its status and payload, and confirm it matched.
Apps
Connect external apps so their events flow into LeadJourney as conversions – Stripe payments, Calendly and Cal.com bookings, and CRM activity from HubSpot, Attio, Close and Pipedrive – auto-attributed to the campaigns that drove them.
Connect Stripe
Connect your Stripe account in minutes – no API keys, no webhooks to wire by hand. Authorize once, then map Stripe events like successful payments, subscriptions and refunds straight onto your conversions.