Report and activity endpoints

Schemas live in lib/schema/reports.ts; every response is parsed through them in the hook. Date filters are anchored to America/New_York. See Reporting and activity.

GET /api/reports/daily-stats

Source

vm_main_campaign_reports — a view over the call_daily_stats rollup table

Parameters

dateFrom, dateTo

{ "data": [ { "created_date": "2026-01-15", "unique_calls": 100, "…": 0 } ], "totals": { "unique_calls": 100 } }

Volume fields: unique_calls, total_duration_m, billed_minutes, calls_picked_up.

Disposition fields: ivr, gatekeeper, dial_failed, qualified_lead, call_back, not_qualified, not_a_seller, not_interested, wrong_num, voice_mail, ai_screening, busy, hangup_early, ai_issue, do_not_call, phone_provider_unavailable, dial_no_answer, user_declined, invalid_destination, dial_busy, user_hangup, error_retell, tel_provider_perm_denied, no_disposition.

totals is computed by the database, so the UI never re-sums the page.

GET /api/reports/funnel

Source

main_required_summaries

Parameters

dateFrom, dateTo

{
  "daily": [ { "created_date": "2026-01-15", "total": 100, "valid_conversation": 40, "hubspot_synced": 12, "follow_up_needed": 8 } ],
  "aggregated": { "total": 100, "valid_conversation": 40, "hubspot_synced": 12, "follow_up_needed": 8 }
}

GET /api/reports/sankey-chart

Source

main_sankey_chart_view

Parameters

dateFrom, dateTo

{
  "daily": [ { "created_date": "2026-01-15", "total_calls": 100,
               "sankey_data": { "nodes": [{ "name": "Total_Calls" }], "links": [{ "source": 0, "target": 1, "value": 40 }] } } ],
  "aggregated": { "total_calls": 100, "sankey_data": { "nodes": [], "links": [] } }
}

source and target are indices into nodes. Node colours come from SANKEY_NODE_COLOR_MAP in lib/constants.ts, so a disposition is the same colour in every chart.

GET /api/reports/daily-agent-performance

Source

vm_main_daily_agent_performance_report — a view over the agent_daily_performance_stats rollup table

Parameters

date, or dateFrom and dateTo

Per date and AI agent: ai_agent_id, ai_agent_name, total_calls, total_analysis, avg_score, total_score, correct_dispositions, correct_disposition_pct, avg_qa_ai_agent_score, total_qa_ai_agent_score, total_critical_issues, hangup_calls, hangup_disposition_rate_pct.

Response

{ "data": […​] }

GET /api/reports/phone-number-pickup-rate

Source

phone_number_pickup_rate

Parameters

state, provider

Per-number pickup rate. The query joins "PhoneNumbers" — quoted, because that table keeps the PascalCase default name.

GET /api/reports/email-campaign-daily

Source

public.v_email_campaign_daily_engagement

Parameters

campaignId, dateFrom, dateTo

GET /api/reports/email-campaign-target-summary

Recipient status distribution over a date range, from vm_email_campaign_target_summary. Each day is the America/New_York date of sentAt. total_opened and total_clicked count targets that are not bounced. The dashboard open and click rates divide those counts by delivered mail (total_sent minus total_bounced). Parameters:: dateFrom, dateTo

GET /api/reports/ai-calling

Source

Outcomes from BigQuery hubspot4.v_calls_latest for owner 86817215, joined to hubspot4.v_contacts_latest, hubspot4.v_deals_latest, and PayabilitySheets.v_supplier_summary_extended. The Calls total on the page comes from GET /api/reports/daily-stats (vm_main_campaign_reports), because not every AI call is synced to HubSpot.

Parameters

dateFrom, dateTo (YYYY-MM-DD). Missing values default to the last 30 Eastern days. Spans longer than 366 days return 400.

Auth via resolveSubscription(). These views have no tenant column, so results are not filtered by subscriptionId.

A deal counts only when created_date is on or after the contact’s first AI call in the range. Deal won is stage closedwon (Active (Won)) or 1045206663 (Closed Won). lost_reasons counts people on a lost best deal, grouped by HubSpot lost reason. Funded is first_purchase_date on or after that first AI call. deal_amount_total is HubSpot deal amount on won deals, not funded dollars. v_deals_latest must exist before deal columns return data.

