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
websitestringWebsite domain, e.g. acme.com. Required only when the key covers more than one.
Responses
-
200One row per funnel definition -
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.
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
-
200The created funnel -
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 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
-
200The funnel -
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.
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
-
200The updated funnel -
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.
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
-
200Deleted -
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.
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
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 daysdimensionstringExample
channelmodelsstringComma-separated attribution models to compare side by side.
Example
first_touch,linearlimitintegerDefault
50
Responses
-
200One row per dimension value -
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.
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
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 dayscompare_daysstring"previous" for the window immediately before, or a number of days to shift back.
Example
previous
Responses
-
200One row per step -
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.