Get Performance Metrics
Get outreach performance metrics: time-series trends and/or conversion funnel.
Set view='timeseries' for daily counts of a single metric (contacts/people/replies/meetings). Set view='funnel' for aggregate conversion data: contacted->replied->met funnel with rates, reply rates by channel, and LinkedIn pipeline breakdown. Set view='both' for everything in one call.
For T1s sent over time: view='timeseries', metric='contacts'. For reply rate and funnel: view='funnel'. Operators: pass for_company to get metrics for a specific client.
UNIT CONVENTIONS USED IN RESPONSES Every count field is one of {messages, unique_people, plans, campaigns, days}; see the Returns block for the unit attached to each field. Note that timeseries 'contacts' and funnel.contacted are BOTH unique_people (i.e. unique people, deduped per person_id), NOT message counts.
DATE-FILTER COLUMN PER METRIC (timeseries)
- metric='people': filters on company_people.created_at — start_date / end_date
- metric='contacts': filters on messages.sent_at (sequence_number=1 only) — start_date / end_date
- metric='replies': filters on company_people.replied_at — start_date / end_date
- metric='meetings': filters on outcomes.observed_at (meeting_booked / meeting_held attributed to an outreach send) — start_date / end_date ONE SOURCE FOR MEETINGS: funnel.meetings, stage_funnel.meetings_all and the 'meetings' timeseries read the same outcomes ledger under the same attribution rule. They differ only in the clock: the funnel windows on the SEND (people sent in the window who met, whenever), the timeseries on the MEETING (people whose meeting fell in the window). A meeting with a person no outreach send reached is in neither. reply_rates_by_channel and stage_funnel share one scope per response mode — the stage funnel is derived from the same per-channel counts, never from a separate query.
Returns (selected fields, with unit + scope + date-filter annotations): view (str): Echoes the requested view.
timeseries (object, when view='timeseries' or 'both'):
metric (str): The metric name.
data (list of {date, count}):
date (str): YYYY-MM-DD.
count (int): unit = unique_people for ALL four metrics (T1-sent dedupe by
person; company_people rows for people/replies; for meetings, each person
once per window at their first meeting outcome in it).
funnel (object, when view='funnel' or 'both'):
total_people (int, unit=unique_people, scope=non-archived company_people in
tenant, no date filter): denominator for contact_rate.
contacted (int, unit=unique_people, scope=non-archived; date-filter:
company_people.last_message_sent_at): distinct people with at least one
sent message in window. NOT the total message count.
replied (int, unit=unique_people, scope=non-archived; date-filter:
company_people.replied_at): distinct people who replied. NOT the total
reply-message count.
meetings (int, unit=unique_people, scope=includes archived; date-filter:
the send's messages.sent_at): send-cohort people with a meeting_booked /
meeting_held outcome, whenever observed. Equal to stage_funnel.meetings_all.
A meeting is a permanent positive outcome so it counts regardless of
archive state.
summary (str): human prose summary of the funnel.
conversion_rates (object): contact_rate, reply_rate, meeting_rate, booking_rate
— percentages computed from the funnel counts above, NOT independent measures.
reply_rates_by_channel (list, when view='funnel' or 'both'):
One entry per channel actually used. Per-channel entries (unit=unique_people
for sent/replied/accepted):
- 'linkedin_connection': sent + accepted + acceptance_rate (NOT a reply_rate;
connection requests are top-of-funnel, replies happen on follow-up DMs).
note: "Acceptance rate, not reply rate. Replies happen on follow-up messages."
accepted counts DISTINCT cohort people with a connection_accepted
OUTCOME on the cohort send's action (webhook-observed or
reply-inferred; organic connections have no cohort send and are
structurally excluded).
- 'linkedin_message': sent + replied + reply_rate (follow-up DMs sent to
connected people).
- 'email': sent + replied + reply_rate.
- 'linkedin_inmail': sent + replied + reply_rate.
stage_funnel (object, when view='funnel' or 'both'):
VRU-862 M3.4: derived with reply_rates_by_channel from ONE
spine cohort projection (actions × outcomes), so the sections
agree by construction. The frame is COHORT-scoped: the
population is the outreach sends in the window (messages.sent_at;
status sent/replied/bounced — bounced sends count as sent), and
every later stage is a subset of that cohort via its actions'
outcome rows, whenever observed. No rate can exceed 100%.
Objective scoping keys on the send's own authoring objective
(messages.objective_id — VRU-878 PR B; a legacy campaign is an
objective now). Each rate in
reply_rates_by_channel also ships a *_detail object carrying
numerator, denominator, and metric_definition_version, and the
response includes metric_definitions (the versioned contracts).
connection_requests_sent (int, unit=unique_people): the
linkedin_connection send cohort.
connections_accepted (int, unit=unique_people): cohort members
with a connection_accepted outcome (webhook-observed or
reply-inferred; organic connections without a Vruum invite
cannot appear — they have no cohort send).
acceptance_rate (float, percent): connections_accepted /
connection_requests_sent.
followups_sent (int, unit=unique_people): the linkedin_message
send cohort.
followup_replies (int, unit=unique_people): cohort members with
a STANDING reply outcome (reversals fold out; the outcome's
observed time is the reply's own, whenever it landed).
followup_reply_rate (float, percent): of the follow-up DMs SENT
(NOT of accepted connections).
meetings (int, unit=unique_people): the cohort's meeting people who
ALSO hold a standing reply (the numerator of
reply_to_meeting_rate). meetings_all is every cohort person with
a meeting outcome (meeting_booked/meeting_held attributed to
cohort actions, influence sets included) and equals
funnel.meetings.
reply_to_meeting_rate (float, percent): a SUBSET rate — cohort
people with BOTH a standing reply and a meeting outcome, over
cohort standing repliers; reply_to_meeting_rate_detail carries
the counts + contract version. Bounded by construction
(meetings without a reply appear in meetings_all, not here).
(The response also carries `coverage`: cohort messages without a
spine action can enter denominators but never numerators —
coverage.complete=false labels a degraded read honestly.)
summary (str): human prose summary.
"Biggest drop:" picks the largest absolute drop among
(connection_requests_sent - connections_accepted),
(followups_sent - followup_replies),
(replied - meetings). This is a raw-count heuristic; it can name
"connection acceptance" even when the acceptance rate is healthy
(e.g. 80%) because the absolute drop is still the largest of the three.
connections (object, when view='connections'):
total_requested (int, unit=plans).
acceptance_rate (float, percent of plans).
avg_time_to_accept_hours (float|null).
withdrawal_rate (float, percent): plans auto-withdrawn / total requested.
retry_success_rate (float): connected-after-retry / completed-retried plans.
by_status (object): plan counts keyed by connection_status.
Request
- Base URL: https://api.vruum.ai
- URL: https://api.vruum.ai/api/analytics/performance
- Auth: HTTPBearer (a scheme the document does not define)
Query parameters
What to return: 'timeseries' for daily counts of a single metric, 'funnel' for aggregate conversion data, 'both' for everything in one call.
Metric for timeseries view. Each value documents its own scope + date-filter column (units in parentheses): 'people' (unique_people) = new company_people rows for this tenant, filtered on company_people.created_at — i.e. imports per day; 'contacts' (unique_people) = T1 messages ONLY (messages.sequence_number=1), deduped per person, filtered on messages.sent_at — follow-up touches NOT counted (to see total send activity per day, query messages table directly); 'replies' (unique_people) = distinct company_people whose replied_at falls in window, filtered on company_people.replied_at (NOT the original message's sent_at); 'meetings' (unique_people) = distinct people with a meeting_booked or meeting_held outcome observed in window, attributed to an outreach send (the funnel's ledger and attribution rule), filtered on outcomes.observed_at. Required when view includes 'timeseries' or 'both'.
Start date (YYYY-MM-DD), inclusive. Filters on the metric-specific timestamp column documented in metric for timeseries view; the funnel view filters the send cohort on messages.sent_at (contacted, replied and meetings are that cohort's people). Omit for last-30-days default on timeseries; omit for all-time on funnel.
End date (YYYY-MM-DD), inclusive of the whole end day. Funnel-view bounds (company_people, messages, outreach_plans) widen a bare date to end-of-day UTC; timeseries 'contacts' uses its next-day exclusive bound. Applied to reply_rates_by_channel and stage_funnel in BOTH modes (unscoped and objective-scoped).
Filter results to objective(s) (UUID, comma-separated for several). Applies to all funnel totals, connections, and timeseries metrics; the per-channel/stage-funnel breakdown scopes to the FIRST objective when several are given. A legacy campaign is an objective now — pass its objective id.
Response
Successful Response
Changes
No recorded changes to this operation across all 1 revision of this API.