FullVision
Reports

Form Report

gethttps://data.fullvision.io/form-report

Shows you how your forms convert, and the submissions and revenue they capture.

Use it when a signup or demo form is losing people and you need to know where — or when you want to read back what people actually typed into it.

The question it answers: which forms turn visitors into leads, and what did those leads go on to be worth?

Two ways to call it

CallWhat you get
Omit form_idOne row per form, ranked by revenue, with starts, submits and the completion rate. This is the "which form is leaking" view.
Pass form_idThat one form: its funnel, the pages it appears on, its revenue, and the captured submissions themselves. This is the "why is it leaking, and who filled it in" view.

What you get

DataWhat it tells you
Funnelstarts → submits and the completion rate
Reach + causal revenuerevenue from people who touched the form vs. plausibly caused by it
Per-page breakdown (single)where the form is used and how it performs on each page
Captured submissions (single)the cursor-paginated submissions list (email / phone / person)
Product eventsexact unique people per product event, over the form's reach set

Where the numbers come from. Product events count exact unique people over the report's range, not estimates or samples, and they are aggregate. The submissions list is the one exception: it is per-person, not aggregate. The Precision notes at the end of this page give the exact warehouse wording.

submissions is personal data. It contains what people typed into your forms — including their names, email addresses and phone numbers. It is the only non-aggregated section of this endpoint. Treat it like customer data: store it accordingly, and do not put it in a shared dashboard by accident.

Form submissions vs. people. submissions answers what did people submit — form_email, form_phone and form_name are the captured payload and exist only here. People answers who did this form acquire: pass ?created_via=form&origin_detail=<form_id> for the person rows.

Reach revenue vs causal revenue

Both appear on every row, and reach is always the larger number:

  • Reach revenue — money from everyone who touched the form, whenever they paid. It sizes the audience; it is not a claim that the form caused the sale.
  • Causal revenue — money the form can plausibly claim: people who bought in the same session, or who were demonstrably influenced by it.

Reach overlaps across forms. FullVision counts a person who submitted three forms under all three, so adding reach numbers across forms invents revenue that does not exist. Compare forms to each other; never total them.

Group form — omit form_id

One row per form — starts, submits and the completion rate, joined to reach and causal revenue — ranked by revenue. A form that has revenue but no traffic in range still appears, so the list is complete.

Single form — supply form_id

SectionWhat it tells you
funnelstarts → submits, with drop-off
by_pagewhere the form is used and how it performs on each page
revenuerevenue from people who touched the form vs. revenue plausibly caused by it
submissionsthe cursor-paginated captured-submissions list (email / phone / person)

Parameters

ParameterWhat it doesWhen you'd use it
form_idAddresses one form and switches the report into the single form. It is the <form> id attribute, falling back to the form's normalized action URL.You picked a form off the ranked list and want its funnel, its pages, or its submissions.
submissions_limitSingle form only: sets the captured-submissions page size (default 50, max 200).You are rendering a submissions table and want a full page of rows rather than the default 50.
submissions_starting_afterSingle form only: returns submissions after this id, taken from submissions.next_cursor.You are paging through a form with hundreds of submissions — ask for the next page instead of raising the limit.
page_url_scopePins the report to one host: marketing or product. all removes the pin and reports the form on every host it appears on.The same form id exists on your marketing site and in your app, and you want one of them — or all to see the id's combined funnel across both.
range, or from + toSets the date window.Always worth setting explicitly — a default window makes two reports look like they disagree.
granularityAdds a series array with one point per daily, weekly or monthly bucket, for charting. Group form only, funnel metrics only.You want to see whether completion rate is drifting rather than one average.
series_bySplits that series by form_id instead of returning one total per bucket. Group form only.You want one line per form rather than a single site-wide line.
fieldsNarrows the response to the blocks you name. It only ever removes blocks, never adds one.Your client only renders the funnel and you don't want to ship personal data you won't display.
order_by, order_dirRe-ranks the complete form list by one column before limit. Group form only. Omit for the revenue ranking.You want the worst completion rates, not the biggest revenue.
totalsAdds a totals object over the complete form list, before limit, plus a totals_meta sidecar. Group form only.You want the site-wide funnel under the per-form table.
compare_to, compare_from, compare_untilAttaches a prior object to each row, measured over a prior window. Group form only.You want "up or down since last month" per form, not a second call.

The series array — granularity

