Contacts
The lead ledger: list contacts with their first and last touch, read one, and pull the full visitor journey behind it.
| Endpoint | Method | Path | Permission |
|---|---|---|---|
| List contacts Your contacts, one row per conversion | GET | /v1/contacts | conversions:read |
| Get contact One conversion, with its deals and value history | GET | /v1/contacts/{id} | conversions:read |
| Update contact Write an outcome back | PATCH | /v1/contacts/{id} | conversions:write |
| Get contact journey Every session behind one conversion, in order | GET | /v1/contacts/{id}/journey | conversions:read |
Base URL https://app.sourceloop.ai/api/v1
List contacts
Your contacts, one row per conversion
GET /v1/contacts
Requires conversions:read
One row per CONVERSION, not per person: someone who converts twice appears twice. `identity_id` is on every row for callers who need to group by person.
Returns your leads with their contact details. New keys include the `pii:read` scope by default, so email and phone come back in full.
If a key is created WITHOUT that scope, email, phone and name are masked and the email domain is preserved, so the row still identifies the company. Use that for keys given to contractors, reporting tools, or AI assistants.
Paginate with `cursor`, taking `next_cursor` from the previous response.
curl -s -X GET "https://app.sourceloop.ai/api/v1/contacts?website=acme.com&period=last%2030%20days&type=Web%20Form" \
-H "Authorization: Bearer $SOURCELOOP_API_KEY" const res = await fetch(
"https://app.sourceloop.ai/api/v1/contacts?website=acme.com&period=last%2030%20days&type=Web%20Form",
{
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SOURCELOOP_API_KEY}`,
},
},
);
const data = await res.json(); import os, requests
res = requests.get(
"https://app.sourceloop.ai/api/v1/contacts?website=acme.com&period=last%2030%20days&type=Web%20Form",
headers={"Authorization": f"Bearer {os.environ['SOURCELOOP_API_KEY']}"},
)
data = res.json() Query parameters
websitestringWebsite domain, e.g. acme.com. Required only when the key covers more than one.
periodstringPlain-language period: "last 30 days", "yesterday", "July 2026", or "2026-07-01..2026-07-31". Ignored when from/to are given.
Example
last 30 daystypestringExample
Web Formevent_namestringstatusstringemailstringRequires the pii:read scope: searching by email is reading it.
include_spambooleanDefault
falsecursorstringlimitintegerDefault
50total_countbooleanInclude meta.total_count, the exact number of matching rows. Off by default because counting scans every match while the page itself reads one page, so on a large workspace the count costs far more than the rows. Paginate with has_more and next_cursor unless you are rendering "70 of 366".
Default
false
Responses
-
200Conversions -
403Something was wrong with the request or the credential. -
429Too many requests for this workspace in the current minute. Wait Retry-After seconds and repeat the request unchanged.
Get contact
One conversion, with its deals and value history
GET /v1/contacts/{id}
Requires conversions:read
Adds the CRM deals this person is attached to and the full history of value changes, each with the reason and the system that made it.
curl -s -X GET "https://app.sourceloop.ai/api/v1/contacts/{id}" \
-H "Authorization: Bearer $SOURCELOOP_API_KEY" const res = await fetch(
"https://app.sourceloop.ai/api/v1/contacts/{id}",
{
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SOURCELOOP_API_KEY}`,
},
},
);
const data = await res.json(); import os, requests
res = requests.get(
"https://app.sourceloop.ai/api/v1/contacts/{id}",
headers={"Authorization": f"Bearer {os.environ['SOURCELOOP_API_KEY']}"},
)
data = res.json() Path parameters
idstringpathrequired
Responses
-
200Conversion detail -
404Something was wrong with the request or the credential. -
429Too many requests for this workspace in the current minute. Wait Retry-After seconds and repeat the request unchanged.
Update contact
Write an outcome back
PATCH /v1/contacts/{id}
Requires conversions:write
How your CRM tells Sourceloop that a lead became worth 14,000, which turns cost-per-lead reporting into cost-per-revenue reporting.
Send an `Idempotency-Key` header. A retried request returns the original result rather than writing twice, and a request that changes nothing writes nothing at all. Both matter because a conversion write propagates to your connected CRM and to ad-platform conversion upload.
Attribution fields are computed by Sourceloop and cannot be set.
curl -s -X PATCH "https://app.sourceloop.ai/api/v1/contacts/{id}" \
-H "Authorization: Bearer $SOURCELOOP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "won",
"sales_value": 14000,
"currency": "USD"
}' const res = await fetch(
"https://app.sourceloop.ai/api/v1/contacts/{id}",
{
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.SOURCELOOP_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"status": "won",
"sales_value": 14000,
"currency": "USD"
}),
},
);
const data = await res.json(); import os, requests
res = requests.patch(
"https://app.sourceloop.ai/api/v1/contacts/{id}",
headers={"Authorization": f"Bearer {os.environ['SOURCELOOP_API_KEY']}"},
json={
"status": "won",
"sales_value": 14000,
"currency": "USD"
},
)
data = res.json() Path parameters
idstringpathrequired
Headers
Idempotency-KeystringheaderA unique token per distinct request. Strongly recommended.
Request body
statusstringsales_valuenumberquote_valuenumbercurrencystringnotesstringlead_status_rawstringlifecycle_stage_rawstringis_spamboolean
Responses
-
200Updated, or unchanged -
404Something was wrong with the request or the credential. -
429Too many requests for this workspace in the current minute. Wait Retry-After seconds and repeat the request unchanged.
Get contact journey
Every session behind one conversion, in order
GET /v1/contacts/{id}/journey
Requires conversions:read
The evidence the attribution numbers rest on: how this person arrived each time, and what they did once there.
Devices are merged via the identity graph first, so someone who browsed on a phone and converted on a laptop is one timeline rather than two half-journeys.
When no raw events exist (a CRM import, or a server-side conversion) the timeline is reconstructed from stored first-touch and last-touch attribution and flagged synthetic:true. Do not present that as a complete history.
curl -s -X GET "https://app.sourceloop.ai/api/v1/contacts/{id}/journey?days=365&limit=2000" \
-H "Authorization: Bearer $SOURCELOOP_API_KEY" const res = await fetch(
"https://app.sourceloop.ai/api/v1/contacts/{id}/journey?days=365&limit=2000",
{
method: "GET",
headers: {
Authorization: `Bearer ${process.env.SOURCELOOP_API_KEY}`,
},
},
);
const data = await res.json(); import os, requests
res = requests.get(
"https://app.sourceloop.ai/api/v1/contacts/{id}/journey?days=365&limit=2000",
headers={"Authorization": f"Bearer {os.environ['SOURCELOOP_API_KEY']}"},
)
data = res.json() Path parameters
idstringpathrequired
Query parameters
daysintegerDefault
365limitintegerDefault
2000
Responses
-
200Journey -
404Something was wrong with the request or the credential. -
429Too many requests for this workspace in the current minute. Wait Retry-After seconds and repeat the request unchanged.