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

  1. First run: call /leads?limit=500, optionally with updated_since.
  2. Upsert each item by id. A lead appears again whenever it changes: a new form submission, status or value.
  3. Follow next_cursor while has_more is true.
  4. Save the last next_cursor and 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