Evidence bundle
GET /api/v1/evidence/export?from=&to= returns a zip that a workspace can hand to an auditor, a funder or a regulator as evidence of what Mandatum did for a period.
Status: draft, schema version 1. The field names and manifest layout are proposed by the engineering side and have not yet been agreed with the Lab. Treat any column as provisional until this notice is removed.
Request #
| Parameter | Required | Meaning |
|---|---|---|
from |
yes | ISO-8601 date or timestamp, inclusive. |
to |
yes | ISO-8601 date or timestamp, exclusive. Must be later than from. |
Authentication is a Bearer key carrying usage:read. The credential's owner must be a workspace owner or admin at the time of the request, the same gate as GET /api/v1/usage.
Response #
200 OK, Content-Type: application/zip, Content-Disposition: attachment; filename="mandatum-evidence-<workspace id>-<from>-<to>.zip", where <from> and <to> are UTC dates.
| Status | code |
When |
|---|---|---|
| 400 | bad_request |
from or to missing, not ISO-8601, or from not before to. |
| 400 | invalid_range |
The period spans more than 397 days. |
| 401 | (no code) |
Missing or invalid key. The body has error and request_id only. |
| 403 | tool_not_enabled_for_key |
The key lacks usage:read. |
| 403 | forbidden |
The credential's owner is not a workspace owner or admin. |
| 413 | EVIDENCE_TOO_LARGE |
The uncompressed payload or the zip exceeds 4 MB. Narrow the period and export again. |
| 500 | internal_error |
A read failed. The response does not say why; the request id in the body matches the server log. |
from and to are ISO-8601 calendar dates (2026-10-01) or date-times with a zone (2026-10-01T00:00:00Z). Other formats are refused.
Limits #
- Each data file holds at most 50,000 rows, the oldest first. If a file has more, the first 50,000 are kept and
manifest.files.<name>.truncatedistrue. Narrow the period to get the rest. - The period may span at most 397 days (
invalid_rangeotherwise). - The uncompressed payload is summed before compression; if it exceeds 4 MB the request is refused with 413 without zipping. The zipped bundle is checked against the same 4 MB limit. Because the pre-check is on uncompressed size, highly compressible data can be refused even when its zip would be small.
- Each row is written as compact JSON. A full citation row is roughly 250 bytes, so the 4 MB limit, not the 50,000-row cap, is what binds for most workspaces: a period with more than about 15,000 verified citations will be refused. Narrow the period.
Files #
manifest.json #
| Key | Type | Meaning |
|---|---|---|
schema_version |
integer | 1. Increases when a file or column changes incompatibly. |
workspace_id |
string (uuid) | The workspace the bundle covers. |
period |
object | from, to (ISO 8601, UTC) and to_inclusive: false. |
generated_at |
string | When the bundle was built (ISO 8601, UTC). |
pricing_version |
string | Version of the price table used for est_cost_usd. |
limits |
object | max_rows_per_file (50000) and max_bundle_bytes (4194304). |
files |
object | Per data file: rows written and whether it was truncated. |
notes |
object | Explanations that apply to this bundle. See below. |
fields |
object | Every column of every data file, with its meaning. The authoritative list. |
notes keys:
gap_reports:"table_missing"when the gap report issuance table does not yet exist in the deployment.gap_reports.jsonis then an empty array.audit:audit_loghas noorganization_idcolumn yet.audit.jsonselects rows bydetail.organizationId, which the audit writer sets on every external call.citations:usage_eventsstores metered units only. Per-citation verdicts are not in the bundle.
usage.csv #
RFC 4180, one row per UTC day, surface and unit. Columns:
day: UTC day (YYYY-MM-DD).surface:rest,mcporapp.unit:call,verified_citation,gap_reportortokens.count: units in the day for that surface and unit.est_cost_usd: estimated model cost for token rows; empty when none was recorded.input_tokens,output_tokens: model tokens for token rows; empty otherwise.
Cells that begin with =, +, -, @ or a tab are prefixed with an apostrophe so the file is safe to open in a spreadsheet.
gap_reports.json #
An array of gap report issuances, oldest first. Each object has:
id,organization_id,analysis_id: identifiers.content_sha256: SHA-256 of the issued report content, so a later copy can be checked against it.citation_count,benchmark_count: what the report contained.price_usd,is_free: the charge for the issuance.issued_by: the user who issued it, or null.issued_at: ISO 8601, UTC.
citations.json #
An array of verified_citation usage events, oldest first. Each object has id, organization_id, actor_kind (key or user), actor_id, surface, tool, unit (always verified_citation), count, latency_ms, request_id and created_at. request_id matches the audit rows and server logs for the same call.
audit.json #
An array of audit rows, ordered by id. Each object has id, timestamp, action, resource_type, resource_id, user_id, detail, hash and prev_hash. hash and prev_hash form the audit hash chain, so a reader can check that no row was altered or removed inside the period.
Open questions for the Lab #
- Is a per-citation verdict needed in the bundle? Verdicts are not persisted today; adding them means storing the verification result.
- Is
content_sha256enough to prove which report was issued, or is the report body itself required? - Is 50,000 rows per file and 4 MB per bundle the right ceiling, or should large periods be split into several bundles?
- Should the bundle be signed (for example a detached signature over the manifest), or is the hash chain sufficient?