Reports

HTTP API routes for reports.

Reports are markdown documents your organization writes together. {reportId} is the report's UUID from the list route. Bodies travel as bodyMarkdown in the report document format and are returned whole; there is no block tree.

RouteUseCLI equivalent
GET /reportsList reports without bodies.ax report list
POST /reportsCreate a report.ax report create
GET /reports/{reportId}One report and its body.ax report get
PATCH /reports/{reportId}Change title, description or body.ax report edit
POST /reports/{reportId}/blocksAppend markdown to the body.ax report append
GET /reports/blocks/kindsStructured block kinds a new block can use.ax report blocks kinds
GET /reports/blocks/schemaOne kind's block schema and example.ax report blocks schema
POST /reports/{reportId}/blocks/addAdd one structured block from JSON.ax report blocks add
GET /reports/{reportId}/blocksThe report's blocks with their IDs.ax report blocks list
POST /reports/trace-quote-sourceFind and read assistant messages for a Trace Quote.MCP: block_list_data_sources (Trace quote candidates for an identified report), report_trace_quote_source
POST /reports/{reportId}/archiveArchive a report. Nothing is deleted.ax report archive
POST /reports/{reportId}/shareGet the report's public link, creating it if needed.ax report share
DELETE /reports/{reportId}/shareRevoke the report's public link.ax report unshare
GET /experiments/{id}/version-selection-rate-evidence-sourcesWhat an A/B Comparison block can name.ax report version-selection-rate-evidence-sources
GET /experiments/{id}/selection-rate-sourcesReferences a selection-rate block can use.ax report selection-rate-sources

Every route takes org_id (required only when the key can see several orgs). Parameters are optional unless marked required. Report write bodies accept lowerCamelCase or snake_case field names. Trace Quote source requests use lowerCamelCase. A report row carries id, orgId, title, description, createdBy, updatedBy, createdAt, updatedAt and, once archived, archivedAt; empty fields are omitted. updatedAt is the conflict stamp the write routes take back.

GET /api/v1/reports

List reports, most recently changed first.

curl -H "Authorization: Bearer $AX_API_KEY" \
  "https://app.514.ax/api/v1/reports?limit=20"
ParameterWhat it does
limitReports per page (default 50, max 200; page_size is an alias).
cursorOpaque nextCursor from the previous page (page_token is an alias).
include_archivedtrue also lists archived reports. Off by default, so a page holds active reports only; send the same value on every page of one listing.

The response is {reports, nextCursor}; nextCursor is omitted on the last page.

POST /api/v1/reports

Create a report.

curl -X POST \
  -H "Authorization: Bearer $AX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Checkout latency","description":"Where the p95 went","bodyMarkdown":"# Summary\n"}' \
  "https://app.514.ax/api/v1/reports"
Body fieldWhat it does
titleRequired. Surrounding whitespace is trimmed; blank is refused.
descriptionOne-paragraph summary shown under the title.
bodyMarkdownInitial body. Normalized in the document format before it is stored, so the response holds the canonical form of what was sent.

Returns {report, bodyMarkdown}. Actor and org come from authentication.

GET /api/v1/reports/{reportId}

One report and its body as markdown.

curl -H "Authorization: Bearer $AX_API_KEY" \
  "https://app.514.ax/api/v1/reports/$REPORT_ID"

Returns {report, bodyMarkdown, contextLinks}; an empty report omits bodyMarkdown, and a report with no linked Context objects omits contextLinks. Each link carries kind and id, plus name and description when they are not empty; an object that can no longer be read has no name. Keep report.updatedAt for PATCH. When the report has a public link, the response also carries it as publicUrl, except for read-only keys, which never receive it.

PATCH /api/v1/reports/{reportId}

Change the title, the description and/or the whole body.

curl -X PATCH \
  -H "Authorization: Bearer $AX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Checkout latency, week 2","expectedUpdatedAt":"2026-09-02T11:30:00.000Z"}' \
  "https://app.514.ax/api/v1/reports/$REPORT_ID"
