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
assigneeIdsthey pass is ignored. - Admins may pass
assigneeIdsto 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 & path | Purpose |
|---|---|
GET /crm-reports | List reports (own + organization-visible) with snapshot counts |
POST /crm-reports | Create a report |
GET /crm-reports/:id | Get a report definition |
PATCH /crm-reports/:id | Update title, description, layout, visibility, or default timeframe |
DELETE /crm-reports/:id | Delete a report (cascades its snapshots) |
POST /crm-reports/metrics/run | Batch-run widgets and return series (live) |
POST /crm-reports/:id/snapshots | Freeze a snapshot |
GET /crm-reports/:id/snapshots | List a report's snapshots |
GET /crm-reports/snapshots/:snapshotId | Get a frozen snapshot |
POST /crm-reports/snapshots/:snapshotId/renew | Extend a snapshot's expiry (+180 days) |
DELETE /crm-reports/snapshots/:snapshotId | Delete 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:
| Field | Type | Notes |
|---|---|---|
widgets | array | Each: { instanceId, metricId, chart, groupBy?, timeframe?, filters? } |
assigneeIds | string[] | Optional admin scope. Ignored for non-admins |
timeframe | object | Report 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.
| Field | Type | Notes |
|---|---|---|
filters | Record<string, string | string[]> | Optional. Per-widget filter map |
Recognised keys (lead-scoped in v1):
| Key | Value type | Meaning |
|---|---|---|
tag | uuid[] | Keep only leads carrying any of these tag ids |
source | lead source enum[] | Keep only leads from these acquisition sources |
status | CRM 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
metricId | Shape | Supported charts | Notes |
|---|---|---|---|
leads.by_tag | category | bar, pie, donut, radar | Distribution 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_breakdown | category | bar | Where 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_agent | leaderboard | leaderboard | Commission 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.
Related Articles
API Overview
Introduction to the Fondaro API — base URL, response format, error handling, and pagination.
Authentication
How to create, use, and revoke Fondaro API keys for programmatic access.
Reports (dashboard)
Build live dashboards over your CRM, freeze them as point-in-time snapshots, export to PDF or CSV, and let AI design a report for you.