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

Funnels

Define a funnel, then compute it: who reached each step, where they dropped out, and the same broken down by channel. Endpoints, parameters, and schemas.

Endpoint Method Path Permission
List funnels List funnels GET /v1/funnels funnels:read
Create funnel Create a funnel POST /v1/funnels funnels:write
Get funnel Get a funnel definition GET /v1/funnels/{id} funnels:read
Update funnel Update a funnel PATCH /v1/funnels/{id} funnels:write
Delete funnel Delete a funnel DELETE /v1/funnels/{id} funnels:write
Funnel breakdown Funnel split by dimension GET /v1/funnels/{id}/breakdown funnels:read
Compute funnel Compute a funnel GET /v1/funnels/{id}/steps funnels:read

Base URL https://app.sourceloop.ai/api/v1

List funnels

List funnels

GET /v1/funnels

Requires funnels:read

The funnels defined on this website: their steps, scope, ordering rule, time window and audience filter.

A funnel is an ordered list of 2-10 steps. Each step matches either the behavioural stream (pages, events, clicks) or the revenue stream (`source: "revenue"` with a subscription lifecycle event), so a single funnel can run from a page view through to money.

NOTE: this path used to serve CRM stage-to-stage conversion. That was a wrapper over the plan GET /v1/metrics already builds, and it held the name of a different feature. For stage conversion use GET /v1/metrics with `metrics=milestones` and `stages`. Two things to remember when you do: a stage a workspace does not use and a stage nobody reached are different answers, and stage counts are not a cohort, so a step rate above 100% is possible and means the later stage was fed from outside the window.

curl -s -X GET "https://app.sourceloop.ai/api/v1/funnels?website=acme.com" \
  -H "Authorization: Bearer $SOURCELOOP_API_KEY"
const res = await fetch(
  "https://app.sourceloop.ai/api/v1/funnels?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/funnels?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 One row per funnel definition
  • 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.

Create funnel

Create a funnel

POST /v1/funnels

Requires funnels:write

Define a funnel. `steps` is required and must hold 2 to 10 steps.

A behavioural step takes any of `event_type`, `path_matches`, `event_name_matches`, `url_matches`, `click_text_matches`. Use `*` as the wildcard. A revenue step takes `{"source":"revenue","revenue_event":"..."}`.

Steps are validated rather than stored as written: a matcher that could never match is rejected, because a funnel is a query every later comparison depends on, and a silent typo shows up months later as a funnel that "stopped working".

Mixing behavioural and revenue steps needs `scope: "visitor"`. Revenue is identity-keyed and web signups are anonymous-keyed, so `user` scope does not join them and the funnel collapses to zero after the first step.

curl -s -X POST "https://app.sourceloop.ai/api/v1/funnels" \
  -H "Authorization: Bearer $SOURCELOOP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Signup to paid",
    "scope": "visitor",
    "step_order": "any_order",
    "filters": {
      "device_type": "mobile"
    },
    "steps": [
      {
        "name": "Signed up",
        "conditions": {
          "event_name_matches": "signup*"
        }
      },
      {
        "name": "Started a trial",
        "conditions": {
          "source": "revenue",
          "revenue_event": "subscription_trial_started"
        }
      },
      {
        "name": "Converted to paid",
        "conditions": {
          "source": "revenue",
          "revenue_event": "subscription_trial_converted"
        }
      }
    ]
  }'
const res = await fetch(
  "https://app.sourceloop.ai/api/v1/funnels",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SOURCELOOP_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      "name": "Signup to paid",
      "scope": "visitor",
      "step_order": "any_order",
      "filters": {
        "device_type": "mobile"
      },
      "steps": [
        {
          "name": "Signed up",
          "conditions": {
            "event_name_matches": "signup*"
          }
        },
        {
          "name": "Started a trial",
          "conditions": {
            "source": "revenue",
            "revenue_event": "subscription_trial_started"
          }
        },
        {
          "name": "Converted to paid",
          "conditions": {
            "source": "revenue",
            "revenue_event": "subscription_trial_converted"
          }
        }
      ]
    }),
  },
);

const data = await res.json();
import os, requests

res = requests.post(
    "https://app.sourceloop.ai/api/v1/funnels",
    headers={"Authorization": f"Bearer {os.environ['SOURCELOOP_API_KEY']}"},
    json={
      "name": "Signup to paid",
      "scope": "visitor",
      "step_order": "any_order",
      "filters": {
        "device_type": "mobile"
      },
      "steps": [
        {
          "name": "Signed up",
          "conditions": {
            "event_name_matches": "signup*"
          }
        },
        {
          "name": "Started a trial",
          "conditions": {
            "source": "revenue",
            "revenue_event": "subscription_trial_started"
          }
        },
        {
          "name": "Converted to paid",
          "conditions": {
            "source": "revenue",
            "revenue_event": "subscription_trial_converted"
          }
        }
      ]
    },
)
data = res.json()

Responses

  • 200 The created funnel
  • 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 funnel

Get a funnel definition

GET /v1/funnels/{id}

Requires funnels:read

curl -s -X GET "https://app.sourceloop.ai/api/v1/funnels/{id}" \
  -H "Authorization: Bearer $SOURCELOOP_API_KEY"
