# Convertlyft read API — worked examples # version 2026-10-09. Short version: https://convertlyft.com/llms.txt # Every call below is GET and read-only. $TOKEN is a cvl_pat_ token. The one # write on THIS surface, POST /api/memory, is not shown here — it is the ledger, # not a report, and it changes nothing you measure. The MCP door has more. ## The one that fails silently # from is OPTIONAL and its absence means ALL TIME. This call succeeds, the numbers # are real, and they answer a question nobody asked: curl -s "https://convertlyft.com/api/reports/kpi" -H "Authorization: Bearer $TOKEN" # Always send from. If you truly want all time, send it empty ON PURPOSE. ## Dates # from/to on the routes below that take a range are ISO 8601 timestamps, # 2026-08-01T00:00:00.000Z. A date alone (2026-08-01) is accepted and means the # whole UTC day: from is its first instant, to its last — the same rule as the MCP # door. A window whose from is after its to is a 400 ("the window runs backwards"). # Errors are application/problem+json (RFC 9457) and keep code and error. ## Read a figure without asserting on a number that does not exist curl -s "https://convertlyft.com/api/reports/kpi?from=2026-08-01T00:00:00.000Z" -H "Authorization: Bearer $TOKEN" \ | jq -e 'if .kpis.conversion.state == "ok" then .kpis.conversion.value >= 0.015 else .kpis.conversion.reason | halt_error(0) end' # state is checked BEFORE the value, and the value key is NOT THERE when state is not # "ok" — a workspace with no Purchase stage mapped, or one whose sessions are under its # own min_sample, has no conversion rate at all. jq reads a missing key as null and # null >= 0.015 is false, so branch on state and print the server's own reason. # Every figure has this shape: state, display, and value+previous+delta only when ok. # ONE key under kpis is not a figure and a loop over .kpis will find nothing in it: # kpis.health is a SELECTOR. Its tile key says which of its two figures the sixth tile # shows, and both are published whatever it says, so read .kpis.health.crash_free_rate # and .kpis.health.lcp_p75_ms — each of those HAS the shape above. ## Quota — branch on the bucket, never sleep on the day one # 429 body: {"error": "...", "bucket": "minute" | "day", "limit": 120} # minute -> back off and retry. day -> stop, log, let the schedule pick it up tomorrow. ## Pin one window across a chain of calls # 1. call any of the seven /api/reports/* endpoints with from/to # 2. keep .window.handle from the response # 3. send ?window_handle= on every later call to those seven # Without it a nine-call chain spans nine slightly different windows and the figures stop adding up. ## GET /api/tools/[name] scopes: the original call's scopes range: none — Takes no time range. Its answer is all time, always; from/to change nothing. what: Any read tool of the catalogue, by its name — the same reading the MCP door serves, as JSON. Arguments ride the query string; ?fields= narrows the payload. curl -s "https://convertlyft.com/api/tools/[name]" -H "Authorization: Bearer $TOKEN" ## GET /api/reports/kpi scopes: sessions:read + reports:read (every one, not any one — holding some is a 403) range: sealed — Echoes window{from,to,handle,data_complete_through,expected_lag_seconds}. Reuse handle to pin it. what: The headline numbers — sessions, conversion, revenue, LCP. curl -s "https://convertlyft.com/api/reports/kpi?from=2026-08-01T00:00:00.000Z&to=2026-08-08T00:00:00.000Z" -H "Authorization: Bearer $TOKEN" ## GET /api/reports/attribution scopes: sessions:read + reports:read (every one, not any one — holding some is a 403) range: sealed — Echoes window{from,to,handle,data_complete_through,expected_lag_seconds}. Reuse handle to pin it. what: Which sources and campaigns the conversions came from. curl -s "https://convertlyft.com/api/reports/attribution?from=2026-08-01T00:00:00.000Z&to=2026-08-08T00:00:00.000Z" -H "Authorization: Bearer $TOKEN" ## GET /api/reports/behavior scopes: sessions:read range: sealed — Echoes window{from,to,handle,data_complete_through,expected_lag_seconds}. Reuse handle to pin it. what: What people did on the page. curl -s "https://convertlyft.com/api/reports/behavior?from=2026-08-01T00:00:00.000Z&to=2026-08-08T00:00:00.000Z" -H "Authorization: Bearer $TOKEN" ## GET /api/reports/paths scopes: sessions:read range: sealed — Echoes window{from,to,handle,data_complete_through,expected_lag_seconds}. Reuse handle to pin it. what: The routes people take through the site. curl -s "https://convertlyft.com/api/reports/paths?from=2026-08-01T00:00:00.000Z&to=2026-08-08T00:00:00.000Z" -H "Authorization: Bearer $TOKEN" ## GET /api/reports/vitals scopes: reports:read range: sealed — Echoes window{from,to,handle,data_complete_through,expected_lag_seconds}. Reuse handle to pin it. what: Core Web Vitals as your visitors measured them. curl -s "https://convertlyft.com/api/reports/vitals?from=2026-08-01T00:00:00.000Z&to=2026-08-08T00:00:00.000Z" -H "Authorization: Bearer $TOKEN" ## GET /api/reports/errors scopes: issues:read + sessions:read (every one, not any one — holding some is a 403) range: sealed — Echoes window{from,to,handle,data_complete_through,expected_lag_seconds}. Reuse handle to pin it. what: Grouped JavaScript errors and the crash-free rate. curl -s "https://convertlyft.com/api/reports/errors?from=2026-08-01T00:00:00.000Z&to=2026-08-08T00:00:00.000Z" -H "Authorization: Bearer $TOKEN" ## GET /api/reports/heatmap scopes: replays:read + sessions:read (every one, not any one — holding some is a 403) range: sealed — Echoes window{from,to,handle,data_complete_through,expected_lag_seconds}. Reuse handle to pin it. what: Click and pointer density for one page. curl -s "https://convertlyft.com/api/reports/heatmap?from=2026-08-01T00:00:00.000Z&to=2026-08-08T00:00:00.000Z" -H "Authorization: Bearer $TOKEN" ## GET /api/funnels scopes: reports:read range: none — Takes no time range. Its answer is all time, always; from/to change nothing. what: Which funnels this site has — the ids the three funnel reports below require. curl -s "https://convertlyft.com/api/funnels" -H "Authorization: Bearer $TOKEN" ## GET /api/funnel-report scopes: reports:read range: from-to — Honours from/to. NO handle and NO data_complete_through; a window_handle here is ignored, not refused. what: Stage-by-stage funnel counts. curl -s "https://convertlyft.com/api/funnel-report?from=2026-08-01T00:00:00.000Z&to=2026-08-08T00:00:00.000Z" -H "Authorization: Bearer $TOKEN" ## GET /api/funnel-report/drilldown scopes: sessions:read range: from-to — Honours from/to. NO handle and NO data_complete_through; a window_handle here is ignored, not refused. what: The sessions behind one funnel stage. curl -s "https://convertlyft.com/api/funnel-report/drilldown?from=2026-08-01T00:00:00.000Z&to=2026-08-08T00:00:00.000Z" -H "Authorization: Bearer $TOKEN" ## GET /api/funnel-report/filter-values scopes: reports:read range: none — Takes no time range. Its answer is all time, always; from/to change nothing. what: The source and campaign values available to filter on. curl -s "https://convertlyft.com/api/funnel-report/filter-values" -H "Authorization: Bearer $TOKEN" ## GET /api/sessions-summary scopes: sessions:read range: none — Takes no time range. Its answer is all time, always; from/to change nothing. what: Session counts. All time — this one takes no range at all. curl -s "https://convertlyft.com/api/sessions-summary" -H "Authorization: Bearer $TOKEN" ## GET /api/sessions/search scopes: sessions:read range: from-to — Honours from/to. NO handle and NO data_complete_through; a window_handle here is ignored, not refused. what: The sessions that match a question, with the id each one is read by. An argument outside the documented filter set is a 400 that names it. curl -s "https://convertlyft.com/api/sessions/search?from=2026-08-01T00:00:00.000Z&to=2026-08-08T00:00:00.000Z" -H "Authorization: Bearer $TOKEN" ## GET /api/issues scopes: issues:read range: none — Takes no time range. Its answer is all time, always; from/to change nothing. what: The issue queue. curl -s "https://convertlyft.com/api/issues" -H "Authorization: Bearer $TOKEN" ## GET /api/board scopes: board:read range: none — Takes no time range. Its answer is all time, always; from/to change nothing. what: The board — what we found and what it is worth. curl -s "https://convertlyft.com/api/board" -H "Authorization: Bearer $TOKEN" ## GET /api/changes scopes: changes:read range: none — Takes no time range. Its answer is all time, always; from/to change nothing. what: Changes shipped on the site, with their markers. curl -s "https://convertlyft.com/api/changes" -H "Authorization: Bearer $TOKEN" ## GET /api/digest scopes: reports:read range: none — Takes no time range. Its answer is all time, always; from/to change nothing. what: The digest a person would have been emailed. curl -s "https://convertlyft.com/api/digest" -H "Authorization: Bearer $TOKEN" ## GET /api/events/stream scopes: sessions:read range: none — Takes no time range. Its answer is all time, always; from/to change nothing. what: Live events, server-sent. It does not end — read it as a stream, not with curl. curl -s "https://convertlyft.com/api/events/stream" -H "Authorization: Bearer $TOKEN" ## GET /api/memory scopes: memory:read range: none — Takes no time range. Its answer is all time, always; from/to change nothing. what: What this workspace already concluded — prior findings with the receipt behind each, decisions, preferences, and questions nobody answered. Read it before deriving any of it again. curl -s "https://convertlyft.com/api/memory" -H "Authorization: Bearer $TOKEN" ## GET /api/r/[receipt_id] scopes: the original call's scopes range: none — Takes no time range. Its answer is all time, always; from/to change nothing. what: Re-read a past answer by its receipt id. Needs whatever scopes the original call needed. curl -s "https://convertlyft.com/api/r/" -H "Authorization: Bearer $TOKEN" ## GET /api/crawl/[id] scopes: reports:read range: none — Takes no time range. Its answer is all time, always; from/to change nothing. what: Where a crawl this workspace started stands, and a page of its rows: url, status, title, words, findings. ?limit= ?offset= page them; ?format=jsonl answers one JSON object per line. curl -s "https://convertlyft.com/api/crawl/[id]" -H "Authorization: Bearer $TOKEN" ## GET /api/crawl/[id]/page scopes: reports:read range: none — Takes no time range. Its answer is all time, always; from/to change nothing. what: One crawled page (?url=): its main content as Markdown, headings and links. ?screenshot=1 answers the page as a browser draws it, image/jpeg. curl -s "https://convertlyft.com/api/crawl/[id]/page" -H "Authorization: Bearer $TOKEN" ## Also answered for a token: a 307 to GET /api/tools/, query intact # Follow the redirect (curl -L); it stays on this origin, so the Authorization header # goes with it. Every read tool on the MCP door is reachable by its own name the same # way; the list, with each tool's scopes, is https://convertlyft.com/.well-known/convertlyft-capabilities.json # GET /api/connections -> /api/tools/cvl_connections_list # GET /api/install-verify -> /api/tools/cvl_install_verify # GET /api/proposals -> /api/tools/cvl_proposals_list # GET /api/replays -> /api/tools/cvl_replays_list # GET /api/replays/moment -> /api/tools/cvl_replay_moment # GET /api/reports/funnel -> /api/tools/cvl_funnel_report # GET /api/reports/page-flow -> /api/tools/cvl_paths # GET /api/reports/page-heatmap -> /api/tools/cvl_heatmap # GET /api/seo/agency-matrix -> /api/tools/cvl_seo_agency_matrix # GET /api/seo/ai -> /api/tools/cvl_seo_ai # GET /api/seo/audit -> /api/tools/cvl_seo_audit # GET /api/seo/backlinks -> /api/tools/cvl_seo_backlinks # GET /api/seo/brief -> /api/tools/cvl_seo_brief # GET /api/seo/competitors -> /api/tools/cvl_seo_competitors # GET /api/seo/flow -> /api/tools/cvl_seo_flow # GET /api/seo/keyword -> /api/tools/cvl_seo_keyword # GET /api/seo/keywords -> /api/tools/cvl_seo_keywords # GET /api/seo/opportunities -> /api/tools/cvl_seo_opportunities # GET /api/seo/rank -> /api/tools/cvl_seo_rank # GET /api/whoami -> /api/tools/cvl_whoami # GET /api/workspaces -> /api/tools/cvl_workspaces_list curl -sL "https://convertlyft.com/api/seo/rank" -H "Authorization: Bearer $TOKEN" ## The SEO room's writes that buy nothing — opt-in scope seo:write # POST with a JSON body; a 307 to POST /api/tools/ keeps the method and body (curl -L). # Idempotent; nothing on the site changes and nothing is bought. # POST /api/seo/track-keywords {"keywords": ["..."], "market": 2784} # POST /api/seo/untrack-keywords {"keywords": ["..."]} # POST /api/seo/set-competitor-state {"domain": "rival.com", "state": "pinned" | "dismissed" | "none"} # POST /api/seo/opportunity-track {"keyword": "..."} # POST /api/seo/set-keyword-money {"keyword": "...", "money": false} # POST /api/seo/map-keyword-page {"keyword": "...", "pageUrl": "https://..."} # POST /api/seo/add-keywords {"keywords": ["..."], "pageUrl": "https://..."} curl -sL -X POST "https://convertlyft.com/api/seo/track-keywords" -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" -d '{"keywords":["power of attorney dubai"]}' ## Paid SEO steps and research on any domain — same opt-in, seo:write # They buy search data with the account's credits, inside the site's daily limit, with no # per-call confirmation. Research answers are stored: the same question again uses no # credits; {"refresh": true} buys again. POST /api/tools/: # cvl_seo_run_flow cvl_seo_make_brief cvl_seo_serp cvl_seo_keyword_ideas cvl_seo_keyword_metrics # cvl_seo_domain_overview cvl_seo_competitor_gap cvl_seo_backlink_profile cvl_seo_link_gap cvl_seo_ai_mentions # Credits used and today's limit: GET /api/tools/cvl_usage (reports:read). curl -s -X POST "https://convertlyft.com/api/tools/cvl_seo_backlink_profile" -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" -d '{"domain":"example.com"}' ## Errors # Branch on .code, never on .error prose: # token_in_query 400 · token_malformed / token_unknown / token_revoked / token_expired 401 # session_required 401 = no credential sent at all · key_invalid 401 = a bearer that is neither a cvl_pat_ token nor a site's secret key # missing_scope / creator_not_member / workspace_mismatch / owner_role_required 403 # 401s carry WWW-Authenticate: Bearer scope="..." naming the scopes the endpoint wanted. # A bad window_handle is 400 with window_handle_malformed | _expired | _workspace_mismatch and a # working example_request you can paste. # On the MCP door the same codes arrive in structuredContent on an isError result, with # required_scope, granted_scopes and a remedy on a scope refusal. Successful results carry # next: [{tool, args_prefilled, why}] — the next call, already filled in. Send it as-is. ## What is not here # No write endpoint among the calls above — every one is GET. The writes are named in the # capabilities manifest with the scope each needs; a token reaches only those its scopes cover # (a connected app: crawl:run and seo:write at most). None changes a measurement. A crawl reads # someone's public site; it changes nothing on yours. # no demo credential is published in this document; there is no anonymous read, and you need a human once, to mint one