granularity is purely additive: rows is unchanged, and omitting it returns exactly the previous response. Each point carries form_starts, form_submits and submit_rate_pct.

The series is funnel metrics only. Reach revenue overlaps across forms, so bucketing it over time would read as additive when it is not.

FullVision labels each bucket with its start date (Monday for weekly) and returns them oldest first. The trailing bucket may be partial — compare it against range.to. Do not add the buckets up to get the range totals — rate metrics are recomputed per bucket. Use rows for totals.

FullVision analyses popup/modal capture separately and does not expose it in v1.

Ranking, totals and comparison

Three group-form parameters, each independently opt-in. Omit all three and the response is exactly the one you got before they existed — no totals, no totals_meta, no prior.

Re-ranking the list — order_by

order_by is a closed enum per report — the legal columns are listed in the order_by dropdown of the API panel above. Omit it and you keep the default revenue ranking, unchanged. order_dir is asc or desc, defaults to desc, and does nothing on its own.

  • The re-rank applies to the complete form list, before limit — a form the revenue ranking buried below the limit surfaces, rather than the same top-20 reshuffled.
  • Nulls sort last in both directions. A form whose page never appears in the traffic view has no page_views; that is unknown, not zero, and floating it to the top of an ascending sort would bury the forms you asked to see.
  • Ties always break on form_id ascending, so the order is total and repeated calls return the forms in the same order.

Whole-set totals — totals

totals=true adds a totals object covering the complete filtered set before limit — every form, not just the returned ones — plus a totals_meta sidecar. Anything other than true/false is a 400; nothing is coerced.

Rates are recomputed from their bases over the whole set, never summed or averaged across rows: submit_rate_pct is whole-set submits over whole-set starts, not the average of the per-form rates.

totals_meta.upper_bound names the columns whose total overcounts, because per-row values overlap between rows. Render these as approximations, not counts — this is the same overlap the reach warning above describes:

ColumnWhy it overcounts
new_customersreach cohorts overlap across forms
revenue_centsreach revenue overlaps across forms
bought_in_session_customers, influenced_customersa person can be counted under several forms
page_viewstwo forms on one page each report that page's views

The causal revenue columns are not marked: they are the MECE per-charge complement, so they sum exactly.

totals_meta.omitted names the columns deliberately left out, so their absence reads as a decision rather than a bug:

ColumnReason
ltv_centsa ratio of two overlap-inflated reach cohorts; the whole-set value would not be a lifetime value of anything
page_pathsa label array, not a metric
page_views_by_patha nested per-row breakdown, not a scalar
eventsa per-row array of named counts; there is no whole-set scalar

A fields trim trims totals with it: neither totals nor prior ever describes a column the response itself does not carry.

Comparing against a prior window — compare_to

compare_to attaches a prior object to each row, measured over a second window:

ValueWindow
previous_periodthe equal-length window immediately before range
previous_yearthe same calendar window one year back
customcompare_from + compare_until — both required here, both rejected otherwise
  • prior: null means the form did not exist in that window. That is a different claim from a prior object of zeros, which means present and measured zero; conflating them reports a brand-new form as flat.
  • Prior-only rows are not introduced. A form that collected then and is retired now does not appear — the list is still the current window's.
  • The prior window differs only in its dates: same workspace, same page_url_scope, same filters, same fields.
  • Independent of totals. A nested totals.prior appears only when both are set.
  • compare_to roughly doubles the cold-path work — every query this report issues runs a second time over the prior window. Only Page Report memoizes its ranked set; this one does not.

Trimming the response — fields

fields is a comma-separated list of response blocks. Omit it and you get every block — it only ever narrows a response, never widens one, so every existing call keeps its exact shape.

The two forms do not accept the same names — submissions exists on the single form only:

BlockWhat it's forGroup columnsSingle sections
funnelwhere people drop outform_starts, form_submits, submit_rate_pctfunnel
revenuewhat the people who filled it in were worthnew_customers, revenue_cents, bought_in_session_*, influenced_*, ltv_cents (lifetime value)revenue
pageswhich pages the form is on, and their trafficpage_paths, page_viewsby_page
submissionswhat people actually typed (personal data)— (400 here)submissions
eventswhat those people did in your producteventsevents
?fields=funnel                       → group rows keep only the funnel columns
?form_id=newsletter&fields=submissions → the single form keeps only the captured list
?fields=submissions                  → 400 — `submissions` is single-form only

