# Report JSON

> The report as JSON, for dashboards, spreadsheets and alerts.

Every report is available as JSON at two addresses:

| URL | Auth |
| --- | --- |
| `https://opsen.dev/o/<org>/report.json` | Your opsen session cookie (owners only) |
| `https://opsen.dev/r/<token>.json` | The share token itself; see [Sharing](/docs/sharing) |

The share-link form is the one to use from scripts: create a link, store it as a secret, and revoke it when the script is retired.

```bash title="Fetch the yearly figure"
curl -s "https://opsen.dev/r/$OPSEN_SHARE_TOKEN.json" | jq '.annual_waste'
```

```python title="Post findings over $100/month to Slack"
import json, os, urllib.request

report = json.load(urllib.request.urlopen(
    f"https://opsen.dev/r/{os.environ['OPSEN_SHARE_TOKEN']}.json"))

big = [f for f in report["findings"] if f["monthly_usd"] >= 100]
text = "\n".join(f"• {f['title']}: ${f['monthly_usd']:,.0f}/mo" for f in big)
urllib.request.urlopen(urllib.request.Request(
    os.environ["SLACK_WEBHOOK_URL"], method="POST",
    data=json.dumps({"text": f"{report['org']}: {text}"}).encode(),
    headers={"content-type": "application/json"}))
```

A `404` means there is no completed report yet, the link was revoked, or the app was uninstalled. The response is the latest **completed** report; an audit in progress does not change it until it finishes.

## Schema

```json title="Response"
{
  "org": "acme",
  "audit_id": "aud_3f9c1e0b7a2d41",
  "taken_at": 1792655874.9,
  "monthly_waste": 403.12,
  "annual_waste": 4837.44,
  "findings": [
    {
      "id": "copilot.idle",
      "category": "copilot",
      "title": "Copilot seats unused for 30+ days",
      "monthly_usd": 266.0,
      "basis": "GitHub's last_activity_at per seat; seats younger than 30 days are excluded",
      "detail": "Reclaiming sets a seat to pending cancellation: …",
      "items": [
        {"login": "carol", "last_activity": null, "plan_type": "business",
         "via_team": null, "monthly_usd": 19.0, "price_source": "list"}
      ],
      "action": {"type": "reclaim_copilot"}
    }
  ],
  "spend": {
    "plan": "team", "seats": 6, "seats_monthly": 24.0,
    "copilot_credits_last_month": 160.0,
    "copilot_credits_by_model": [{"model": "claude-opus-5-5", "usd": 129.5}],
    "copilot_seats": 16, "copilot_monthly": 304.0,
    "actions_last_month": 42.5, "actions_source": "billing usage report"
  },
  "withheld": [],
  "coverage": {
    "repositories": 2, "repositories_with_workflows": 2, "runs": 13,
    "jobs_priced": 12, "members": 35, "copilot_seats": 16, "errors": 0,
    "truncated": {}, "api_calls": 26, "repository_selection": "all"
  },
  "attribution": null,
  "sources": {
    "gitlab": {
      "name": "GitLab", "total_usd": 444.0, "budget": "platforms",
      "kpis": [["Plan", "Premium"], ["Billable seats", "10"]],
      "lines": [{"name": "6 seats in use", "usd": 174.0},
                {"name": "4 dormant seats", "usd": 116.0, "waste": true, "fid": "gitlab.dormant"}],
      "note": "Premium at $29 a user a month, list price."
    }
  }
}
```

Timestamps are Unix seconds, UTC. Money is US dollars.

## Fields

| Field | Type | |
| --- | --- | --- |
| `findings[].id` | string | Stable identifier, one of the ids below |
| `findings[].monthly_usd` | number | The finding's contribution to `monthly_waste` |
| `findings[].basis` | string | How the figure was measured, and whether prices were `list` or `contract` |
| `findings[].items` | array | The rows behind the finding; shape depends on `id` |
| `findings[].category` | string | `seats`, `copilot`, `actions`, or the platform it came from: `gitlab`, `azure_devops`, `atlassian`, `jetbrains`, `circleci`, `cursor`, `openai` |
| `findings[].action.type` | string | `reclaim_copilot`, `cancel_invitations`, `disable_workflow`, `fix_pr`, `source` (a one-click change on a platform, with `source` and `name`) or `export` |
| `withheld[]` | `{id, reason}` | Findings not reported, and why |
| `coverage.truncated` | object | Limits that were hit, keyed by what was capped |
| `attribution` | object or null | Claude Code spend, when a key is connected |
| `sources` | object | One entry per connected platform: its monthly `total_usd`, the `budget` it counts toward, and `lines` that add up to it. Waste lines carry the `fid` of their finding |

## Finding ids

| id | Documented in |
| --- | --- |
| `seats.dormant_member` | [Seats](/docs/seats#dormant-members) |
| `seats.dormant_outside_collaborator` | [Seats](/docs/seats#dormant-outside-collaborators) |
| `seats.stale_invitations` | [Seats](/docs/seats#stale-invitations) |
| `copilot.idle` | [Copilot](/docs/copilot) |
| `actions.superseded` | [Actions Minutes](/docs/actions-minutes#superseded-pull-request-runs) |
| `actions.dormant_schedules` | [Actions Minutes](/docs/actions-minutes#schedules-on-dormant-repositories) |
| `actions.hung` | [Actions Minutes](/docs/actions-minutes#jobs-that-hang) |

New finding ids may be added. Existing ids and fields are not renamed or removed.
