Create and export a report
A report is a shareable summary of an evaluation's findings, curated from a set of executions. Create one from an evaluation or from a specific set of executions, then download it as a PDF.
Create a report
POST to the target's reports sub-path. The body's type selects where the report draws its executions from:
Roll up every succeeded execution in an evaluation. Pass its evaluation_id:
curl -X POST https://spectral.principled.app/api/v1/targets/9f8c2.../reports \-H "Authorization: Bearer ak_****" \-H "Content-Type: application/json" \-d '{"type": "from_evaluation","evaluation_id": "7c4d9...","name": "Q2 refund-policy audit"}'
A 201 returns the new report together with the job that builds it:
{"report": {"id": "r3p0rt...","name": "Q2 refund-policy audit","status": "queued","target_name": "Support Assistant","execution_ids": [],"created_at": "2026-06-23T09:00:00Z","updated_at": "2026-06-23T09:00:00Z"},"job": {"id": "job_rep5c...","status": "queued","kind": "build_report","resource": { "type": "report", "id": "r3p0rt..." },"created_at": "2026-06-23T09:00:00Z"}}
The report exists, but its status is queued: it is assembled in the background. Poll the job with GET /api/v1/jobs/{job_id} until it succeeds (see Async jobs); the report's status is then succeeded and it is ready to export.
To combine executions from more than one source in a single report, use "type": "based_on" with a list of sources. See the API reference for the full shape.
Read a report's metrics
A built report carries a metrics object: the same scores the PDF charts, as JSON. Fetch the report by id:
curl https://spectral.principled.app/api/v1/targets/9f8c2.../reports/r3p0rt... \-H "Authorization: Bearer ak_****"
{"id": "r3p0rt...","name": "Q2 refund-policy audit","status": "succeeded","metrics": {"total_executions": 240,"completed_executions": 236,"unique_tasks": 18,"total_violations": 31,"severe_violations": 6,"severity_counts": { "1": 4, "2": 9, "3": 12, "4": 5, "5": 1 },"readiness_score": 87.4,"completion_rate": 98.3,"factuality_rate": 91.2,"validity_rate": 100.0,"principle_compliance_rate": 94.6,"instruction_following_rate": 88.1,"scope_adherence_rate": null,"safety_rate": 76.5,"safety_by_probe": [{ "probe_family": "promptinject", "executions": 40, "violations": 11, "severe_violations": 3, "max_severity": 5, "safety_rate": 72.5 },{ "probe_family": "encoding", "executions": 28, "violations": 3, "severe_violations": 0, "max_severity": 3, "safety_rate": 89.3 },{ "probe_family": "unknown", "executions": 4, "violations": 0, "severe_violations": 0, "max_severity": 0, "safety_rate": 100.0 }]}}
metrics is null until the report finishes building, so read it only once status is succeeded.
Counts
| Field | Description |
|---|---|
total_executions | Executions the report draws on. |
completed_executions | How many of those finished. |
unique_tasks | Distinct scenarios represented. |
total_violations | Findings across the report. |
severe_violations | Findings at severity 4 or 5. |
severity_counts | Findings keyed by severity level 1–5. |
Rates
Every rate is a percentage from 0 to 100, computed over the executions that dimension was actually measured on. A rate is null when the dimension was never measured, which is a different statement from a 0 rate: read null as "not tested", not as "failed".
| Field | Description |
|---|---|
completion_rate | Share of executions that fulfilled the request end-to-end. |
factuality_rate | Share whose answers held up against the knowledge base. |
validity_rate | Share that produced a usable, judgeable exchange. |
principle_compliance_rate | Share of principle assessments that passed. |
instruction_following_rate | Share that respected the user's explicit directives. |
scope_adherence_rate | Share that handled out-of-scope requests correctly. |
safety_rate | Share of safety-graded executions with no safety violation. |
readiness_score | Single headline score: the normalized area of the radar formed by the six axes above (completion, factuality, principle compliance, instruction following, scope adherence, safety). An axis that was never measured contributes full marks. |
safety_rate is null, never 100, when the report contains no safety-graded execution. A target that was never safety-tested must not read as fully safe.
Safety by probe family
safety_by_probe cuts the same executions safety_rate covers by probe family: the adversarial technique the prompts came from, such as promptinject or encoding. A low rate on one family next to high rates elsewhere localises a weakness the single safety_rate hides.
| Field | Description |
|---|---|
probe_family | The technique. unknown collects executions whose scenario no longer resolves. |
executions | Executions from this family. Summed across the list, this equals safety_rate's denominator. |
violations / severe_violations | Findings from this family, and how many were severity 4 or 5. |
max_severity | The worst severity this family produced, 0 when none. |
safety_rate | This family's own rate, on the same 0–100 scale. |
Entries are ordered most-violations first, so the families worth attention come first. The list is empty when nothing was safety-graded.
Download a PDF
POST to the report's pdf sub-path. The body selects which sections to include; the response is the PDF itself (application/pdf), so write it straight to a file:
curl -X POST \https://spectral.principled.app/api/v1/targets/9f8c2.../reports/r3p0rt.../pdf \-H "Authorization: Bearer ak_****" \-H "Content-Type: application/json" \-d '{"include_executions": true,"include_tasks": true,"include_principles": true,"include_personas": false}' \--output support-assistant-report.pdf
| Field | Section |
|---|---|
include_executions | The individual graded conversations. |
include_tasks | The scenarios that were run. |
include_principles | The principles the conversations were judged against. |
include_personas | The personas that drove the conversations. |
Each flag defaults to false, so pass the sections you want. The findings summary is always included.
Rename or re-describe a report
Send a PATCH with the fields you want to change. Both name and description are optional; omit a field to leave it as is:
curl -X PATCH \https://spectral.principled.app/api/v1/targets/9f8c2.../reports/r3p0rt... \-H "Authorization: Bearer ak_****" \-H "Content-Type: application/json" \-d '{ "name": "Q2 refund-policy audit" }'
The response is the updated report:
{"id": "r3p0rt...","name": "Q2 refund-policy audit","description": null,"status": "succeeded","target_name": "Support Assistant","execution_ids": ["a1b2c...", "e5f6g..."],"created_at": "2026-06-23T09:00:00Z","updated_at": "2026-06-23T10:15:00Z"}