Three endpoints over behavioral analytics: list the funnels saved in the dashboard, run one (or an ad-hoc definition you compose on the fly), and run a path (Sankey) query showing what people do after a starting event.
List saved funnels
Saved funnels are shared config across live and test mode, so this endpoint returns the same list regardless of which key prefix you use.
curl https://www.vevee.org/api/v1/analytics/funnels \
-H "Authorization: Bearer sk_live_YOUR_KEY"{
"ok": true,
"data": {
"funnels": [
{
"id": "fnl_signup_to_paid",
"appId": "app_9f2c1a",
"name": "Signup to paid",
"definition": {
"steps": [
{ "events": [{ "kind": "event", "name": "signed_up" }] },
{ "events": [{ "kind": "event", "name": "onboarding_completed" }] },
{ "events": [{ "kind": "event", "name": "paywall_shown" }] },
{ "events": [{ "kind": "event", "name": "checkout_completed" }] }
],
"conversionWindowHours": 168,
"range": { "kind": "last_30_days" }
},
"createdAt": "2026-06-02T09:14:00.000Z",
"updatedAt": "2026-07-18T11:40:12.000Z"
}
]
}
}Run a funnel
Pass exactly one of funnelId (a saved funnel from the list above) or definition (an ad-hoc funnel spec, never persisted). Passing both, or neither, returns invalid_request.
By saved funnel id
curl https://www.vevee.org/api/v1/analytics/funnels/query \
-H "Authorization: Bearer sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "funnelId": "fnl_signup_to_paid" }'Ad-hoc definition
curl https://www.vevee.org/api/v1/analytics/funnels/query \
-H "Authorization: Bearer sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"definition": {
"steps": [
{ "events": [{ "kind": "event", "name": "paywall_shown" }] },
{ "events": [{ "kind": "event", "name": "checkout_completed" }] }
],
"conversionWindowHours": 24,
"range": { "kind": "last_7_days" }
}
}'The response wraps the result with the funnel's name (null for ad-hoc definitions):
{
"ok": true,
"data": {
"name": "Signup to paid",
"result": {
"totalEntered": 1240,
"totalConverted": 301,
"overallConversion": 0.2427,
"steps": [
{
"step": 1,
"label": "signed_up",
"optional": false,
"reachedCount": 1240,
"conversionFromStart": 1,
"conversionFromPrev": 1,
"avgTimeFromPrevSec": null
},
{
"step": 2,
"label": "onboarding_completed",
"optional": false,
"reachedCount": 812,
"conversionFromStart": 0.6548,
"conversionFromPrev": 0.6548,
"avgTimeFromPrevSec": 431
},
{
"step": 3,
"label": "paywall_shown",
"optional": false,
"reachedCount": 790,
"conversionFromStart": 0.6371,
"conversionFromPrev": 0.9729,
"avgTimeFromPrevSec": 96
},
{
"step": 4,
"label": "checkout_completed",
"optional": false,
"reachedCount": 301,
"conversionFromStart": 0.2427,
"conversionFromPrev": 0.381,
"avgTimeFromPrevSec": 218
}
]
}
}
}Run a path query
Starting from one event, walk each person's timeline forward up to maxSteps events within windowHours, and aggregate the transitions into a layered node/edge graph. Paths are stateless - there is no pathId to save; every call is ad-hoc.
curl https://www.vevee.org/api/v1/analytics/paths/query \
-H "Authorization: Bearer sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"start": { "name": "onboarding_completed" },
"maxSteps": 4,
"windowHours": 48,
"compressRepeats": true,
"minEdgeShare": 0.02,
"range": { "kind": "last_30_days" }
}'Parameters
| Name | Type | Description | |
|---|---|---|---|
start.name | required | string | Captured analytics event name to start every person's path from. |
maxSteps | required | number | Total nodes in the path including the start. Range 2 to 10. |
windowHours | required | number | Max wall-clock distance from the start event to still count. Range 1 to 168. |
compressRepeats | required | boolean | Collapse consecutive repeats of the same event name while walking. |
minEdgeShare | required | number | Drop edges whose share of totalStarters falls below this. Range 0 to 1. |
range | required | FunnelRange | Same relative/custom date range shape as funnels. |
{
"ok": true,
"data": {
"totalStarters": 812,
"totalCompleters": 214,
"nodes": [
{ "depth": 0, "event": "onboarding_completed", "count": 812, "share": 1 },
{ "depth": 1, "event": "paywall_shown", "count": 601, "share": 0.7401 },
{ "depth": 1, "event": "__dropoff__", "count": 211, "share": 0.2599 },
{ "depth": 2, "event": "checkout_completed", "count": 214, "share": 0.2635 },
{ "depth": 2, "event": "__dropoff__", "count": 387, "share": 0.4766 }
],
"edges": [
{ "depth": 0, "from": "onboarding_completed", "to": "paywall_shown", "count": 601, "share": 0.7401 },
{ "depth": 0, "from": "onboarding_completed", "to": "__dropoff__", "count": 211, "share": 0.2599 },
{ "depth": 1, "from": "paywall_shown", "to": "checkout_completed", "count": 214, "share": 0.2635 },
{ "depth": 1, "from": "paywall_shown", "to": "__dropoff__", "count": 387, "share": 0.4766 }
]
}
}Errors
invalid_key(401) - bad or revoked API key.requires_secret_key(403) - apk_*key was used.invalid_request(400) - malformed body, or both/neither offunnelIdanddefinitionsupplied.not_found(404) -funnelIddoes not match a saved funnel on this app.
list_funnels, run_funnel, and run_path_query - see MCP server. Funnel and path definitions are covered in depth in the Analytics & funnels guide.