Skip to content

API operations

All 21 operations on the Convultra API and MCP server, grouped by the question they answer, with every trap that would make a figure misleading.

article |14 min |Updated Sep 2026
On this page · 7 sections

The Convultra API and MCP server are in Beta. This is every operation, grouped the way a question arrives. Every path is relative to https://data.convultra.com/v1, and each operation has an MCP tool of the same name. {project} accepts a project UUID, a proj_... tracking key, or a domain such as acme.example, whichever you have.

You can usually omit {project}. If the credential can see exactly one project, the API resolves it. If it can see several, an omitted project is a 400 that names the candidates, so a script or an assistant can choose rather than guess.

What have I got, and is it working?

OperationPathScopeWhat it is for
get_meGET /menoneWhat this credential is: name, scopes, reachable projects, rate limit. Call it first when you do not know which projects are available, and to confirm a new key works.
get_metric_definitionsGET /meta/metricsnoneConvultra’s definitions of visitor, session, conversion, revenue, conversion rate, blended ROAS, CPA, AI referral and filtered-out traffic, plus the conversion event types, range presets and scope summaries. Read it before comparing any Convultra figure with another tool’s.
list_projectsGET /projectsprojects:readEvery project this credential can read, with tracking health, reporting currency and timezone. Pass include_archived=true for paused and archived ones. Use it to turn a domain into a project id.
get_projectGET /projects/{project}projects:readOne project in detail: attribution model and lookback, the four deduplication windows, currency, timezone, and which ad platforms are connected (status and timing only, never credentials). Reach for it to explain why figures are attributed as they are.
get_tracking_healthGET /projects/{project}/tracking-healthprojects:readWhether the script is installed and receiving events, when the last event arrived, which custom tracking domains are live, whether monitoring mode is on, and the deduplication windows in force.

Call get_tracking_health before concluding anything from a drop. A tracking outage and a bad week look identical in the numbers, and the outage is more common. monitoring_mode: true is its own trap: tracking is healthy and recording, but nothing is being sent to the ad platforms.

How did they do?

OperationPathScopeWhat it is for
get_overviewGET /projects/{project}/overviewanalytics:readThe metric strip as one call: revenue, conversions, conversion rate, visitors, sessions, pageviews, bounce rate, average order value, recovered conversions and revenue, AI-referral figures, filtered-out counts, and, with spend:read, spend, blended ROAS and CPA. compare=previous_period or compare=previous_year adds a previous block and a change block of percentage deltas.
get_timeseriesGET /projects/{project}/timeseriesanalytics:readThe same metrics as one row per hour, day, week or month. Every bucket in the range is present, zero-filled, so a gap in the array is a bug rather than a quiet day. Choose columns with metrics=revenue,conversions.

Reach for get_overview when the question is “how did we do”; get_timeseries when it is “what changed and when”, or when drawing a chart.

get_timeseries traps, all things a chart would hide:

  • visitors is unique within each bucket, so summing buckets double-counts people who came back. Ask for a wider granularity instead; week and month buckets re-count uniques properly rather than adding days up.
  • conv_rate is per bucket, conversions divided by visitors of that same bucket. Do not sum or average it across buckets.
  • spend is unavailable at hourly granularity, because ad platforms report cost per day and spreading a daily figure over 24 hours would invent detail the platform never gave.
  • granularity=hour requires a range of 7 days or fewer.
  • Week and month buckets that extend past the requested range carry partial: true. A half week that looks like a whole one is the commonest way a chart lies.

Where did the traffic come from?

OperationPathScopeWhat it is for
get_breakdownGET /projects/{project}/breakdownanalytics:readRank one dimension by traffic and revenue. One dimension= per call, 20 to choose from.
get_ai_referralsGET /projects/{project}/ai-referralsanalytics:readHumans who arrived by clicking a link inside ChatGPT, Claude, Perplexity, Gemini, Copilot and similar: sessions, conversions and revenue per assistant, a daily timeline, and the pages they land on.
get_filtered_outGET /projects/{project}/filtered-outanalytics:readThe bot and AI-crawler activity Convultra detected and excluded from every reported figure, named per bot. Use it to answer “is a crawler inflating our numbers”: it is not, and this is the evidence.

get_breakdown dimensions:

GroupDimensions
Acquisitionsource, channel, referrer, paid_platform, ai_platform
Campaignutm_source, utm_medium, utm_campaign, utm_term, utm_content
Contentpage, entry_page, exit_page
Geographycountry, region, city
Technologydevice, browser, os
Behaviorevent_type

Every dimension returns the same row shape, { key, label, visitors, sessions, pageviews, bounce_rate, conversions, revenue, conv_rate }, with rates as 0 to 1 ratios, so you can switch dimension without switching parser. Narrow with search= (a case-insensitive substring on the dimension value), trim columns with metrics=, and page with limit= (default 25, maximum 500) and cursor=.

