People
This endpoint returns the individual people behind your numbers — one person you name by identifier, or a filtered list of them you can page through.
Use it when a report gave you a count and you now need the actual humans in it: to open one customer's record, or to pull the list you are going to email, sync or hand to sales.
The question it answers: who are these people, how did they arrive, and what have they paid me?
Two forms: a list, or one person
q is the switch, and it is the only one.
- Omit
qand you get a list — person rows, newest first, paged with a cursor. This is the list-building primitive: it is not a ranked top-N, so you page through all of it rather than reading the first page as "the top". - Pass
qand you get one person — their row, an aggregated lifetime-revenue summary, and (for paid-acquired customers only) the campaign that acquired them. FullVision rejects an identifier matching several people rather than guessing; re-address with the exactperson_id.
What comes back
| Block | What it is | When you'd read it |
|---|---|---|
| Person row (both forms) | Who this person is and how they arrived: email, your own user id for them (external_id), Stripe customer id, visitor id, first-touch channel and source, created_via and origin_detail, paid, current_plan, plus a journey_id pointer. | You are building a list, or you want the one-line answer to "where did this customer come from?" |
revenue (single form) | The person's lifetime revenue rolled up from their payments: lifetime_revenue_cents, charge_count, first_charge_at, last_charge_at. Payments only — subscription starts, cancellations and refunds are excluded, so the count is charges, not events. | You want to know what one customer is actually worth before you decide how to treat them. |
acquisition (single form) | The campaign that acquired them — present only for customers acquired through paid advertising, null otherwise. | You are checking whether a specific customer came from an ad you paid for. |
Rows carry personal data: the person's email address, the id your own
system uses for them (external_id), and their Stripe customer id. /people,
Journeys and
Form Report's submissions block are the
only surfaces that return personal data — everything else returns aggregates.
/people requires the customer:read scope, and fields never lowers it.
People never returns the event timeline. A person row tells you who someone
is, never what they did. For that, follow the row's journey_id to
Journeys — the only surface that returns
timelines.
People vs. form submissions. /people answers who did this form acquire —
pass ?created_via=form&origin_detail=<form_id> for the person rows.
Form Report's submissions block answers
what did people submit: form_email, form_phone and form_name are the
captured payload and exist only there, never on a person row.
Addressing
| Parameter | What it does | When you'd use it |
|---|---|---|
q | Looks up exactly one person by an identifier: email address, cus_… Stripe customer id, person or visitor UUID, or the user id from your own system. Omit it for the list form. | You have an email from a support ticket, or a Stripe customer id from a webhook, and you want that person's record and lifetime revenue. |
Filtering the list
Every filter below matches on the person's first touch or current state, not on a date range of activity.
| Parameter | What it does | When you'd use it |
|---|---|---|
created_via | stripe, form or events — which kind of record first identified this person. | You want only the people a form captured, and not the ones who appeared straight in Stripe. |
origin_detail | The secondary value of that first record, read according to created_via: a form id, an event name, or a plan name. | You want the people acquired by one specific form, so you pass created_via=form and the form's id here. |
first_touch_channel | One of the nine channel buckets (Organic Search, AI, Organic Social, Paid Search, Paid Social, Email, Direct, Referral, Non-attributed) — the bucket the person's first visit landed in. | The channel report says AI sent you customers and you want to see who they are. |
first_touch_source | The concrete source behind that channel — Google, LinkedIn, ChatGPT, Direct, and so on. | AI traffic converts and you want to know whether it was ChatGPT or Claude that sent the customers. |
paid | true returns only people with a payment-bearing Stripe event; false returns only those without one. | You want to email everyone who signed up but never paid. |
has_email | true returns only rows with a non-empty email address. | You are exporting a list for an email tool, and a row with no address is useless to you. |
current_plan | Plan name from the person's most recent payment-bearing Stripe record. | You are contacting everyone currently on the Starter plan about an upgrade. |
range / from + to | Date window on created_at — when the person first appeared. | You want the people who arrived in July, not everyone you have ever seen. |
limit | Rows per page (default 20, max 1000). | You are walking the whole list and want fewer round trips. |
starting_after | Returns the people after this cursor — pass back the next_cursor from the previous response. | You are paging through the complete list: keep calling until has_more is false. |
Cohort drilldowns — first_touch_page, scope, form_id
Two selectors answer "who is behind that number" for a page's or a form's customer count. They change the underlying source; they are not extra filters layered on the usual people list.
| Parameter | What it does | When you'd use it |
|---|---|---|
first_touch_page | Switches the list to the payers whose first-touch landing page was this page. Reconciles with page-report's customer counts for the same window. Pass the page exactly as page-report returns it. | A blog post shows 12 customers in the page report and you want to see those 12 people. |
scope | Applies to first_touch_page only: new (default) counts people whose first-ever payment falls inside the range; renew counts existing customers who charged again inside it. | The page report's number looks too good, and you want to know how much of it is genuinely new business. |
form_id | Switches the list to the payers who submitted this form. Reconciles with form-report's revenue for the same window. Pass the id exactly as form-report returns it. | A demo form is credited with revenue and you want the names of the people who filled it in. |
Things to know:
- The two are mutually exclusive. They select different cohorts, so passing
both is a
400, never a silent precedence rule. - Rows change shape. A drilldown returns that source's columns (e.g.
revenue_in_range_centsandfirst_sub_datefor a page cohort;submit_countandlifetime_revenue_centsfor a form cohort), not the standard person row. - A drilldown is a single page of results. Neither source supports cursor
pagination, so
starting_afterwith a drilldown is a400rather than a silently ignored parameter, and the response always carrieshas_more: false/next_cursor: null. Raiselimit(max 1000) instead. form_idis a reach cohort. A payer who submitted N forms appears under all N — never sum across forms.- Both sources need
customer:read, the same scope/peoplealready requires, so the rows stay behind the same gate.
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.
Blocks exist on the single form only, because the list form is one cursor-paginated person list with no separable sections:
| Block | Single-form key | What it costs |
|---|---|---|
revenue | revenue | the lifetime-revenue summary (one filtered timeline read) |
acquisition | acquisition | the acquiring campaign — an unfiltered scan of paid-acquired customers, the single most expensive read on this endpoint |
?q=jane@acme.com&fields=revenue → the person row + `revenue`, no campaign scan
?fields=revenue → 400 — the list form has no blocksRules that bite:
- FullVision resolves the legal set AFTER form selection, not per endpoint. Same
route, two answers: FullVision honours
fieldswithqand rejects it without it, because omittingqpicks the list form, which declares no blocks. - Empty, duplicated, unknown, and
*are all400s —fields=,fields=revenue,revenue,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. The contract is correct trimming — you get exactly the blocks you asked for and nothing else — never one fewer round trip per block dropped. It happens to buy a real saving here, but do not budget on that shape holding everywhere.
personandjourney_idare never droppable — they are the addressing result, not a block.fieldsnever lowers the scopes the endpoint requires.?fields=revenuestill needscustomer:read, and the rows still carry personal data.
API key as Bearer token
In: header
Query Parameters
Exact identifier lookup — email, cus_* id, person/visitor UUID, or external id. Omit for the group form.
Group form only: number of ranked rows to return (default 20, max 1000). Ignored when the addressing key is supplied.
1 <= value <= 1000Return rows after this cursor (from next_cursor).
Group form: the people behind one landing page's customer count — pass the cleaned page exactly as page-report returns it. Pair with scope. Mutually exclusive with form_id.
With first_touch_page: new (first-ever payment inside the range, default) or renew (existing customer who charged again inside it).
Value in
- "new"
- "renew"
Group form: the payers who submitted this form (reach cohort). A payer who submitted N forms appears under all N — never sum across forms. Mutually exclusive with first_touch_page.
Creation-mode bucket of the person's earliest record.
Value in
- "stripe"
- "form"
- "events"
Secondary value of the first-touch record, scoped by created_via.
First-touch channel bucket.
Value in
- "Organic Search"
- "AI"
- "Organic Social"
- "Paid Search"
- "Paid Social"
- "Email"
- "Direct"
- "Referral"
- "Non-attributed"
Granular classified first-touch source.
true = has a payment-bearing Stripe event.
Value in
- "true"
- "false"
true = the person has a non-empty email.
Value in
- "true"
- "false"
Plan name of the latest payment-bearing Stripe record.
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"
Custom start date (YYYY-MM-DD). Ignored if range is set.
^\d{4}-\d{2}-\d{2}$Custom end date (YYYY-MM-DD). Ignored if range is set.
^\d{4}-\d{2}-\d{2}$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.
Responses
One person (q supplied) or the criteria-filtered person cohort
/peoplecurl -X GET "https://data.fullvision.io/people?limit=20&created_via=form&paid=true"{ "person": {}, "journey_id": "string", "revenue": { "lifetime_revenue_cents": 0, "charge_count": 0, "first_charge_at": "string", "last_charge_at": "string" }, "acquisition": {}, "is_cached": true, "age_s": 0, "last_refresh": "string"}