# Reports

> Create and run CRM reports from a fixed metric catalog, and freeze snapshots of their results.

The Reports API saves reports (a layout of widgets), runs their widgets against live CRM data, and freezes snapshots of the results. Every widget names a metric from a fixed catalog, so a report can never run an arbitrary query.

These endpoints take a signed-in member's session token (`Authorization: Bearer <session-token>`); API keys do not open them. See [Authentication](https://www.fondaro.com/docs/api/authentication.md).

## Who sees which data

The server works out the scope on every run:

- A **member** sees only records they are assigned to. Any `assigneeIds` they send is ignored.
- An **admin** may send `assigneeIds` to report on those people. Without it, an admin sees only their own records.
- Metrics marked admin only return an empty series to a member instead of an error.

A member who opens a report an admin shared still sees only their own data.

## Run widgets

`POST /crm-reports/metrics/run`

Runs a batch of widgets and returns one series per widget, keyed by `instanceId`. Nothing is saved.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `widgets` | Body, array | Yes | Each `{ instanceId, metricId, chart, groupBy?, timeframe?, filters? }` |
| `widgets[].metricId` | Body, string | Yes | A metric id from the [catalog](#metric-catalog) |
| `widgets[].chart` | Body, string | Yes | `bar`, `line`, `area`, `pie`, `donut`, `radar`, `radial`, `stat`, `leaderboard` or `table` |
| `widgets[].groupBy` | Body, string | No | Split the result: `status`, `source`, `language`, `country`, `currency`, `stage`, `direction`, `outcome` or `assignee`, where the metric supports it |
| `widgets[].timeframe` | Body, object | No | Overrides the report timeframe for this widget |
| `widgets[].filters` | Body, object | No | Narrow the rows before they are counted (see [Widget filters](#widget-filters)) |
| `timeframe` | Body, object | Yes | `{ preset, from?, to?, bucket }` |
| `assigneeIds` | Body, string[] | No | Admins only: the people to report on |

`timeframe.preset` is `last_7d`, `last_30d`, `last_90d`, `this_month`, `last_month`, `this_quarter`, `last_quarter`, `ytd` or `custom` (then send `from` and `to` as ISO dates). `bucket` is `day`, `week`, `month` or `quarter`.

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

```json
{
  "results": [
    {
      "instanceId": "w1",
      "shape": "timeseries",
      "series": [{ "key": "value", "label": "New leads" }],
      "points": [{ "bucket": "2026-09-01", "value": 12 }]
    },
    {
      "instanceId": "w2",
      "shape": "stat",
      "series": [],
      "points": [],
      "stat": { "value": 34.5, "previous": 30.1, "deltaPct": 14.6, "unit": "%" }
    }
  ]
}
```

`shape` is `timeseries` (points carry `bucket`), `category` (points carry `category`), `stat` (a `stat` object with `value`, and optionally `previous`, `deltaPct`, `unit`, `currency`) or `leaderboard`. An unknown `metricId` returns an empty series. Widgets run six at a time.

### Widget filters

`filters` is a map of filter id to a value or a list of values. A single value may be sent as a string.

| Key | Applies to | Value |
|---|---|---|
| `tag` | Lead metrics | Tag ids; keeps leads carrying any of them |
| `source` | Lead metrics | Lead sources |
| `status` | Lead metrics | Lead statuses |
| `invoicedNotCollected` | Deal metrics that offer it | `true` or `"true"` to switch on |
| `collectionDueThisMonth` | Deal metrics that offer it | `true` or `"true"` to switch on |
| `commissionsUnpaid` | Deal metrics that offer it | `true` or `"true"` to switch on |

Unknown keys are ignored. Several deal filters together must all match.

## Metric catalog

| Category | Metric ids |
|---|---|
| Leads | `leads.created_over_time`, `leads.by_status`, `leads.by_source`, `leads.by_language`, `leads.by_country`, `leads.qualification_rate`, `leads.status_funnel`, `leads.avg_time_in_status`, `leads.purchased_revenue`, `leads.by_tag`, `leads.time_to_first_answer` |
| Deals | `deals.created_over_time`, `deals.pipeline_by_stage`, `deals.pipeline_value_by_stage`, `deals.win_rate`, `deals.won_lost_over_time`, `deals.revenue_won_over_time`, `deals.avg_deal_size`, `deals.avg_time_to_close`, `deals.lost_reasons`, `deals.avg_time_in_stage`, `deals.stalled`, `deals.commission_won_over_time`, `deals.commission_breakdown`, `deals.commission_by_agent` and `deals.commission_due_by_agent` (admin only) |
| Tasks | `tasks.completed_over_time`, `tasks.completion_rate`, `tasks.overdue`, `tasks.status_split` |
| Calls | `calls.over_time`, `calls.by_outcome`, `calls.answer_rate`, `calls.avg_duration`, `calls.cost_over_time`, `calls.sentiment`, `calls.top_objections` |
| Emails and messages | `emails.sent_over_time`, `emails.by_status`, `messages.by_channel` |
| Team | `team.leaderboard`, `team.activity_by_assignee`, `team.time_to_first_answer` (all admin only) |

A metric's shape decides its charts: `timeseries` takes `line`, `area` or `bar`; `category` takes `bar`, `pie`, `donut` or `radar`; `stat` takes `stat` or `radial`; `leaderboard` takes `leaderboard` or `table`.

## Reports

### List reports

`GET /crm-reports`

Returns the reports you can see, most recently updated first: your own and those shared with the organization (an admin sees all). Each carries `snapshotCount` and `latestSnapshotAt`.

```bash
curl https://api.fondaro.com/crm-reports \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
[
  {
    "id": "c4e1...",
    "ownerId": "user_2abc...",
    "title": "Monthly pipeline",
    "description": null,
    "layout": { "widgets": [] },
    "defaultTimeframe": { "preset": "last_30d", "bucket": "day" },
    "savedAssigneeIds": null,
    "visibility": "organization",
    "aiGenerated": false,
    "snapshotCount": 2,
    "latestSnapshotAt": "2026-09-30T08:00:00.000Z",
    "createdAt": "2026-09-01T10:00:00.000Z",
    "updatedAt": "2026-09-30T08:00:00.000Z"
  }
]
```

### Create a report

`POST /crm-reports`

Returns the new report. Every widget's `metricId` and chart are checked against the catalog.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `title` | Body, string | Yes | Up to 200 characters |
| `description` | Body, string or null | No | |
| `layout` | Body, object | No | `{ widgets: [...] }`. Each widget is `{ instanceId, metricId, chart, title?, groupBy?, timeframe?, filters?, layout: { x, y, w, h } }` on a 12-column grid |
| `defaultTimeframe` | Body, object | No | `{ preset, from?, to?, bucket }` |
| `savedAssigneeIds` | Body, string[] or null | No | The people an admin's snapshot reports on |
| `visibility` | Body, `private` or `organization` | No | Who else can open it |

```bash
curl -X POST https://api.fondaro.com/crm-reports \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Monthly pipeline",
    "visibility": "organization",
    "layout": { "widgets": [
      { "instanceId": "w1", "metricId": "deals.pipeline_by_stage", "chart": "bar",
        "layout": { "x": 0, "y": 0, "w": 6, "h": 4 } }
    ] }
  }'
```

```json
{ "id": "c4e1...", "title": "Monthly pipeline", "visibility": "organization", "aiGenerated": false }
```

### Read, change or delete a report

| Endpoint | Purpose |
|---|---|
| `GET /crm-reports/:id` | Read one report |
| `PATCH /crm-reports/:id` | Change any of the create fields |
| `DELETE /crm-reports/:id` | Delete the report and its snapshots. Returns `204` |

Anyone who can see a report can read it; only its owner or an admin can change, delete or snapshot it.

```bash
curl -X PATCH https://api.fondaro.com/crm-reports/c4e1... \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "title": "Pipeline this quarter", "defaultTimeframe": { "preset": "this_quarter", "bucket": "week" } }'
```

```json
{ "id": "c4e1...", "title": "Pipeline this quarter", "defaultTimeframe": { "preset": "this_quarter", "bucket": "week" } }
```

## Snapshots

A snapshot freezes the report's layout and computed results under your scope at that moment. It expires 180 days after it is made or renewed and is deleted 30 days after expiry.

### Freeze a snapshot

`POST /crm-reports/:id/snapshots`

```bash
curl -X POST https://api.fondaro.com/crm-reports/c4e1.../snapshots \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
{
  "id": "9a7b...",
  "reportId": "c4e1...",
  "createdBy": "user_2abc...",
  "title": "Monthly pipeline (2026-10-02)",
  "layout": { "widgets": [] },
  "data": { "w1": { "instanceId": "w1", "shape": "category", "series": [], "points": [] } },
  "assigneeIds": ["user_2abc..."],
  "timeframe": { "from": "2026-09-02", "to": "2026-10-02", "bucket": "day" },
  "expiresAt": "2027-03-31T10:00:00.000Z",
  "aiGenerated": false,
  "createdAt": "2026-10-02T10:00:00.000Z"
}
```

### Other snapshot endpoints

| Endpoint | Purpose |
|---|---|
| `GET /crm-reports/:id/snapshots` | List a report's snapshots, newest first |
| `GET /crm-reports/snapshots/:snapshotId` | Read one snapshot |
| `POST /crm-reports/snapshots/:snapshotId/renew` | Set its expiry to 180 days from now |
| `DELETE /crm-reports/snapshots/:snapshotId` | Delete it. Returns `204` |

## Errors

| Status | When |
|---|---|
| `400` | Validation failed, an unknown metric, or a chart the metric does not support |
| `401` | No valid session token |
| `403` | You cannot see the report, or you are not its owner or an admin when changing it |
| `404` | The report or snapshot does not exist in your organization |

Source: https://www.fondaro.com/docs/api/crm-reports
