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 |
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.
Fetch the yearly figure
curl -s "https://opsen.dev/r/$OPSEN_SHARE_TOKEN.json" | jq '.annual_waste'
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
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 |
seats.dormant_outside_collaborator |
Seats |
seats.stale_invitations |
Seats |
copilot.idle |
Copilot |
actions.superseded |
Actions Minutes |
actions.dormant_schedules |
Actions Minutes |
actions.hung |
Actions Minutes |
New finding ids may be added. Existing ids and fields are not renamed or removed.