Deals
CRM deals and the pipelines they move through, including values, stages and write-back.
| 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 |
| Update deal Move a deal, or change its value, in your CRM | PATCH | /v1/deals/{id} | deals:write |
| 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.
Update deal
Move a deal, or change its value, in your CRM
PATCH /v1/deals/{id}
Requires deals:write
Your CRM owns every field on a deal, so this does not write a value that lives only here: the change is saved and queued for push back to HubSpot or Pipedrive. Check `push.queued` in the response. When it is false the value was saved but NOT sent (outbound disabled, or the connection is not active) and the next sync from the CRM will overwrite it.
`status` is translated for you. HubSpot has no writable status field, because won and lost are stages there, so {"status":"won"} is resolved to that pipeline's won stage and `notes` reports which. Pipedrive accepts status directly. If the pipeline has no matching stage, HubSpot returns 422 asking for a stage_id from /pipelines.
Send an `Idempotency-Key`. A retry returns the original result rather than pushing to the CRM twice, and a request that changes nothing writes and pushes nothing.
`expected_revenue` is not writable: both providers compute it from amount and stage probability. Attribution is computed by Sourceloop and cannot be set.
curl -s -X PATCH "https://app.sourceloop.ai/api/v1/deals/{id}" \
-H "Authorization: Bearer $SOURCELOOP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "won",
"amount": 14000
}' const res = await fetch(
"https://app.sourceloop.ai/api/v1/deals/{id}",
{
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.SOURCELOOP_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"status": "won",
"amount": 14000
}),
},
);
const data = await res.json(); import os, requests
res = requests.patch(
"https://app.sourceloop.ai/api/v1/deals/{id}",
headers={"Authorization": f"Bearer {os.environ['SOURCELOOP_API_KEY']}"},
json={
"status": "won",
"amount": 14000
},
)
data = res.json() Path parameters
idstringpathrequired
Headers
Idempotency-KeystringheaderA unique token per distinct request. Strongly recommended: this writes to your CRM.
Request body
namestringamountnumbercurrencystringclose_datestringstatusstringAllowed values 3
openwonlost
stage_idstringFrom GET /pipelines. Takes precedence over status.
Responses
-
200Updated and queued for push, or unchanged -
403Something was wrong with the request or the credential. -
404Something was wrong with the request or the credential. -
422Something 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.