> ## Documentation Index
> Fetch the complete documentation index at: https://docs.contraforce.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Pull Reporting into a BI Tool

> Use a service account to pull a monthly incident and agent summary for each workspace into Power BI, Excel or another BI tool.

The [reporting endpoints](https://docs.contraforce.com/api-reference/endpoints.md#reporting) return the same totals and trends as the portal's reporting page. This guide pulls a month-by-month summary for each workspace, one row per workspace and month, ready to load into a BI tool.

> [!NOTE]
>
> **Who is this for?** Security engineers and analysts who build dashboards in Power BI, Excel or another BI tool from ContraForce reporting.

## Prerequisites

* A [service account](https://docs.contraforce.com/guides/general-support/service-accounts.md) with a credential that carries `incidents:read`.
* A role for the service account on each workspace you want to report on. Keep a list of those workspace IDs: the service account reads only the workspaces it holds a role on.

## How it works

Ask for a range longer than 92 days and every trend comes back with one point per month. One call to each of two endpoints per workspace then covers up to a year:

* `POST /reporting/incident-flow`: incidents received, closed (and who closed them) and investigated, and the median time to close.
* `POST /reporting/agent-investigations`: agent runs and how they ended.

Name one workspace in `workspaceIds` to get that workspace's figures on their own.

## Example

This Python script pulls January to October 2026 for each workspace and writes `reporting-summary.csv`. It reads the credential from the `CONTRAFORCE_CLIENT_ID` and `CONTRAFORCE_CLIENT_SECRET` environment variables.

```python theme={null}
import base64
import csv
import os
import time

import requests

BASE_URL = "https://portal.contraforce.com/api/v2"
WORKSPACES = {
    "3f2b8c1e-7d4a-4e5b-9a61-2c8f0d7e4b19": "Contoso Retail",
    "9a7d2e64-1b3c-4f8e-b5a2-6e0c4d1f8a73": "Fabrikam Health",
}

credential = f"{os.environ['CONTRAFORCE_CLIENT_ID']}:{os.environ['CONTRAFORCE_CLIENT_SECRET']}"
session = requests.Session()
session.headers["Authorization"] = "Basic " + base64.b64encode(credential.encode()).decode()


def report(path, workspace_id):
    body = {"from": "2026-01-01", "to": "2026-10-31", "workspaceIds": [workspace_id]}
    while True:
        response = session.post(f"{BASE_URL}/reporting/{path}", json=body, timeout=60)
        if response.status_code == 429:
            time.sleep(60)  # The budget refills every minute.
            continue
        response.raise_for_status()
        return response.json()["data"]


with open("reporting-summary.csv", "w", newline="") as file:
    writer = csv.writer(file)
    writer.writerow([
        "workspace", "month", "received", "closed", "closed_by_agent", "closed_by_analyst",
        "investigated", "median_hours_to_close", "agent_runs", "resolved_by_agent", "escalated",
    ])
    for workspace_id, name in WORKSPACES.items():
        flow = report("incident-flow", workspace_id)
        runs_by_month = {point["date"]: point for point in report("agent-investigations", workspace_id)["points"]}
        for point in flow["points"]:
            runs = runs_by_month.get(point["date"], {})
            median = point["medianTimeToCloseSeconds"]
            writer.writerow([
                name,
                point["date"],
                point["received"],
                point["closed"],
                point["closedBy"].get("Agent", 0),
                point["closedBy"].get("Analyst", 0),
                point["investigated"],
                round(median / 3600, 1) if median is not None else "",
                runs.get("runs", 0),
                runs.get("resolvedByAgent", 0),
                runs.get("escalated", 0),
            ])
```

Each row's `month` is `yyyy-MM`, a UTC calendar month.

## Things to know

* `received` counts incidents detected in the month and `closed` counts closures in the month, so an incident detected in March and closed in April counts once in each.
* `medianTimeToCloseSeconds` is `null` for a month with no closures whose detection time is known.
* Each workspace takes two calls. The [rate limit](https://docs.contraforce.com/api-reference/endpoints.md#rate-limits) is 120 reporting calls a minute per service account, so pace the calls when you report on more than 60 workspaces, rather than relying on the wait after a `429`.
* Service accounts get the activity counts, not the spend: cost fields come back `null`.
* For the incidents behind a figure, such as every incident escalated in March, read the records with `/reporting/facts` or export them with `/reporting/facts/export`.

## Related guides

> [card] [Reporting Endpoints](https://docs.contraforce.com/api-reference/endpoints.md#reporting)
>
> Every reporting endpoint, its limits, the record types and the rate limits.

> [card] [Reporting Object Models](https://docs.contraforce.com/api-reference/models.md#reporting)
>
> Example requests and responses, and the keys each breakdown uses.

> [card] [Service Accounts](https://docs.contraforce.com/guides/general-support/service-accounts.md)
>
> Create a service account and issue the credential this guide uses.

> [card] [Error Reference](https://docs.contraforce.com/api-reference/errors.md)
>
> What each error code means and how to act on it.

> [!NOTE]
>
> Need help? Contact [support@contraforce.com](mailto:support@contraforce.com).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.