Body fieldWhat it does
expectedUpdatedAtRequired. The updatedAt you last read.
titleNew title; blank is refused.
descriptionNew description; an empty string clears it.
bodyMarkdownReplacement for the whole body; an empty string clears it.

The route is presence-aware: a field you send changes, a field you leave out stays. Sending none of the three is a 400. When the report changed since expectedUpdatedAt, the route answers 409 with {error, message} and writes nothing; read the report again and retry with its new stamp. Returns {report, bodyMarkdown}.

POST /api/v1/reports/{reportId}/blocks

Append markdown to the end of the body, after a blank line, leaving what is there untouched.

curl -X POST \
  -H "Authorization: Bearer $AX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"markdown":"> [!FINDING] Slower on Fridays\n> The p95 doubles after 16:00 UTC."}' \
  "https://app.514.ax/api/v1/reports/$REPORT_ID/blocks"
Body fieldWhat it does
markdownRequired. Prose, headings, callouts, ax:// mentions or ax-block fences. Blank is refused.
expectedUpdatedAtRefuse the append with 409 when the report changed since this stamp. Omit to append to the body as it stands.

Returns {report, bodyMarkdown} with the whole body after the append.

GET /api/v1/reports/blocks/kinds

The structured block kinds a new block can use, from the report block registry, with retired kinds left out. Returns {kinds}, each with kind, label, description, whenToUse, and the sources and figures it reads.

curl -H "Authorization: Bearer $AX_API_KEY" \
  "https://app.514.ax/api/v1/reports/blocks/kinds"

GET /api/v1/reports/blocks/schema

One kind's block schema: the same payload as the MCP report_block_schema tool for operation add, with schema (JSON Schema), example, wrapper, runtimeConstraints, sources, guidance and a revision that changes with the contract. An unknown kind returns 404 with the closest kinds.

curl -H "Authorization: Bearer $AX_API_KEY" \
  "https://app.514.ax/api/v1/reports/blocks/schema?kind=finding"
ParameterWhat it does
kindRequired. A kind from the kinds route.

POST /api/v1/reports/{reportId}/blocks/add

Add one block, validated against its kind's schema, at the end or after a block. Returns {reportId, blockId, updatedAt, blocks}, where blockId is the new block's ID.

curl -X POST -H "Authorization: Bearer $AX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"block":{"kind":"ax-block","block":{"kind":"finding","id":"agents-skip-docs","title":"Agents skip the docs","conclusion":"Most runs called the API first."}}}' \
  "https://app.514.ax/api/v1/reports/<REPORT_ID>/blocks/add"
Body fieldWhat it does
blockRequired. The block, as the schema route describes it.
afterBlockIdInsert after this block ID from the list route. Omit to add at the end.
expectedUpdatedAtRefuse with 409 when the report changed since this stamp.

An invalid block returns 400: a known kind's error names each failing field, and an unknown kind's names the closest kinds.

GET /api/v1/reports/{reportId}/blocks

The saved body's blocks in document order: {reportId, updatedAt, blocks}, each block with position, blockId, type, kind for structured blocks, text (a preview) for text blocks, and parentBlockId inside columns.

curl -H "Authorization: Bearer $AX_API_KEY" \
  "https://app.514.ax/api/v1/reports/<REPORT_ID>/blocks"

POST /api/v1/reports/{reportId}/archive

Archive a report. The row, its body, its follows and the Context citation a saved product-import report carries all stay; archivedAt is set and the report leaves GET /reports unless include_archived=true. GET /reports/{reportId} still returns it. Archiving an archived report returns it unchanged. Nothing is deleted.

curl -X POST -H "Authorization: Bearer $AX_API_KEY" \
  "https://app.514.ax/api/v1/reports/$REPORT_ID/archive"

Returns {report} with archivedAt set.

