Leads Sync API
When totals from the Reporting API are not enough, these endpoints return the leads themselves, so you can keep a copy in your own database and fetch only what changed.
You need an API key and the project ID. All endpoints are GET under https://api.tagpilot.io/integrations/v1/lead-tracking/projects.
| Path | Returns |
|---|---|
/{projectId}/leads |
Leads that changed, oldest change first, each with its source and status history |
/{projectId}/leads/{leadId} |
One lead, plus every form submission (captures) with its own source |
/{projectId}/transitions |
Status changes in the order Tag Pilot received them |
/leads and /transitions take these parameters:
| Parameter | Meaning |
|---|---|
limit |
Items per page, 1–500; default 100 |
updated_since |
ISO 8601 date-time, e.g. 2026-10-01T00:00:00Z: start from changes at or after it |
cursor |
The next_cursor of a previous page; takes priority over updated_since |
A lead
{
"id": "01a16539-4dfb-7c28-880a-250d59ba8999",
"email_masked": "j******e@g***l.com",
"status": "Prospect",
"status_changed_at": "2026-10-21T10:00:00Z",
"value": null,
"currency": null,
"capture_count": 1,
"last_captured_at": "2026-10-20T14:12:09Z",
"created_at": "2026-10-20T14:12:09Z",
"updated_at": "2026-10-21T10:05:31Z",
"source": {
"utm_source": "google",
"utm_medium": "cpc",
"utm_campaign": "q4_coaching",
"utm_content": null,
"utm_term": null,
"click_id_types": ["gclid"],
"cookie_click_id_types": [],
"referrer_host": "www.google.com",
"landing": null
},
"transitions": [
{
"id": "1f1ce542-21bd-682e-bd3b-3187131ed527",
"status": "Prospect",
"occurred_at": "2026-10-21T10:00:00Z",
"received_at": "2026-10-21T10:05:31Z",
"value": null,
"currency": null,
"conversion_sent_at": "2026-10-21T10:05:32Z"
}
]
}
| Field | Meaning |
|---|---|
id |
Stable lead ID; use it as your primary key |
status |
Latest CRM status, null until the CRM reports one |
capture_count |
Form submissions matched to the lead; 0 = known only from the CRM |
updated_at |
Last change to the lead; the feed is ordered by it |
source |
Where the latest form submission came from; null for CRM-only leads |
source.click_id_types |
Ad click IDs on the form page, e.g. gclid (Google Ads), fbclid (Meta), msclkid (Microsoft) |
source.cookie_click_id_types |
Click IDs found only in ad cookies: an earlier ad click, possibly weeks old |
source.landing |
The visitor's last landing with UTMs, a click ID or a referrer before the form, with page and landed_at |
transitions |
Every status the lead reached, oldest first; conversion_sent_at = when it was sent to ad platforms |
Syncing
- First run: call
/leads?limit=500, optionally withupdated_since. - Upsert each item by
id. A lead appears again whenever it changes: a new form submission, status or value. - Follow
next_cursorwhilehas_moreistrue. - Save the last
next_cursorand start the next run from it. When nothing changed, you get an empty page with the same cursor, so it is always safe to save.
Changes from the last two minutes are held back until they are fully saved, so a cursor never skips one; polling more often than every few minutes gains nothing. Leads with no activity for the project's retention_days are deleted and stop appearing: drop them on your side too.
import json, urllib.parse, urllib.request
BASE = "https://api.tagpilot.io/integrations/v1/lead-tracking/projects"
HEADERS = {"Authorization": "Bearer " + KEY, "User-Agent": "acme-dashboard/1.0"}
def sync_leads(project_id, cursor=None):
"""Fetch every lead changed since `cursor`; return the cursor to save."""
while True:
query = {"limit": 500, **({"cursor": cursor} if cursor else {})}
url = f"{BASE}/{project_id}/leads?{urllib.parse.urlencode(query)}"
with urllib.request.urlopen(urllib.request.Request(url, headers=HEADERS)) as r:
page = json.load(r)
for lead in page["items"]:
upsert_lead(lead) # your storage, keyed by lead["id"]
cursor = page["next_cursor"]
if not page["has_more"]:
return cursor