---
title: "Export a batch (ideas-json)"
method: GET
path: "/api/agent/analysis-batches/{batchId}/export"
tags: ["Batches"]
---

# Export a batch (ideas-json)

`GET /api/agent/analysis-batches/{batchId}/export`

## Request

- Base URL: `https://preuve.ai`
- URL: `https://preuve.ai/api/agent/analysis-batches/{batchId}/export`
- Auth: API key in header `x-preuve-key`

Structured export for a batch. Includes only completed, export-ready items; every skipped item is listed in omittedItems with a reason. A single exportedAt is pinned per export so the same batch state always serializes identically.

## Path parameters

- `batchId` string, uuid, required

## Query parameters

- `format` 'ideas-json'
- `verbosity` 'full' | 'summary'

## Response `200`

The batch export payload.

- BatchExport: Note the asymmetry with the other two exports: this one carries no exportType. The single-analysis and agency-report exports each stamp one, this payload identifies itself by batchId instead, and the schema follows the wire rather than inventing a discriminator that is never sent.
  - `schemaVersion` 2
  - `batchId` string, uuid: The batch this export was built from.
  - `exportedAt` string, date-time
  - `verbosity` 'full' | 'summary': The verbosity every idea in this payload was built at, echoed back. On "summary", each idea's details.omitted names what was dropped.
  - `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.
  - `upsell` ExportUpsell: Purchase context for a locked export or buyable quota refusal. Export upsells are free-owner only; a paid non-regional caller may receive a scan_pack top-up on INSUFFICIENT_TOKENS. Ignore it if you only need the data contract.
    - `offer` 'founder_report' | 'scan_pack': "founder_report" is the $59 one-time tier. "scan_pack" is a top-up for an account that already bought and has no deep credits left.
    - `price` string: Display price text, e.g. "$59 one-time".
    - `guarantee` string: Guarantee text for offer=founder_report. Omitted for scan_pack top-ups; an empty string is never used.
    - `url` string, uri: Purchase or report URL, with attribution query parameters.
    - `locked` string[]: Section names the offer covers. Distinct from the top-level locked object, which names response FIELDS.
    - `note` string: The same facts as one sentence, for a client that relays text.
  - `counts` object: The batch's own item tally (total, plus one key per item status such as COMPLETED or FAILED) with the export's two counts merged on top. Read the named keys; do not assume the set is closed.
    - `total` integer: Items in the batch.
    - `exported` integer: Items whose report made it into ideas.
    - `omitted` integer: Items listed in omittedItems instead.
  - `ideas` object[]
  - `omittedItems` object[]: Every incomplete item with the reason it was skipped.
    - `id` string, nullable: Null when the item is a late-collision stub that never became a run.
    - `clientRunId` string
    - `reportId` string, nullable
    - `status` string
    - `reason` string

## Other responses

- `400`: The request was rejected before any work started: malformed JSON (INVALID_JSON), an id that is not one of ours (INVALID_ANALYSIS_ID, INVALID_BATCH_ID), an export format we do not serve (UNSUPPORTED_FORMAT), or a field the schema cannot express - an idea too short to analyse, a batch outside 1..5 items (INVALID_BATCH_SIZE), two batch items sharing a clientRunId (DUPLICATE_CLIENT_RUN_ID), an unknown scanType. No paid quota, tokens, or scans were consumed. On the CREATE routes a rate-limit window can still be: those per-account limiters are claimed ahead of schema validation on purpose, so that malformed spam is metered, and retrying unchanged spends another slot. Enrich no longer works that way - since 2026-09-15 its 12/min window is claimed after the body is validated, so a malformed enrich costs nothing. Fix the request either way; retrying it unchanged returns the same answer.
- `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 was not granted the scope this route requires (INSUFFICIENT_SCOPE). Scopes are per route and fixed when the key is created: analysis:write to start a run, analysis:read to poll it, export:read to export, batch:write and batch:read for batches. They are never widened afterwards, so this is not retryable - the user reconnects the client and approves the missing permission, or creates a new key with it. Distinct from 401, which means the key itself was not accepted.
- `404`: Unknown id, or the resource belongs to another account. The code names what was looked up: ANALYSIS_NOT_FOUND on the analysis routes, BATCH_NOT_FOUND on the batch ones. The analysis routes also answer REPORT_DELETED when the run exists but its report was deleted from the dashboard: nothing is left to poll, enrich or export.
- `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/d58b3d396fa7?raw)
