Skip to content New SourceLoop MCP: chat with your attribution data in Claude, ChatGPT & Cursor
SourceLoop
API reference

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

websitestring

Website domain, e.g. acme.com. Required only when the key covers more than one.

statusstring

Allowed values 3

  • open
  • won
  • lost

pipeline_idstring

stage_idstring

account_idstring

owner_emailstring

min_amountnumber

closed_afterstring

closed_beforestring

channelstring

First-touch channel that introduced the company.

sourcestring

First-touch source.

campaignstring

First-touch campaign.

latest_channelstring

sortstring

Allowed values 2

  • amount
  • updated

cursorstring

limitinteger

Default 50

total_countboolean

Include 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

  • 200 Deals with attribution
  • 400 Something was wrong with the request or the credential.
  • 429 Too 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

websitestring

Website domain, e.g. acme.com. Required only when the key covers more than one.

Responses

  • 200 Pipelines with stages
  • 400 Something was wrong with the request or the credential.
  • 429 Too 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

  • 200 Deal detail
  • 404 Something was wrong with the request or the credential.
  • 429 Too 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-Keystringheader

A unique token per distinct request. Strongly recommended: this writes to your CRM.

Request body

namestring

amountnumber

currencystring

close_datestring

statusstring

Allowed values 3

  • open
  • won
  • lost

stage_idstring

From GET /pipelines. Takes precedence over status.

Responses

  • 200 Updated and queued for push, or unchanged
  • 403 Something was wrong with the request or the credential.
  • 404 Something was wrong with the request or the credential.
  • 422 Something was wrong with the request or the credential.
  • 429 Too 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

daysinteger

Default 365

Responses

  • 200 Journey
  • 404 Something was wrong with the request or the credential.
  • 429 Too many requests for this workspace in the current minute. Wait Retry-After seconds and repeat the request unchanged.

Track every conversion to its true source

Capture and send full attribution data from every signup, lead, booking, and sale to your CRM and ad platforms, so you know exactly what's driving revenue.

Without SourceLoop

Untagged

Kayden Floyd

kayden@abc.com

  • SourceUnknown
  • MediumUnknown
  • CampaignUnknown
  • Landing pageUnknown
Journey
No touchpoints captured

With SourceLoop

Auto-tagged

Kayden Floyd

kayden@abc.com · Acme Co.

  • Channel Paid Social
  • CampaignFree_demo
  • Landing page/pricing
Journey
Synced to HubSpot Google Ads Meta