Deals
CRM deals and the pipelines they move through, with their values, stages and the marketing behind each one. Endpoints, parameters, and schemas.
| Endpoint | Method | Path | Permission |
|---|---|---|---|
| List deals Your pipeline, with the attribution we computed for it | GET | /v1/deals | deals:read |
| List pipelines Pipelines and their stages, in order | GET | /v1/pipelines | deals:read |
| Get deal One deal, with its people and its history | GET | /v1/deals/{id} | deals:read |
| Get deal journey The marketing behind one deal | GET | /v1/deals/{id}/journey | deals:read |
Base URL https://app.sourceloop.ai/api/v1
List deals
Your pipeline, with the attribution we computed for it
GET /v1/deals
Requires deals:read
Deals mirrored from your CRM, each carrying the first-touch and last-touch attribution Sourceloop derived by walking the touchpoints of everyone linked to the deal. Your CRM does not hold those fields, which is the reason to read deals here rather than from the CRM API.
Every row includes `stage.normalized_bucket` (open, won, lost, other). Stage names are chosen by the customer, so a report that trusts the label breaks the day someone renames a stage.
Returns a connection-required error, not an empty list, when no CRM is connected. "No deals" and "no CRM" are different answers.
curl -s -X GET "https://app.sourceloop.ai/api/v1/deals?website=acme.com" \
-H "Authorization: Bearer $SOURCELOOP_API_KEY" const res = await fetch(
"https://app.sourceloop.ai/api/v1/deals?website=acme.com",
{
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/deals?website=acme.com",
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.
statusstringAllowed values 3
openwonlost
pipeline_idstringstage_idstringaccount_idstringowner_emailstringmin_amountnumberclosed_afterstringclosed_beforestringchannelstringFirst-touch channel that introduced the company.
sourcestringFirst-touch source.
campaignstringFirst-touch campaign.
latest_channelstringsortstringAllowed values 2
amountupdated
cursorstringlimitintegerDefault
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
-
200Deals with attribution -
400Something 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.
List pipelines
Pipelines and their stages, in order
GET /v1/pipelines
Requires deals:read
Needed to interpret a deal's stage: which stages exist, what order they run in, their win probability, and which of the customer's stage names actually mean won.
curl -s -X GET "https://app.sourceloop.ai/api/v1/pipelines?website=acme.com" \
-H "Authorization: Bearer $SOURCELOOP_API_KEY" const res = await fetch(
"https://app.sourceloop.ai/api/v1/pipelines?website=acme.com",
{
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/pipelines?website=acme.com",
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.
Responses
-
200Pipelines with stages -
400Something 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 deal
One deal, with its people and its history
GET /v1/deals/{id}
Requires deals:read
Adds the contacts on the deal, the append-only stage history (which survives a stage being renamed later, so time-in-stage stays computable) and the pipeline it belongs to.
curl -s -X GET "https://app.sourceloop.ai/api/v1/deals/{id}" \
-H "Authorization: Bearer $SOURCELOOP_API_KEY" const res = await fetch(
"https://app.sourceloop.ai/api/v1/deals/{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/deals/{id}",
headers={"Authorization": f"Bearer {os.environ['SOURCELOOP_API_KEY']}"},
)
data = res.json() Path parameters
idstringpathrequired
Responses
-
200Deal 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.
Get deal journey
The marketing behind one deal
GET /v1/deals/{id}/journey
Requires deals:read
Every session by every contact attached to the deal. The closed loop stated in full: not "paid search influenced 60,000 of pipeline", but the sessions that claim actually rests on.
curl -s -X GET "https://app.sourceloop.ai/api/v1/deals/{id}/journey?days=365" \
-H "Authorization: Bearer $SOURCELOOP_API_KEY" const res = await fetch(
"https://app.sourceloop.ai/api/v1/deals/{id}/journey?days=365",
{
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/deals/{id}/journey?days=365",
headers={"Authorization": f"Bearer {os.environ['SOURCELOOP_API_KEY']}"},
)
data = res.json() Path parameters
idstringpathrequired
Query parameters
daysintegerDefault
365
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.