---
title: "Export a client report"
method: GET
path: "/api/agent/agency/reports/{id}/export"
tags: ["Agency"]
---

# Export a client report

`GET /api/agent/agency/reports/{id}/export`

## Request

- Base URL: `https://preuve.ai`
- URL: `https://preuve.ai/api/agent/agency/reports/{id}/export`
- Auth: API key in header `x-preuve-key`

Structured JSON for one COMPLETED client report. Starter and refunded reports keep their normal content restrictions, named under locked.

## Path parameters

- `id` string, uuid, required

## Query parameters

- `verbosity` 'full' | 'summary'

## Response `200`

The export payload.

- AgencyReportExport
  - `schemaVersion` integer
  - `exportType` 'agency-report'
  - `dataAsOf` string, date-time
  - `exportedAt` string, date-time
  - `report` object: The report payload, same item shape as the ideas-json export. On verbosity=summary its details.omitted names what was dropped.
  - `ownerExclusions` object[]: Competitor cards the workspace hid from the client view.
    - `name` string
    - `reasonCode` string, nullable
    - `hiddenAt` string, date-time, nullable
  - `locked` ExportLocks: Which fields this export left null, and why. Present only when the caller's tier withheld something, and absent entirely when the payload is complete. Read it before concluding a field is absent: a null under one of these paths is locked, not missing and not a failed analysis. It ships to every tier, paid included, and carries no price and no URL. On a batch export there is ONE object for the whole batch, the union of every exported item's withheld fields, deduplicated by path.
    - `reason` string: Why the fields were withheld. "starter_scan" today.
    - `unlockedBy` string: What lifts the lock. "founder_report" today.
    - `fields` object[]: One entry per withheld field.
      - `path` string: Dotted path into the export, e.g. "details.pivots". A path may name several keys of one object with a pipe, e.g. "details.risks[].explanation|mitigation".
      - `label` string: What that path holds, in plain language. Some labels also say the field needs an enrich call once the report is deep.
    - `note` string: One sentence restating that the nulls are a tier boundary rather than missing data.

## Other responses

- `400`: verbosity must be full or summary (INVALID_VERBOSITY).
- `401`: Missing x-preuve-key header (MISSING_API_KEY), or a malformed, unknown or revoked key (INVALID_AGENT_KEY - the same envelope for all three, on purpose). Repeated failures are throttled per IP and answer 429.
- `403`: The key is valid but does not carry the one Agency scope the route it called requires (INSUFFICIENT_SCOPE). The two are enforced per route and neither implies the other: POST /api/agent/agency/analyses requires agency:write, and every other Agency route - the workspace context, the report list, the report poll and the export - requires agency:read, so there is no separate export scope on this surface and a key carrying only the other Agency scope is refused here too. Note that every key created before 2026-09-09 hits this: these routes previously accepted the personal analysis:read / export:read scopes, and scopes are fixed at creation rather than backfilled, so an older key needs a new grant. Neither route to one is unconditional. Reconnecting the client works on any plan, free included, but /authorize only ever intersects the scopes the client requests, so a client replaying the scope list it cached at registration mints another Agency-less key and revokes the working one on the way - check the consent screen lists the Agency permissions before approving. Creating a key by hand cannot silently fail, but it needs "Include Agency workspace access" ticked AND an account that may create keys by hand: a paid personal plan, or membership of a Consultant or Agency workspace with an active subscription or a one-time unlock.
- `404`: Unknown report, or it belongs to another workspace (REPORT_NOT_FOUND / AGENCY_NOT_FOUND).
- `409`: Report is not complete yet (REPORT_NOT_READY).
- `429`: Rate limited (RATE_LIMITED or DAILY_LIMIT_REACHED). RATE_LIMITED carries a Retry-After header with the seconds until the window resets; the quota-shaped codes clear on a different clock and put resetAt in the body instead.
- `500`: Unexpected server error (INTERNAL_ERROR, or TRIGGER_DISPATCH_FAILED when a deep scan could not be handed to the job runner). Never tell the user their quota is safe: on a deep scan the spend can already have happened when the failure hit, and an ambiguous dispatch deliberately keeps the report and its funding because the job may have been accepted before its response was lost. Treat consumption as unknown and retry with backoff. On an operation that takes a clientRunId, retry with the SAME one - that reconciles the original run, whatever state it reached, instead of starting and charging for a second one. On every other operation there is nothing to reconcile: the read or export simply failed, so a plain backed-off retry is the whole recovery.
- `503`: A dependency was unavailable or deliberately paused (SERVICE_DISABLED, LLM_PAUSED, PROFILE_LOOKUP_FAILED, and the fail-closed answer when the rate-limit backend is down). Any authenticated route can answer this one: authentication meters itself through the same fail-closed limiter, so an outage there refuses the request before the handler runs. Retryable, and no paid quota, tokens, or scans were consumed - a pause that lands mid-run refunds the claim it had already made. Back off and try again rather than falling back to a different endpoint.

---

[API](https://skmtc.dev/preuve/apis/preuve-agent-api.md) · [All operations](https://skmtc.dev/preuve/apis/preuve-agent-api/llms.txt) · [OpenAPI document](https://skmtc.dev/preuve/apis/preuve-agent-api/revisions/b8e99c2a4fc7?raw)
