Reports API
Quick example
- curl
- Python
- Node.js
# Run a catalog report and download as CSV
curl "$TRINITITE_BASE/v1/reports/run/ciso.alerts_by_severity?format=csv&period=30d" \
-H "Authorization: Bearer $TRINITITE_API_KEY" \
-o alerts.csv
import os, requests
resp = requests.get(
f"{os.environ['TRINITITE_BASE']}/v1/reports/run/ciso.alerts_by_severity",
headers={"Authorization": f"Bearer {os.environ['TRINITITE_API_KEY']}"},
params={"format": "csv", "period": "30d"})
open("alerts.csv", "wb").write(resp.content)
const url = `${process.env.TRINITITE_BASE}/v1/reports/run/ciso.alerts_by_severity?format=csv&period=30d`;
const r = await fetch(url, { headers: { Authorization: `Bearer ${process.env.TRINITITE_API_KEY}` } });
const buf = Buffer.from(await r.arrayBuffer());
require('fs').writeFileSync('alerts.csv', buf);
See Enterprise Reporting for the architecture.
Overview
A horizontal BI surface that sits beside the persona dashboards in the rest of the platform. It provides:
- A catalogue of 60+ parameterised reports curated across eight personas.
- A semantic layer of whitelisted sources (tables and views) with typed dimensions, metrics, filters, and time-grain support.
- A safe query-builder that lets you compose your own saved-report definitions without touching SQL.
- Multi-format rendering: JSON, CSV, plain PDF, branded PDF, XLSX.
- Scheduled delivery (cron) over email, webhook, SFTP, and S3.
- Signed artifact downloads with row-level organisation isolation enforced at compile time.
Authentication: Authorization: Bearer <session_token | api_key> with analytics:read (and analytics:create / analytics:update / analytics:delete for the saved reports and schedules surfaces).
Most of this surface requires the enterprise_reporting entitlement. Branded PDF needs enterprise_reporting.branded_pdf; the query builder needs enterprise_reporting.builder; schedules need enterprise_reporting.schedules. Insufficient entitlement returns 403 entitlement_denied.
Endpoints
Catalogue
| Method | Path | Permission |
|---|---|---|
GET | /v1/reports/catalog | analytics:read |
GET | /v1/reports/catalog/{reportId} | analytics:read |
GET | /v1/reports/semantic-sources | analytics:read |
Runs & artifacts
| Method | Path | Permission |
|---|---|---|
POST | /v1/reports/runs | analytics:read |
GET | /v1/reports/runs | analytics:read |
GET | /v1/reports/runs/{runId} | analytics:read |
GET | /v1/reports/runs/{runId}/artifacts/{format} | analytics:read |
POST | /v1/reports/runs/{runId}/artifacts/{format}/sign | analytics:read |
GET | /v1/reports/artifacts/local/{relPath} | (signed URL only) |
Saved reports
| Method | Path | Permission |
|---|---|---|
GET | /v1/reports/saved | analytics:read |
GET | /v1/reports/saved/{savedReportId} | analytics:read |
POST | /v1/reports/saved | analytics:create |
PUT | /v1/reports/saved/{savedReportId} | analytics:update |
DELETE | /v1/reports/saved/{savedReportId} | analytics:delete |
Schedules
| Method | Path | Permission |
|---|---|---|
POST | /v1/reports/schedules | analytics:create |
GET | /v1/reports/schedules | analytics:read |
GET | /v1/reports/schedules/{scheduleId} | analytics:read |
PUT | /v1/reports/schedules/{scheduleId} | analytics:update |
DELETE | /v1/reports/schedules/{scheduleId} | analytics:delete |
Personas & catalogue
| Persona | Examples |
|---|---|
ceo | ceo.executive_summary, ceo.roi_quarterly |
cfo | cfo.cost_by_model, cfo.tool_cost_by_type |
ciso | ciso.alerts_by_severity, ciso.threat_source_breakdown |
cro | cro.var_trend, cro.sir_utilization |
gc | gc.failed_audit_events, gc.correction_trend_monthly |
auditor | auditor.evidence_pack, auditor.governance_coverage |
ops | ops.hourly_activity, ops.tool_block_rate |
Semantic layer
GET /v1/reports/semantic-sources enumerates every available source with its full schema (typed dimensions, metrics, filters, supported time grains, and the mandatory organisation column). The semantic layer is the atomic unit of the query builder — you can only reference dimensions / metrics / filters that the source declares.
Available sources include governance_daily (unified daily outcomes), tool_usage_hourly (MCP tool invocations), proxy_logs (row-level LLM calls), audit_logs (actor / action / resource), liability_daily, nhi_daily, tool_cost_hourly, mcp_alert_events, mcp_config_audit, correction_patterns, roi_snapshots.
Report definition shape
interface ReportDefinition {
id: string;
version: number;
persona: 'ceo' | 'cfo' | 'ciso' | 'cro' | 'gc' | 'auditor' | 'ops';
title: string;
description: string;
source_id: string; // whitelisted source
dimensions: string[]; // whitelisted dimension keys
metrics: string[]; // whitelisted metric keys (>= 1)
time_grain?: 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
default_filters: ReportFilterBinding[];
sort?: ReportSortSpec[];
limit?: number; // hard cap 10,000
params?: ReportParamSpec[];
chart_hints?: ChartHint[];
redact?: RedactionRule[];
entitlement_key?: string;
tags?: string[];
}
All whitelisting is enforced eagerly when a definition is registered, and re-checked at execution time inside the SQL compiler.
Run a report
POST /v1/reports/runs — synchronous; returns the run record, a 100-row preview, and downloadable artifacts.
{
"report_id": "ceo.executive_summary",
"formats": ["pdf_branded", "json"],
"params": {
"period_start": "2026-04-01T00:00:00Z",
"period_end": "2026-04-30T23:59:59Z"
},
"render_options": {
"title": "Acme Corp — April 2026 Executive Summary",
"subtitle": "Confidential — Board Distribution Only",
"organization_name": "Acme Corp",
"logo_url": "https://assets.acme.example.com/logo.svg"
}
}
| Field | Type | Description |
|---|---|---|
report_id | string | One-of: catalogue report ID |
saved_report_id | string | One-of: tenant-authored saved report ID |
formats | string[] | Any of json, csv, pdf, pdf_branded, xlsx |
params | object | Parameter overrides declared by the definition's params |
render_options | object | Branded-PDF parameters |
Response — 201 Created:
{
"run": {
"run_id": "rrun_01JF8RRRN1A2B3C4D5E6F7G8H9I",
"organization_id": "org_01JF8RORG1A2B3C4D5E6F7G8H9I",
"report_id": "ceo.executive_summary",
"status": "ready",
"started_at": "2026-05-01T22:14:00Z",
"finished_at": "2026-05-01T22:14:08Z",
"row_count": 1
},
"result_preview": {
"columns": ["period", "ars_score", "liability_shielded_usd", "peer_percentile"],
"rows": [["2026-04", 0.86, 1284000, 89]],
"preview_truncated": false
},
"artifacts": [
{
"format": "json",
"url": "/v1/reports/runs/rrun_01JF8RRRN1A2B3C4D5E6F7G8H9I/artifacts/json",
"size_bytes": 1842
},
{
"format": "pdf_branded",
"url": "/v1/reports/runs/rrun_01JF8RRRN1A2B3C4D5E6F7G8H9I/artifacts/pdf_branded",
"size_bytes": 248792
}
]
}
result_preview.rows is truncated to 100 rows; fetch a format artifact for the full payload. The hard row cap on every report is 10,000.
Sign an artifact for download
POST /v1/reports/runs/{runId}/artifacts/{format}/sign returns a short-lived signed URL the caller can hand off without exposing platform credentials:
{ "ttl_seconds": 3600 }
{
"url": "https://api.trinitite.ai/v1/reports/artifacts/local/...?expires=1746148800&sig=...",
"expires_at": "2026-05-01T23:14:00Z"
}
Verifiable by re-computing the HMAC over (rel_path, expires) with the platform's signing key — the algorithm is documented in your enterprise contract.
Saved reports (query builder)
A saved report is a tenant-authored ReportDefinition derived from a catalogue base.
POST /v1/reports/saved
{
"base_report_id": "ops.hourly_activity",
"title": "Acme Ops — APAC Region Hourly",
"description": "Hourly activity filtered to APAC.",
"default_filters": [
{ "field": "region", "op": "eq", "value": "apac" }
],
"tags": ["acme", "apac", "ops"]
}
The platform validates that all referenced fields exist on the base report's source before persisting.
PUT /v1/reports/saved/{savedReportId}
Replace the saved definition. Returns the updated record.
DELETE /v1/reports/saved/{savedReportId}
204 No Content. Schedules pointing at this saved report stop firing on the next tick (and are flagged with disabled_reason: "saved_report_deleted" until you remove or re-target them).
Schedules
Cron-driven recurring delivery of a report over one or more channels.
POST /v1/reports/schedules
{
"saved_report_id": "rsav_01JF8RRSV1A2B3C4D5E6F7G8H9I",
"cron": "0 8 * * MON",
"timezone": "America/Los_Angeles",
"formats": ["pdf_branded"],
"channels": [
{ "type": "email", "to": ["board@acme.example.com"], "subject": "Acme weekly executive summary" },
{ "type": "s3", "bucket": "acme-reports", "prefix": "trinitite/weekly/", "region": "us-west-2" }
],
"enabled": true
}
| Field | Type | Description |
|---|---|---|
report_id / saved_report_id | string | One-of |
cron | string | Standard 5-field cron expression |
timezone | IANA TZ | Timezone for cron evaluation |
formats | string[] | Same enum as runs |
channels | array | One or more of email, webhook, sftp, s3 |
enabled | boolean | Soft-disable without deleting |
Channel shapes
// email
{ "type": "email", "to": ["..."], "cc": [], "bcc": [], "subject": "..." }
// webhook
{ "type": "webhook", "url": "https://...", "headers": { "X-API-Key": "..." }, "secret": "..." }
// Webhooks are signed with X-Trinitite-Signature (HMAC-SHA256 over the body using `secret`)
// sftp
{ "type": "sftp", "host": "...", "port": 22, "username": "...", "remote_path": "/incoming/", "auth": { "type": "password" | "key", "value": "..." } }
// s3
{ "type": "s3", "bucket": "...", "prefix": "...", "region": "us-west-2", "kms_key_id": "..." }
List / get / update / delete
GET /v1/reports/schedules (cursor pagination), GET /v1/reports/schedules/{id}, PUT /v1/reports/schedules/{id} (replace), DELETE /v1/reports/schedules/{id} (204 No Content).
Webhook signature verification
Webhook deliveries include the header X-Trinitite-Signature: t=<unix>,v1=<hmac_sha256_hex>. To verify:
import hmac, hashlib
signature_header = request.headers["X-Trinitite-Signature"]
parts = dict(p.split("=") for p in signature_header.split(","))
ts, sig = parts["t"], parts["v1"]
body = request.body # raw bytes
expected = hmac.new(
secret.encode("utf-8"),
f"{ts}.".encode("utf-8") + body,
hashlib.sha256,
).hexdigest()
assert hmac.compare_digest(expected, sig)
Reject any signature where t is more than 5 minutes off from your clock.
Errors
| HTTP | error.code | Cause |
|---|---|---|
400 | validation_error | Body or query failed schema validation |
400 | bad_request | Definition references unknown dimension / metric |
401 | unauthenticated | Missing or invalid credential |
403 | forbidden | Caller lacks the required analytics:* permission |
403 | entitlement_denied | Caller's organisation lacks the required entitlement |
404 | not_found | Catalogue report, saved report, run, artifact, or schedule not found |
409 | conflict | Cron expression collides with an existing schedule on the same definition |
410 | resource_gone | Run has expired (artifacts garbage-collected) |
422 | unprocessable_entity | Row cap (10,000) would be exceeded |
429 | rate_limited | Per-organization rate limit exceeded |
Best practices
- Use saved reports for stable views. A saved report freezes the parameters; a catalogue run with
paramsis good for one-offs. - Schedule signed downloads, not raw PDFs. Webhook + signed URL avoids large binary payloads in your SIEM.
- Cap the row count. Most BI dashboards do not need 10,000 rows. Lower
limitin your saved report to keep PDFs sane. - Branded PDFs aren't free. They re-render every run; prefer plain PDF or XLSX where the design isn't needed.
Next steps
- Operational telemetry → Analytics API
- Audit-grade artifacts → Attestation & Compliance
- Per-record forensic detail → Logs API