Rules that bite:

  • FullVision resolves the legal set AFTER form selection, not per endpoint. Supplying form_id picks the single form and its block list; omitting it picks the group form and its own. submissions therefore 400s without form_id, naming the blocks legal on the group form.
  • Empty, duplicated, unknown, and * are all 400s — fields=, fields=funnel,funnel, fields=nope, fields=*. A stray comma is a typo, not a request for everything. The error message lists the legal blocks inline.
  • Trimming does not guarantee a query saved per block. Sources are shared: on the single form funnel and pages are two folds of the same form-performance read. The contract is correct trimming — you get exactly the blocks you asked for and nothing else — never one fewer round trip per block dropped.
  • fields selects columns, never which rows exist. ?fields=events returns the same forms the untrimmed call does, with only the events column on each. The Precision notes say which queries decide the list.
  • Row identity is never droppable. form_id, host_scope and range survive every trim.
  • granularity is independent. The series array is not a block: ?fields=revenue&granularity=weekly still returns one.
  • fields never lowers the scopes the endpoint requires. ?fields=funnel still needs customer:read alongside attribution:read and web:read, and submissions is still personal data.

Landing-page traffic — page_paths and page_views

Each group row carries the pages the form appeared on in range, and their summed traffic. Use them to tell "nobody sees this form" apart from "everybody abandons it".

  • page_paths — the distinct pages where visitors saw the form (raw, host-qualified).
  • page_views — summed page_views of those pages, or null when none of them appear in web-pages for the range.

null is not 0. null means no traffic data for those pages (render "—"); 0 means the pages exist and genuinely had no views. Forms that only ever appear via form-revenue — with no form-performance row in range — have an empty page_paths and therefore null views.

The endpoint returns paths raw. Dashboards that collapse per-user id segments (/dashboard/60843/home → /dashboard/:id/home) should keep doing that client-side; the report deliberately does not, because a workspace can legitimately want the real paths.

Segmenting the series — series_by

Supply series_by alongside granularity (group form only) to split the series by one dimension instead of returning scope-level totals. series then holds one point per (bucket × segment), each carrying key — the segment value — so the response can feed a stacked or multi-line chart directly.

Accepted here: form_id.

granularity=weekly                 → [{ date, <metrics> }, …]
granularity=weekly&series_by=form_id  → [{ date, key, <metrics> }, …]

Notes that bite:

  • A source view that has no breakdown for the requested dimension is dropped from the segmented series, not smeared across every segment — copying a scope-level total onto each segment would read as per-segment data and be wrong. So a segmented series can carry fewer metric keys than the flat one.
  • FullVision skips rows whose dimension value is blank: an unnamed series cannot be labelled or filtered by the caller.
  • An unrecognised series_by returns an empty series (not an error, and not a silent fallback to the flat series).
  • Segment values may contain spaces (search queries, campaign names) — key is the full value.
  • rows is unaffected. Do not sum segmented buckets to get range totals; use rows.

Authorization

AuthorizationBearer <token>

API key as Bearer token

In: header

Query Parameters

form_id?string

The id attribute (falling back to its normalized action URL). Omit for the ranked group form.

limit?integer

Group form only: number of ranked rows to return (default 20, max 1000). Ignored when the addressing key is supplied.

Range1 <= value <= 1000
submissions_limit?integer

Single form only: captured-submissions page size (default 50, max 200).

Range1 <= value <= 200
submissions_starting_after?string

Single form only: return submissions after this id (from submissions.next_cursor).

page_url_scope?string

Which host to scope to. Defaults to marketing. all removes the host predicate entirely (every host in the workspace).

Value in

  • "marketing"
  • "product"
  • "all"
range?string

Preset date range. Canonical names (last_30_days, last_7_days, last_3_months, last_6_months, last_year, all_time, …) and dashboard short keys (30d, 7d, 3m, 6m, 1y, all) are both accepted. Mutually exclusive with from/to.

Value in

  • "last_7_days"
  • "last_14_days"
  • "last_28_days"
  • "last_30_days"
  • "last_90_days"
  • "current_month"
  • "this_month"
  • "last_3_months"
  • "last_6_months"
  • "last_year"
  • "all_time"
  • "7d"
  • "14d"
  • "28d"
  • "30d"
  • "90d"
  • "3m"
  • "6m"
  • "1y"
  • "all"
