CRM Reports

Endpoints for building, running, and freezing CRM reports: report CRUD, the batch metric run contract, the widget catalog, and assignee gating.

Overview

The CRM Reports API persists reports (saved widget layouts), runs their widgets against live data, and freezes point-in-time snapshots. All endpoints are under /crm-reports, require a Clerk bearer token, and resolve the active organization from the request.

Every request is scoped to the caller's organization and re-gated by role:

  • Members only ever see rows they own or are assigned to. Any assigneeIds they pass is ignored.
  • Admins may pass assigneeIds to aggregate across the selected members, or omit it to see only their own data.

This gating is recomputed on the server for every metric run, so a member opening an admin's shared report still sees only their own data.

Endpoints

Method & pathPurpose
GET /crm-reportsList reports (own + organization-visible) with snapshot counts
POST /crm-reportsCreate a report
GET /crm-reports/:idGet a report definition
PATCH /crm-reports/:idUpdate title, description, layout, visibility, or default timeframe
DELETE /crm-reports/:idDelete a report (cascades its snapshots)
POST /crm-reports/metrics/runBatch-run widgets and return series (live)
POST /crm-reports/:id/snapshotsFreeze a snapshot
GET /crm-reports/:id/snapshotsList a report's snapshots
GET /crm-reports/snapshots/:snapshotIdGet a frozen snapshot
POST /crm-reports/snapshots/:snapshotId/renewExtend a snapshot's expiry (+180 days)
DELETE /crm-reports/snapshots/:snapshotIdDelete a snapshot

The metric run contract

POST /crm-reports/metrics/run is the hot path the builder calls on every config change. It runs all widgets with bounded concurrency and returns one series per widget, keyed by instanceId.

Request body:

FieldTypeNotes
widgetsarrayEach: { instanceId, metricId, chart, groupBy?, timeframe?, filters? }
assigneeIdsstring[]Optional admin scope. Ignored for non-admins
timeframeobjectReport default: { preset, from?, to?, bucket }

Widget filters

A widget may carry a filters object that narrows the population before it is aggregated. This is distinct from groupBy, which splits the result into series.

FieldTypeNotes
filtersRecord<string, string | string[]>Optional. Per-widget filter map

Recognised keys (lead-scoped in v1):

KeyValue typeMeaning
taguuid[]Keep only leads carrying any of these tag ids
sourcelead source enum[]Keep only leads from these acquisition sources
statusCRM status enum[]Keep only leads in these CRM statuses

Filters apply to the lead metrics that declare them (every leads.* metric except leads.avg_time_in_status). They are ignored by deal, task, call, email, and team metrics. A single value may be sent as a string or as a one-element array; the runner coerces it. The runner parses filters defensively: it validates tag ids as UUIDs and ignores any unrecognised key. Report persistence does not validate filters, so the saved value round-trips verbatim and only the runner interprets it.

curl -X POST https://api.fondaro.com/crm-reports/metrics/run \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "timeframe": { "preset": "last_30d", "bucket": "day" },
    "assigneeIds": [],
    "widgets": [
      { "instanceId": "w1", "metricId": "leads.created_over_time", "chart": "area" },
      { "instanceId": "w2", "metricId": "deals.win_rate", "chart": "radial" }
    ]
  }'

Each result is a MetricSeries:

{
  "instanceId": "w1",
  "shape": "timeseries",
  "series": [{ "key": "value", "label": "New leads" }],
  "points": [{ "bucket": "2026-05-01", "value": 12 }],
  "stat": null
}

shape is one of timeseries, category, stat, or leaderboard. Timeseries points carry a bucket field; category points carry a category field; stat results carry a stat object with value, previous, and deltaPct.

The widget catalog

Reports can only reference metrics from a fixed catalog (the single source of truth shared by the builder and the AI designer). The backend validates every metricId and chart type on create, so arbitrary queries are never possible. Metrics span six categories: leads, deals, tasks, calls, emails, and team (team metrics are admin-only). Each metric declares its shape, default chart, the chart types it supports, any group-by dimensions, any filters it accepts, and an adminOnly flag.

Tag and commission metrics

metricIdShapeSupported chartsNotes
leads.by_tagcategorybar, pie, donut, radarDistribution of leads across tags. Counts are non-exclusive: a lead counts toward every tag it carries, so the points can sum to more than the lead total. Accepts the lead filters. Supports timeframe. Not admin-only
deals.commission_breakdowncategorybarWhere commission on won deals goes, in the dominant currency. Emits five ordered points: total commission, collaborator payout, agency commission, internal agent payout, agency net. Supports timeframe. adminOnly: true
deals.commission_by_agentleaderboardleaderboardCommission earned per internal agent on won deals, in the dominant currency. Supports timeframe. adminOnly: true

Both commission metrics are adminOnly. As with every admin-only metric, a non-admin run returns an empty series rather than an error, and the metric is hidden from the catalog the builder shows to non-admins.

Snapshots

A snapshot stores a frozen copy of the layout plus the computed series. It is computed under the caller's scope at freeze time, expires after 180 days (renewable), and is hard-deleted by a daily cleanup job 30 days after expiry.

Fondaro Help

Docs & support

Hi there, how can we help?

Browse popular articles or ask a question below.

Popular articles

Or ask a question