Four things to know before quoting a breakdown:

  • visitors is always null here. The underlying queries count sessions, not unique people, so conv_rate on these rows is conversions divided by sessions, a different denominator from the overview’s. Do not compare the two rates directly. The response says so in meta.visitors_unavailable_reason.
  • Some dimensions cannot report conversions or revenue and return null for them. null means “this dimension cannot tell you”, not zero.
  • Some dimensions are capped at 10 or 20 rows with no way to page further. Then meta.truncated_by_pipe is true, has_more is false, and the tail of the distribution is genuinely unavailable. Present the rows as the top ten, not the whole picture. On those dimensions search filters the truncated top N, not the full set, and the response says so in meta.search_scope.
  • Geography rows below five sessions are suppressed and counted in meta.suppressed_rows. See Privacy and redaction.

source, channel and utm_campaign additionally carry spend, roas and cpa when the credential holds spend:read.

AI referrals and AI crawlers are different populations and must never be added. An AI referral is a human session: it counts in overview visitors, conversions and revenue already, so adding get_ai_referrals figures to a total double-counts. An AI crawler (GPTBot, ClaudeBot, PerplexityBot, CCBot) is a bot: it is filtered out at ingest and appears in no traffic, conversion or revenue figure anywhere in this API. Within get_filtered_out, do not add total_bot_events and total_ai_crawler_events either. Most AI crawler user agents contain “bot”, so they appear in both lists, and the AI figure is a subset view, not a separate population.

What converted?

OperationPathScopeWhat it is for
list_eventsGET /projects/{project}/eventsanalytics:readEvery event type this project recorded in the range, with how often it fired, how many sessions fired it, attached revenue, and is_conversion. Call this before guessing an event name. Names come from the customer’s own tracking code.
get_event_timeseriesGET /projects/{project}/events/{event_type}/timeseriesanalytics:readOne event type, day by day: count, unique_visitors, value. The follow-up to list_events when the question is “when did add_to_cart fall off”.
get_funnelGET /projects/{project}/funnelanalytics:readHow many sessions completed an ordered sequence of 2 to 6 event types, and where they dropped out.
list_conversionsGET /projects/{project}/conversionsconversions:readOne row per conversion: value, currency, event type, order or lead id, first-touch source, geography, device, the page it happened on, the referring domain, ad click ids, recovery status and delivery status.
get_conversionGET /projects/{project}/conversions/{event_id}conversions:readOne conversion in full, plus the visitor’s page-by-page journey through the session that converted, the time from first page to conversion, and every delivery attempt with its failure reason in plain English.
get_attributionGET /projects/{project}/attributionconversions:readHow conversions and revenue are credited across ad platforms under last_click (default), first_click or linear, plus touchpoint depth and assisted conversions.
get_recoveryGET /projects/{project}/recoveryconversions:readHow many conversions the ad platforms’ own pixels would have missed, and why: by reason, by platform and by day.

Macro versus micro. Seven event types are conversions: purchase, lead, signup, subscription, download, contact, schedule. They feed the overview totals and are forwarded to the ad platforms. Everything else, including add_to_cart, view_content, begin_checkout, pageview and click, is a micro event and is deliberately excluded from conversion counts. is_conversion on every event row carries this, so you never infer it from a name.

get_funnel. List steps in the order they happen, comma-separated or repeated (steps=pageview,add_to_cart,purchase). A session counts for a step only if it fired every earlier step before it, within window_seconds (default 2592000, which is 30 days; maximum 90 days) of the first step. Steps must be distinct. A surprisingly empty funnel almost always means the steps are in the wrong order or an event name is wrong, so call list_events first. drop_off_rate and conversion_from_previous are null on step 1: there is no earlier step to be lost from, and a zero there would read as “nobody was lost”, which is a different claim.

list_conversions is the incremental-pull endpoint. Filter with event_type, source, min_value, delivery_status (delivered, pending, failed, not_forwarded), recovered (true or false) and since= (an ISO timestamp; only conversions strictly after it). Page with limit (default 50, maximum 200) and cursor. Its traps:

  • Rows are newest first. Keep calling while page.has_more is true; do not stop on a short page. Filters are applied after the newest 2000 rows in the range have been fetched, so a filtered page can come back short while more matches still exist.
  • Paging beyond 2000 conversions in one range is refused with 400. Narrow the range, or pull incrementally with since=.
  • landing_page and time_to_convert_seconds are always null on this endpoint. get_conversion computes the latter from the session journey.
  • delivery.status: "not_forwarded" means no forwarding attempt exists at all. That covers organic conversions, which are never sent, and micro events. It is not a failure.
  • Conversions the customer has removed are excluded.

get_conversion scans the newest conversions within lookback_days (default 90, maximum 400) because the underlying query cannot be keyed by event id. Anything older, or deeper than that scan, returns 404; list_conversions with explicit start_date and end_date still returns the row itself, just without the journey. You never supply a session id: the session is resolved server-side from the conversion, and no other session’s journey is reachable through this API.

