Push leads into Leadtrak from anywhere
One endpoint, one key. Send a lead from your own website form, your CRM, Zapier, a call tracking provider or a spreadsheet script — and it is scored, routed, notified and followed up exactly like a lead that came through a Leadtrak funnel.
Quick start
Create a key under Settings → API keys, then post a lead. That is the whole integration.
curl -X POST https://app.leadtrak.ai/api/v1/leads \
-H "Authorization: Bearer lt_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"email": "jane@example.com",
"phone": "07700 900123",
"name": "Jane Smith",
"source": "website-contact-form",
"answers": {
"budget": "over-10k",
"timescale": "this month",
"message": "Need a quote for a new roof"
}
}'
You get back the lead as we stored it:
{
"ok": true,
"duplicate": false,
"lead": {
"id": "8f2c1a9e4b7d3c05",
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "07700900123",
"score": 0,
"status": "new",
"source": "website-contact-form",
"created_at": "2026-08-19T09:14:22+00:00",
"answers": { "budget": "over-10k", "timescale": "this month", "message": "..." }
}
}
Note what changed. The phone came in as
07700 900123 and was stored as 07700900123.
Email is lowercased and trimmed. Names arriving in ALL CAPS or all
lowercase are tidied, and names already correctly cased are left alone.
This normalisation happens on every route in, so your data stays
consistent whether it came from a funnel or from you.
Authentication
Every request carries a write key. Create one per integration — that way, if one leaks, you revoke that one rather than breaking everything.
Preferred:
Authorization: Bearer lt_your_key_here
Also accepted:
X-API-Key: lt_your_key_here
A write_key field in the JSON body still works, because the
WordPress plugin sends it that way. Prefer a header for anything new:
request bodies get written to proxy logs and error trackers far more
casually than headers do.
POST /api/v1/leads
Fields
| Field | Type | Notes |
|---|---|---|
email | string | Required unless phone is given. |
phone | string | Required unless email is given. Punctuation is fine. |
name | string | Optional. Up to 190 characters. |
answers | object | Anything else you collected. Shown on the lead and in the notification email. |
score | integer | 0–100. Use it if you already qualify leads your side. |
funnel_id | integer | Attribute the lead to one of your funnels. Ignored if it is not yours. |
source | string | Where it came from. utm_source is accepted as an alias. |
medium | string | utm_medium also accepted. |
campaign | string | utm_campaign also accepted. |
referrer | string | The page they came from. |
landing_url | string | The page they landed on. |
visitor_id | string | The Leadtrak tracking id, if you have it. This is what links the lead to the ad that produced it. |
dedupe_hours | integer | Duplicate window. Defaults to 24. Set 0 to always create a new lead. |
Responses
| Status | Meaning |
|---|---|
201 | Lead created. |
200 | An existing lead was updated — a duplicate, or an idempotent replay. |
401 | Missing or invalid key. |
402 | Your plan's monthly lead limit is reached. Upgrade to keep capturing. |
422 | Validation failed. The response names the field. |
429 | More than 120 requests in a minute on this key. Retry after 60 seconds. |
A validation failure tells you exactly what was wrong:
{
"ok": false,
"error": "validation_failed",
"message": "The lead was rejected. See errors.",
"errors": { "email": "That is not a valid email address." }
}
Validation
A lead is only ever rejected when there is no way to contact the person. Everything else is accepted and cleaned up.
-
Email must be a real-looking address with a proper
top-level domain.
jane@exampleandjane@example.care refused; they only ever produce bounces, and bounces damage your sending reputation. -
Phone must be 7–15 digits once punctuation is
stripped.
+44 (0)7700 900123is fine. -
One good field is enough. If the email is valid and
the phone is nonsense, the lead is still created — we drop the bad
phone from its column and keep what was typed in
answers.phone_as_givenso you can see it and correct it. Losing a contactable lead over an optional field would be the wrong trade every time.
Duplicates and retries
By default, the same email or phone arriving again within 24
hours updates the existing lead instead of creating a second
one. The response has "duplicate": true and the same
lead.id.
When merging, we are deliberately conservative:
- Blank fields are filled in from the newer submission.
- Fields we already have are never overwritten.
- New answers are added to the existing ones.
- A higher score wins; a lower one is ignored.
- The repeat is recorded on the lead's timeline — someone coming back is a buying signal worth seeing.
Set "dedupe_hours": 0 if every submission really is a separate lead.
Idempotency
If your integration retries after a timeout, send an
Idempotency-Key header. We remember what that key produced
for 24 hours and replay the same answer rather than creating a second
lead.
curl -X POST https://app.leadtrak.ai/api/v1/leads \
-H "Authorization: Bearer lt_your_key_here" \
-H "Idempotency-Key: order-10021" \
-H "Content-Type: application/json" \
-d '{"email":"jane@example.com"}'
The replay comes back as 200 with
"replayed": true.
What happens to a lead once it arrives
This is the part worth understanding. A lead sent over the API is not just stored — it goes through the same nine steps as every lead captured by a Leadtrak funnel, in this order.
-
Validated. If there is no usable email or phone, it is
rejected with a
422and nothing is written. You find out immediately rather than discovering months later that a third of your leads were junk. -
Checked against your plan. If you are at your monthly
limit you get a
402and no lead is created. On a paid plan over its included volume, the overage is recorded for billing and the lead proceeds. - Deduplicated. A repeat within the window merges into the existing lead.
- Normalised. Email lowercased and trimmed, phone reduced to digits, names tidied. This runs through the same hook your own customisations can extend.
- Saved. The lead row is written before anything slow runs. Everything after this point can fail without costing you the lead — that is the single most important rule in the system.
- Counted. Usage is recorded against your plan.
-
Attributed. If you sent a
visitor_id, the lead is joined to that visitor's earlier touchpoints and credited across all four attribution models — first touch, last touch, linear and time decay. This is what makes cost-per-qualified-lead possible. - Notified. A notification job is queued. Your routing rules run inside it and decide who hears about this lead, and who it is assigned to. Nothing is emailed inline, so a slow mail server never slows down your form.
-
Fanned out. The
lead_capturedevent fires, which:- enrols the lead in any matching follow-up sequence;
- opens a conversation so it appears in your inbox from minute zero;
- fires your webhooks with a
lead.createdevent, signed with HMAC-SHA256.
After that
From there the lead behaves like any other: it appears in your inbox and
lead list, it can be assigned, tagged, moved through a pipeline, and it
feeds the ROI dashboard. Changing its status fires
lead.status_changed to your webhooks, plus a specific event
when it becomes qualified, won or lost.
Connecting a lead to the ad that produced it
If you want cost-per-lead by source to be accurate, send the
visitor_id. The Leadtrak tracking script writes it into a
first-party cookie and into localStorage under
lt_vid; read it in your form handler and pass it through.
// on your page
const visitorId =
localStorage.getItem('lt_vid') ||
document.cookie.match(/(?:^|;\s*)lt_vid=([^;]+)/)?.[1] ||
'';
// send it to YOUR server with the form, then include it in the API call
Without it, the lead is still captured — it just starts a fresh attribution timeline instead of joining the one that began when the person clicked your ad.
Other ways to get leads in
Leadtrak funnels
The built-in route. A published funnel captures, scores and attributes with no configuration at all, and the tracking pixel ships underneath it automatically.
WordPress plugin
Drop-in support for Contact Form 7, Gravity Forms, WPForms and Elementor. Submissions from any of them arrive as leads, through the same intake as everything else.
Direct funnel capture
POST /api/capture/{slug} posts answers straight to one of
your funnels, scored against that funnel's own weights. Useful for a
custom front end on a Leadtrak funnel.
Zapier and friends
Anything that can make an HTTP POST works. Use the endpoint on this
page with a Bearer key, and set an Idempotency-Key if the
platform retries.
Handling errors well
-
Retry on 429 and 5xx, with a backoff. Send the same
Idempotency-Keyso a retry cannot double up. - Do not retry on 422. The data is wrong; retrying it unchanged will fail identically. Log it and fix the source.
- Treat 402 as a signal, not a bug. It means the account is at its plan limit. Alert someone rather than silently dropping leads.
-
Always check
duplicate. If you are counting submissions, a200with"duplicate": trueis not a new lead.
Something not working?
Post a lead with a deliberately bad email and check you get a
422 — that confirms your key and your request shape are
both right, without creating test data. If it still is not behaving,
get in touch and include the response body.