const res = await fetch(
  "https://app.sourceloop.ai/api/v1/funnels/{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/funnels/{id}",
    headers={"Authorization": f"Bearer {os.environ['SOURCELOOP_API_KEY']}"},
)
data = res.json()

Path parameters

idstringpathrequired

Responses

  • 200 The funnel
  • 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.

Update funnel

Update a funnel

PATCH /v1/funnels/{id}

Requires funnels:write

Send only the fields you are changing. Editing steps changes what every historical comparison of this funnel means, so it is a deliberate act rather than a merge.

curl -s -X PATCH "https://app.sourceloop.ai/api/v1/funnels/{id}" \
  -H "Authorization: Bearer $SOURCELOOP_API_KEY"
const res = await fetch(
  "https://app.sourceloop.ai/api/v1/funnels/{id}",
  {
    method: "PATCH",
    headers: {
      Authorization: `Bearer ${process.env.SOURCELOOP_API_KEY}`,
    },
  },
);

const data = await res.json();
import os, requests

res = requests.patch(
    "https://app.sourceloop.ai/api/v1/funnels/{id}",
    headers={"Authorization": f"Bearer {os.environ['SOURCELOOP_API_KEY']}"},
)
data = res.json()

Path parameters

idstringpathrequired

Responses

  • 200 The updated funnel
  • 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.

Delete funnel

Delete a funnel

DELETE /v1/funnels/{id}

Requires funnels:write

Permanent. There is no archive state to fall back on.

curl -s -X DELETE "https://app.sourceloop.ai/api/v1/funnels/{id}" \
  -H "Authorization: Bearer $SOURCELOOP_API_KEY"
const res = await fetch(
  "https://app.sourceloop.ai/api/v1/funnels/{id}",
  {
    method: "DELETE",
    headers: {
      Authorization: `Bearer ${process.env.SOURCELOOP_API_KEY}`,
    },
  },
);

const data = await res.json();
import os, requests

res = requests.delete(
    "https://app.sourceloop.ai/api/v1/funnels/{id}",
    headers={"Authorization": f"Bearer {os.environ['SOURCELOOP_API_KEY']}"},
)
data = res.json()

Path parameters

idstringpathrequired

Responses

  • 200 Deleted
  • 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.

Funnel breakdown

Funnel split by dimension

GET /v1/funnels/{id}/breakdown

Requires funnels:read

Who was dropping out, not just where. A 40% drop-off is not actionable; "12% on desktop and 61% on mobile" is a bug report.

Credit for entering and completing is distributed across the dimension values a person touched, per the attribution model. Under `linear`, `position_based` and `time_decay` the counts are fractional, which is correct: a person who touched three channels is not three people.

curl -s -X GET "https://app.sourceloop.ai/api/v1/funnels/{id}/breakdown?period=last%2030%20days&dimension=channel&models=first_touch%2Clinear" \
  -H "Authorization: Bearer $SOURCELOOP_API_KEY"
const res = await fetch(
  "https://app.sourceloop.ai/api/v1/funnels/{id}/breakdown?period=last%2030%20days&dimension=channel&models=first_touch%2Clinear",
  {
    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/funnels/{id}/breakdown?period=last%2030%20days&dimension=channel&models=first_touch%2Clinear",
    headers={"Authorization": f"Bearer {os.environ['SOURCELOOP_API_KEY']}"},
)
data = res.json()

Path parameters

idstringpathrequired

Query parameters

periodstring

Plain-language period: "last 30 days", "yesterday", "July 2026", or "2026-07-01..2026-07-31". Ignored when from/to are given.

Example last 30 days

dimensionstring

Example channel

modelsstring

Comma-separated attribution models to compare side by side.

Example first_touch,linear

limitinteger

Default 50

Responses

  • 200 One row per dimension value
  • 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.

Compute funnel

Compute a funnel

GET /v1/funnels/{id}/steps

Requires funnels:read

One row per step: how many people reached it, the two conversion rates, and the median time to get there.

`reached` counts people who satisfied this step AND every step before it, so it is non-increasing by construction and a funnel can never widen.

`conv_vs_prev` and `conv_vs_first` are different numbers and quoting one as the other is the usual funnel mistake: 40% of the previous step is not 40% of the top.

Pass `compare_days=previous` to get the window immediately before this one in the same response, which is the only comparison where both periods are the same length by construction.

curl -s -X GET "https://app.sourceloop.ai/api/v1/funnels/{id}/steps?period=last%2030%20days&compare_days=previous" \
  -H "Authorization: Bearer $SOURCELOOP_API_KEY"
const res = await fetch(
  "https://app.sourceloop.ai/api/v1/funnels/{id}/steps?period=last%2030%20days&compare_days=previous",
  {
    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/funnels/{id}/steps?period=last%2030%20days&compare_days=previous",
    headers={"Authorization": f"Bearer {os.environ['SOURCELOOP_API_KEY']}"},
)
data = res.json()

Path parameters

idstringpathrequired

Query parameters

periodstring

Plain-language period: "last 30 days", "yesterday", "July 2026", or "2026-07-01..2026-07-31". Ignored when from/to are given.

Example last 30 days

compare_daysstring

"previous" for the window immediately before, or a number of days to shift back.

Example previous

Responses

  • 200 One row per step
  • 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.

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