API reference
The Lead Source public API lets an external integration read your leads and where they came from, on your behalf, after you connect it to your Lead Source account. It is the same API the Lead Source apps for Zapier and Make use.
Overview
Section titled “Overview”- Base URL:
https://app.leadsource.co - Format: JSON. Every response is
application/json. - Authentication: OAuth 2.0 authorization code with PKCE (see below). Every data request
carries an
Authorization: Bearer <access_token>header. - Permissions: read-only. The only scope is
leads:read. - Tenant scope: a connection is bound to one Lead Source account; if the connecting user is limited to specific websites, the API returns data only for those websites.
Authentication (OAuth 2.0 + PKCE)
Section titled “Authentication (OAuth 2.0 + PKCE)”Lead Source is a confidential OAuth client with mandatory PKCE (S256). Registration of a
client (client_id / client_secret / redirect URI) is arranged with Lead Source support.
1. Authorize
Section titled “1. Authorize”Send the user to the authorize endpoint. They sign in and approve access, then get
redirected back to your redirect_uri with a one-time code (and your state).
GET https://app.leadsource.co/api/oauth/authorize ?client_id=<your client id> &redirect_uri=<your allow-listed redirect uri> &response_type=code &scope=leads:read &state=<opaque csrf value> &code_challenge=<base64url(sha256(code_verifier))> &code_challenge_method=S256code_challenge + code_challenge_method=S256 are required. An unknown client or a
redirect URI that is not allow-listed is rejected with 400.
2. Exchange the code for tokens
Section titled “2. Exchange the code for tokens”POST https://app.leadsource.co/api/oauth/tokenContent-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=<code from step 1>&client_id=<your client id>&client_secret=<your client secret>&redirect_uri=<same redirect uri as step 1>&code_verifier=<the original code verifier>Response:
{ "access_token": "ls_oat_…", "token_type": "bearer", "expires_in": 3600, "refresh_token": "ls_ort_…", "scope": "leads:read"}3. Refresh (rotating)
Section titled “3. Refresh (rotating)”Access tokens expire (expires_in seconds). Exchange the refresh token for a new pair.
Refresh tokens rotate: each refresh returns a new refresh_token — store it and discard
the old one. Re-presenting a consumed refresh token revokes the whole token family.
POST https://app.leadsource.co/api/oauth/tokenContent-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=<current refresh token>&client_id=<your client id>&client_secret=<your client secret>Authenticated requests
Section titled “Authenticated requests”Send the access token on every data request:
Authorization: Bearer <access_token>Endpoints
Section titled “Endpoints”All endpoints require the Authorization: Bearer header and the leads:read scope.
Connection — GET /api/v1/me
Section titled “Connection — GET /api/v1/me”Returns the connected account (used to label the connection and to check the token still works).
{ "account_id": "…", "account_name": "Acme Co (owner@acme.com)", "scopes": ["leads:read"], "connection_id": "…"}Sites — GET /api/v1/sites
Section titled “Sites — GET /api/v1/sites”Lists the active websites on the connected account, ordered by domain.
{ "data": [ { "id": "…", "domain": "example.com" } ] }Forms — GET /api/v1/sites/{siteId}/forms
Section titled “Forms — GET /api/v1/sites/{siteId}/forms”Lists the genuine detected forms for one website (ordered by lead volume). {siteId} is a
site id from the sites endpoint. A malformed or unknown site id returns an empty list.
{ "data": [ { "id": "…", "form_identifier": "jotform:2412…", "label": "/contact/ (42 leads)", "page_path": "/contact/", "lead_count": 42 } ]}The id here is the value to pass as detected_form_id when filtering leads.
New leads (polling) — GET /api/v1/triggers/leads
Section titled “New leads (polling) — GET /api/v1/triggers/leads”Returns leads newest-first for polling integrations. It excludes spam, test, and deleted
leads. Poll on your own schedule and de-duplicate on each lead’s id.
| Query param | Required | Description |
|---|---|---|
site_id | no | Only leads from this website (a site id). Omit for all sites. |
detected_form_id | no | Only leads from this form (a form id). Omit for all forms. |
limit | no | Page size, 1–100. Default 50. |
cursor | no | Opaque cursor from a prior response’s next_cursor, for the next page. |
{ "data": [ /* Lead objects, newest first */ ], "next_cursor": "…" }next_cursor is null on the last page. site_id / detected_form_id must be valid UUIDs,
else 400 invalid_filter; a malformed cursor is 400 invalid_cursor.
Find a lead — GET /api/v1/actions/find-lead
Section titled “Find a lead — GET /api/v1/actions/find-lead”Exact email lookup (case- and whitespace-insensitive). Returns 0 or 1 result.
GET /api/v1/actions/find-lead?email=person@example.com{ "data": [ /* 0 or 1 Lead object */ ] }Objects
Section titled “Objects”| Field | Type | Notes |
|---|---|---|
id | string (uuid) | Use as {siteId} and as the site_id filter. |
domain | string | The website domain. |
| Field | Type | Notes |
|---|---|---|
id | string (uuid) | Use as the detected_form_id filter. |
form_identifier | string | Raw detected identifier (e.g. jotform:…). |
label | string | Human-readable label for a picker. |
page_path | string | Page the form was seen on. |
lead_count | integer | Real lead count for the form. |
Contact details are returned in full (never masked).
| Field | Type | Notes |
|---|---|---|
id | string (uuid) | Stable unique id — de-duplicate polling on this. |
created_at | string | UTC ISO-8601 with milliseconds. |
name | string / null | Lead’s name. |
email | string / null | Lead’s email (lowercased). |
phone | string / null | Lead’s phone, as captured. |
source | string | Where they came from (display name). |
source_type | string / null | Source category. |
source_detail | string / null | Extra source detail. |
source_confidence | string / null | Attribution confidence. |
first_touch_source | string / null | First-touch source, as a JSON string. |
form_name | string / null | The form the lead came through. |
site_domain | string / null | The website the lead came from. |
raw_source | string / null | Raw source data, as a JSON string (campaign tracking parameters, referrer, page). |
fields_raw | string / null | The submitted form fields, as a JSON string. |
Pagination & polling
Section titled “Pagination & polling”The New leads endpoint is keyset-paginated and newest-first. For a full backfill, follow
next_cursor until it is null. For ongoing polling, request the newest page each interval
and de-duplicate on id — the standard polling-trigger pattern.
Rate limits
Section titled “Rate limits”Per connection (or per client / IP for the OAuth endpoints). Over the limit returns
429 rate_limited with a Retry-After header (seconds).
| Endpoint(s) | Limit |
|---|---|
POST /api/oauth/token | 30 / minute / client, and 10 / minute / IP |
GET /api/oauth/authorize | 20 / minute / IP |
GET /api/v1/sites, …/forms | 60 / minute / connection |
GET /api/v1/triggers/leads | 120 / minute and 600 / hour / connection |
Errors
Section titled “Errors”Errors use { "error": "<code>", "message": "<detail>" } with a matching HTTP status.
| Status | error | Meaning |
|---|---|---|
400 | invalid_grant | Bad/expired/used code, redirect-uri mismatch, or PKCE mismatch. |
400 | invalid_filter / invalid_cursor | A filter id is not a UUID, or the cursor is malformed. |
401 | invalid_token / token_expired | Missing/invalid access token, or it has expired (refresh it). |
401 | invalid_client | Unknown client id or wrong client secret at the token step. |
403 | insufficient_scope | The token lacks the required scope. |
429 | rate_limited | Slow down; retry after Retry-After seconds. |