analytics

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.
get/api/analytics/performance

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

viewstring

What to return: 'timeseries' for daily counts of a single metric, 'funnel' for aggregate conversion data, 'both' for everything in one call.

metricstring nullable

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_datestring nullable

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_datestring nullable

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).

objective_idstring nullable

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

unknown required

Changes

No recorded changes to this operation across all 1 revision of this API.