Developer guide

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.

1
Endpoint to learn
9
Things that happen automatically on arrival
120
Requests per minute, per key
24h
Default duplicate window

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.

A write key can create leads for your whole account. Keep it server-side. Do not put it in front-end JavaScript, a mobile app or anything a visitor can view the source of. If you need to capture from a browser, post to your own server first and forward it from there.

POST /api/v1/leads

Fields

FieldTypeNotes
emailstringRequired unless phone is given.
phonestringRequired unless email is given. Punctuation is fine.
namestringOptional. Up to 190 characters.
answersobjectAnything else you collected. Shown on the lead and in the notification email.
scoreinteger0–100. Use it if you already qualify leads your side.
funnel_idintegerAttribute the lead to one of your funnels. Ignored if it is not yours.
sourcestringWhere it came from. utm_source is accepted as an alias.
mediumstringutm_medium also accepted.
campaignstringutm_campaign also accepted.
referrerstringThe page they came from.
landing_urlstringThe page they landed on.
visitor_idstringThe Leadtrak tracking id, if you have it. This is what links the lead to the ad that produced it.
dedupe_hoursintegerDuplicate window. Defaults to 24. Set 0 to always create a new lead.

Responses

StatusMeaning
201Lead created.
200An existing lead was updated — a duplicate, or an idempotent replay.
401Missing or invalid key.
402Your plan's monthly lead limit is reached. Upgrade to keep capturing.
422Validation failed. The response names the field.
429More 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@example and jane@example.c are 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 900123 is 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_given so 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.

  1. Validated. If there is no usable email or phone, it is rejected with a 422 and nothing is written. You find out immediately rather than discovering months later that a third of your leads were junk.
  2. Checked against your plan. If you are at your monthly limit you get a 402 and no lead is created. On a paid plan over its included volume, the overage is recorded for billing and the lead proceeds.
  3. Deduplicated. A repeat within the window merges into the existing lead.
  4. Normalised. Email lowercased and trimmed, phone reduced to digits, names tidied. This runs through the same hook your own customisations can extend.
  5. 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.
  6. Counted. Usage is recorded against your plan.
  7. 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.
  8. 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.
  9. Fanned out. The lead_captured event 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.created event, signed with HMAC-SHA256.
Everything after step 5 is best-effort. If a webhook endpoint is down or a sequence is misconfigured, the lead is still saved and still in your inbox. A broken integration can never cost you a lead.

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.

About the funnel builder →

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-Key so 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, a 200 with "duplicate": true is 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.