Each warm_leads row has engagement_since (first Warm Lead call in the range, or first AI call for won-only contacts), human_calls_since (HubSpot calls by an owner other than 86817215), and emails_since (from hubspot4.v_emails_latest). Both counts run from engagement_since to now. v_emails_latest must exist in BigQuery, otherwise the whole report request fails.

GET /api/reports/reengagement-daily

Source

BigQuery outreach_app.v_re_engagement_daily_call_summary

Parameters

dateFrom, dateTo (YYYY-MM-DD list dates). Missing values default to the last 14 Eastern days. Spans longer than 90 days return 400.

Auth via resolveSubscription(). The view has no tenant column, so results are not filtered by subscriptionId.

Response
{
  "date_from": "2026-08-24",
  "date_to": "2026-09-07",
  "daily": [{ "list_date": "2026-09-01", "segment_name": "Lost deal", "total_contacts": 40, "called_contacts": 12, "connected_contacts": 3 }],
  "by_segment": [{ "segment_name": "Lost deal", "total_contacts": 200, "called_contacts": 80, "connected_contacts": 15, "calls_connected": 10 }],
  "funnel": { "total_contacts": 200, "called_contacts": 80, "uncalled_contacts": 120, "connected_contacts": 15, "positive_calls_contacts": 4 },
  "contact_funnel": { "total_contacts": 40, "called_contacts": 18, "uncalled_contacts": 22, "never_called_contacts": 10, "connected_contacts": 6, "positive_calls_contacts": 2 }
}

by_segment and funnel are summed entry counts in lib/services/reengagement-daily-call-report-service.ts. contact_funnel counts each HubSpot contact once for the same range. Dates are bound as STRING and compared with DATE(@dateFrom) / DATE(@dateTo).

daily and by_segment include per-disposition call counts from the view (calls_connected, calls_account_created, calls_api_connected, calls_hang_up, and the rest of the mapped HubSpot IDs). The segment chart stacks those counts. connected_contacts uses the view’s conversation-disposition IN list. positive_calls_contacts is Warm Lead, Follow up scheduled, API Connected Confirmed, or Account Created. uncalled_contacts is contacts on that list date with no human call on the list date or the next day. never_called_contacts is contacts on that row with no human call on or after their first list date. The view still supplies the date-window counts; the query adds never_called_contacts.

GET /api/reports/reengagement-contacts

Contact-level rows for list dates in the requested range. dialled is that row’s own window (human call on the list date or the next day; owner 86817215 excluded). ever_dialled is true when any of the contact’s list dates, on or after their first list date, has such a call. dialStatus filters on ever_dialled. Parameters:: dateFrom, dateTo, dialStatus (all, dialled, not_dialled), segment, search, page, pageSize.

GET /api/reports/reengagement-contacts/history?hubspotContactId= returns up to 100 list appearances for one HubSpot contact, newest first. Each history row’s dialled flag is that date’s own window.

GET /api/reports/reengagement-results

Source

BigQuery outreach_app.v_re_engagement_results_summary

Parameters

none (all-time aggregation)

Auth via resolveSubscription(). The view has no tenant column, so results are not filtered by subscriptionId. The query returns one aggregated row of COUNTIF buckets — it does not select contact fields.

Status matching is LOWER(TRIM(…​)). API needed counts needed and required. Remainder buckets are other (non-null unmatched) and null. with_first_purchase counts contacts whose supplier has a non-null first_purchase_date. attributed_wins counts contacts whose first_purchase_date is on or after first_listdate.

Response
{
  "total_contacts": 2547,
  "no_supplier": 1559,
  "with_first_purchase": 88,
  "attributed_wins": 12,
  "api": { "confirmed": 172, "in_review": 67, "needed": 341, "other": 1, "null": 1966 },
  "access": { "incomplete": 295, "rejected": 65, "pending": 83, "verified": 138, "other": 0, "null": 1966 },
  "payability": { "pending": 894, "active": 18, "suspended": 70, "other": 0, "null": 1565 }
}

GET /api/activity

The unified feed: Activity rows merged with Call rows.

Parameter Filter

type

A single ActivityType

types

Comma-separated ActivityType values

search

Free text

campaignId

Restrict to one campaign

dateFrom, dateTo

Date range

page, pageSize

Pagination

Response

{ data, total, page, pageSize }

Normalisation is done by lib/utils/map-activity-feed.ts; campaign names are resolved in bulk by lib/utils/resolve-activity-campaign-names.ts rather than per row. Hook: useActivity(params?).

Views prefixed vm_ are materialised. If a report looks stale, check the view’s refresh schedule before investigating the route.