get_attribution traps:

  • platforms rows do not sum to totals.total_conversions. Only conversions carrying at least one captured ad click can be attributed to a platform; organic, direct and email conversions are in the total and in no platform row. totals.attributed_share tells you how much of the business the platform rows describe. When it is low, say so.
  • time_to_convert buckets by number of touchpoints, not elapsed time, despite the name. Its bucket labels read "1 touch", "4-5 touches".
  • linear conversions are fractional by construction, since credit is split across touchpoints.
  • assisted_conversions is what a last-click report hides: a platform whose first-click figure far exceeds its last-click figure is an introducer that last-click reporting undersells.

get_recovery answers “why does Google report fewer conversions than you do”. recovered_conversions is the gap; platform_would_report_conversions is what the pixel alone would have shown; by_reason splits it across ios_itp, cookie_expired, cookie_missing, ad_blocker and firefox_etp. Its denominators count only attributed conversions, those with a captured click id, so recovery_rate is the recovered share of attributable conversions, not of all conversions. Expect it to differ from anything you compute against the overview total.

Did it reach the ad platforms?

OperationPathScopeWhat it is for
list_deliveriesGET /projects/{project}/deliverydelivery:readEvery attempt to send a conversion to an ad platform, newest first, with the failure reason in plain English. Filter by platform, status, conversion_id or since.
get_delivery_summaryGET /projects/{project}/delivery/summarydelivery:readDelivered, pending and failed counts, the delivery rate, the revenue sitting behind failed deliveries (value_at_risk), a per-platform breakdown and a daily failure count. Limit to one platform with platform=.
retry_deliveryPOST /projects/{project}/delivery/{id}/retrydelivery:writeRe-queue one failed delivery. {id} is the delivery attempt id from list_deliveries, not a conversion id.

Google Ads and OpenAI Ads forwarding are live today. Meta, Microsoft Ads and TikTok forwarding are coming soon, so until they launch you will only see delivery rows for Google Ads and OpenAI Ads.

Use get_delivery_summary for “is our server-side tracking healthy” and to find the day a connection broke; list_deliveries to see which conversions failed and why.

  • Rows are attempts, not conversions. One conversion sent to two platforms is two rows; a retried conversion is one row per attempt. Never count rows as conversions; that is what the summary is for. In the summary, value_at_risk is deduplicated by conversion (one $200 order failing on two platforms is $200 at risk, not $400) while the counts deliberately are not.
  • Statuses are pending, retrying, success and failed. success means the platform accepted the upload, not that it attributed it to an ad.
  • Rows are dated by when the attempt was logged, which is shortly after the conversion itself, so a range edge can put a conversion and its delivery on different days.
  • A conversion never sent to any platform produces no row at all and so appears nowhere in these figures. For those, use list_conversions with delivery_status=not_forwarded.
  • reason is a plain-English sentence aimed at a marketer, never the platform’s raw error text. Only failures have one; a pending row has not been answered yet.
  • get_delivery_summary marks itself partial: true with a partial_reason when the range holds more rows than one scan covers. Every figure in a partial response under-reports. Narrow the range.

retry_delivery is the only write in this version, and it is not idempotent: each call queues one more attempt, so calling it twice sends twice. It is refused when the attempt is not failed, when no automatic attempts remain, and when the conversion has aged out of the platform’s own window: Google Ads and Microsoft Ads accept conversions up to 90 days after the click, Meta only 7. Past that you get 409 outside_retry_window, and no amount of retrying will change the platform’s answer.

Retry when the cause was a broken connection, a throttle or an unreachable platform and the underlying cause is now fixed. Do not retry when the platform said the click was not recognized, the conversion was outside its window, or it already has the conversion. Queueing is not delivery: check back with list_deliveries a few minutes later.

What did it cost?

OperationPathScopeWhat it is for
get_spendGET /projects/{project}/spendspend:readAd cost from the connected platforms (Google Ads, Microsoft Ads and OpenAI Ads today), split by platform and campaign, with the revenue and conversions Convultra attributes to each.

Check available first. When it is false, spend is unknown, and reason names the missing step: no ad platform connected, no ad account selected on a connected platform, or the nightly cost sync has not succeeded. Do not report that as zero spend; do not quote a ROAS at all.

When mixed_currency is true the platforms report in different currencies, total_spend is null, and you must quote the per-platform figures with their own currency. Convultra does not convert between currencies here. Inventing an exchange rate would silently corrupt every downstream total.

Campaign revenue and conversions are Convultra’s own attributed figures, joined on the click ids and campaign signals the click carried. They are not the platform’s reported conversions and will not match the ad platform UI exactly. get_recovery explains most of that difference.

Range and timezone

Fourteen of the 21 operations take a date range. Four of them bucket on UTC days regardless of the timezone you pass. See Date ranges and timezones.

Next in Developers Date ranges and timezones →