POST /api/v1/reports/{reportId}/share

Return the report's public link, creating it if the report has none. Anyone with the URL can read the report and its data without signing in. A report has at most one public link, so calling this again returns the same URL.

curl -X POST -H "Authorization: Bearer $AX_API_KEY" \
  "https://app.514.ax/api/v1/reports/$REPORT_ID/share"

Returns {reportId, url, created, createdAt}. created is true when this call created the link. Read-only keys are refused.

DELETE /api/v1/reports/{reportId}/share

Revoke the report's public link. Anyone holding the old link loses access; sharing again creates a new one.

curl -X DELETE -H "Authorization: Bearer $AX_API_KEY" \
  "https://app.514.ax/api/v1/reports/$REPORT_ID/share"

Returns {reportId, revoked}. A report without a public link succeeds and omits revoked (false). Read-only keys are refused.

POST /api/v1/reports/trace-quote-source

List a run's assistant messages, or read one listed message's exact text and recorded identity.

curl -X POST \
  -H "Authorization: Bearer $AX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"experimentId":"<EXPERIMENT_ID>","runId":"<RUN_ID>"}' \
  "https://app.514.ax/api/v1/reports/trace-quote-source"
Body fieldWhat it does
experimentId, runIdRequired. The experiment and one of its runs in your organization.
entityIdCopy a listed source.message.entityId to read that message; omit to list.
afterSeq, afterEntityIdPass the listing's nextAfterSeq and nextAfterEntityId together for the next page.

A ready listing returns messages, each with a canonical source, an exact text prefix in preview, and previewTruncated. A ready message read returns its exact text, recorded identity, and block, the Trace Quote payload to write in an ax-block fence. Copy the whole source; never invent identifiers or identity fields. The returned block already carries the message's text as text, which you may edit. Missing, unavailable and empty sources return no quote text or block. Reports must be enabled for your organization.

GET /api/v1/experiments/{id}/version-selection-rate-evidence-sources

The ids an A/B Comparison (version-selection-rate-evidence) block can name for one experiment: its versions, its products with each registered product version's fingerprint, and its selection tests. No selection counts or rates.

curl -H "Authorization: Bearer $AX_API_KEY" \
  "https://app.514.ax/api/v1/experiments/$EXPERIMENT_ID/version-selection-rate-evidence-sources"
ParameterWhat it does
experiment_fingerprintRead products and tests at one of the experiment's versions. Omit to read them across every version.

Returns {experimentId, experimentFingerprint, experimentVersions, products, tests} with every field present; an empty experimentFingerprint means every version. An experiment without a registered version, or a fingerprint that is not one of its versions, returns 404.

Only test names with one definition in the chosen scope are listed. Set experiment_fingerprint to narrow the scope when a name has several definitions.

GET /api/v1/experiments/{id}/selection-rate-sources

List the references a selection-rate block is configured with for one experiment. {id} is the experiment ID returned by the experiment listing.

curl -H "Authorization: Bearer $AX_API_KEY" \
  "https://app.514.ax/api/v1/experiments/$EXPERIMENT_ID/selection-rate-sources?test_name=uses-cli"
ParameterWhat it does
experiment_fingerprintRead the tests at this version instead of the current one.
test_nameCheck a test name against that version.

Returns {experimentId, experimentFingerprint, versions, tests, scope, selectedTest}. experimentFingerprint is the version tests were read at. Each test has a name and a test-<uuid> key. scope lists the values each filter may hold (agents, agentModes, models, products, environments, promptIds). selectedTest is set only when exactly one test at that version has test_name. An experiment with no version in the org, or an unregistered fingerprint, returns 404. No rates are returned.

Selection blocks can persist partial choices with configuration: "unconfigured" and empty strings for missing experiment IDs or test names. Source changes clear stale test, version, scope and search choices in the same write. Incomplete blocks do not query measurements; completing the required choices removes the marker.