from?string

Custom start date (YYYY-MM-DD). Ignored if range is set.

Match^\d{4}-\d{2}-\d{2}$
to?string

Custom end date (YYYY-MM-DD). Ignored if range is set.

Match^\d{4}-\d{2}-\d{2}$
granularity?string

Group form only. When set, the response gains a series array: scope-level totals per date bucket at this grain, for charting. Purely additive — rows is unchanged, and omitting this returns exactly the previous response. Ignored when an addressing key selects the single form.

Value in

  • "daily"
  • "weekly"
  • "monthly"
series_by?string

Group form only, requires granularity: split the series by one dimension. The response's series then holds one point per (bucket × segment), each carrying key. Accepted values: page-report → page; channel-report → channel (concrete source, the grain of sources[]), channel_category (the 9 buckets, the grain of rows[]); keyword-report → query; form-report → form_id; ad-report → campaign; email-report → campaign. An unrecognised value returns an empty series. Under a channel-report split, visitor counts are emitted as attributed_visitors (attribution cohort); unique_visitors on split points is a deprecated alias of that same value until 2026-11-26, not the host-scoped traffic count rows carry.

fields?string

Comma-separated response blocks to return. OMIT for every block of the resolved form (the default — this param only ever narrows a response, never widens one). The legal set is resolved AFTER form selection, so it depends on whether an addressing key was supplied: some blocks exist only on the single form, and a report whose resolved form declares none rejects fields with 400. An unknown or duplicated name is a 400 listing the legal blocks for that form.

order_by?string

Group form only: rank the COMPLETE row set by this column before limit and the cursor slice. Omit for this report's default ranking, which is unchanged. Ties always break on the row identity, so the order is total and a cursor walk visits every row exactly once. Legal values: form_id, form_starts, form_submits, form_successes, submit_rate_pct, new_customers, revenue_cents, bought_in_session_revenue_cents, bought_in_session_customers, influenced_revenue_cents, influenced_customers, ltv_cents, page_views, submissions_count.

Value in

  • "form_id"
  • "form_starts"
  • "form_submits"
  • "form_successes"
  • "submit_rate_pct"
  • "new_customers"
  • "revenue_cents"
  • "bought_in_session_revenue_cents"
  • "bought_in_session_customers"
  • "influenced_revenue_cents"
  • "influenced_customers"
  • "ltv_cents"
  • "page_views"
  • "submissions_count"
order_dir?string

Direction for order_by, default desc. Ignored when order_by is omitted. Rows whose sort column is null sort LAST in both directions.

Value in

  • "asc"
  • "desc"
totals?string

Group form only: also return a totals object over the COMPLETE filtered set (before limit), plus a totals_meta sidecar naming the columns that are upper bounds rather than exact counts and the columns deliberately omitted. Anything other than true/false is a 400 — never coerced.

Value in

  • "true"
  • "false"
compare_to?string

Group form only: also measure each row over a prior window and attach it as prior. previous_period is the equal-length window immediately before; previous_year is the same calendar window one year back; custom requires compare_from + compare_until. A row present now but absent then carries prior: null — distinct from a prior row of zeros, which means present and measured zero. Rows present ONLY in the prior window are not introduced. Independent of totals: a totals.prior sub-object appears only when both are set. Note this genuinely doubles the cold-path work on every report except page-report.

Value in

  • "previous_period"
  • "previous_year"
  • "custom"
compare_from?string

Prior-window start (YYYY-MM-DD). Required with compare_to=custom, rejected otherwise.

Match^\d{4}-\d{2}-\d{2}$
compare_until?string

Prior-window end (YYYY-MM-DD). Required with compare_to=custom, rejected otherwise.

Match^\d{4}-\d{2}-\d{2}$

Responses

Single-form report (form_id supplied) or the ranked form group

get/form-report
curl -X GET "https://data.fullvision.io/form-report?form_id=newsletter&range=last_30_days"
Example Responses
{  "form_id": "string",  "host_scope": "string",  "range": {    "from": "string",    "to": "string"  },  "funnel": {},  "by_page": [    {}  ],  "revenue": {},  "submissions": {    "rows": [      {}    ],    "has_more": true,    "next_cursor": "string"  },  "events": [    {      "name": "string",      "people": 0    }  ],  "is_cached": true,  "age_s": 0,  "last_refresh": "string"}
application/json