> ## Documentation Index
> Fetch the complete documentation index at: https://docs.preuve.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Start a batch

> Create up to 5 analysis runs in one batch. Idempotent on the batch clientRunId. Each item needs its own unique clientRunId. Item scanType is optional and falls back to the batch's, so set it per item only to mix starter and deep in one batch.




## OpenAPI

````yaml /api-reference/openapi.yaml post /api/agent/analysis-batches
openapi: 3.1.0
info:
  title: Preuve Agent API
  version: '2.0'
  summary: Startup idea validation for AI agents and scripts.
  description: >
    API for programmatic startup idea validation: a 0-100 viability score,
    competitors, market sizing and source-linked evidence from 60+ live sources.
    Every request carries a single x-preuve-key header holding the whole API key
    - no signing, no bearer token. See the Authentication guide.


    Every response is JSON, errors included: an unknown route answers 404 with
    the same { error, code } envelope as every other failure, so branch on code
    and never parse a message. Error codes are stable and retired ones stay
    listed on the Errors page. scanType "starter" costs nothing and returns a
    real report, which makes it the sandbox: an agent can exercise the whole
    create - poll - enrich - export loop without spending a cent. GET /api/agent
    is the unauthenticated discovery document.
  termsOfService: https://preuve.ai/terms
  contact:
    name: Preuve AI developer support
    url: https://preuve.ai/support
    email: hello@preuve.ai
servers:
  - url: https://preuve.ai
security:
  - preuveKey: []
tags:
  - name: Discovery
    description: Unauthenticated documents that describe the API itself.
  - name: Analyses
    description: One idea at a time - create, poll, enrich, export.
  - name: Batches
    description: Up to five analyses in one idempotent batch.
  - name: Tools
    description: Utilities that spend no scan quota.
  - name: Agency
    description: >-
      Client work on a Consultant or Agency workspace (agency:read /
      agency:write).
externalDocs:
  description: Developer docs - quickstart, authentication, quotas, error codes
  url: https://docs.preuve.ai/
paths:
  /api/agent/analysis-batches:
    post:
      tags:
        - Batches
      summary: Start a batch
      description: >
        Create up to 5 analysis runs in one batch. Idempotent on the batch
        clientRunId. Each item needs its own unique clientRunId. Item scanType
        is optional and falls back to the batch's, so set it per item only to
        mix starter and deep in one batch.
      operationId: createBatch
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateBatchRequest'
      responses:
        '200':
          description: >
            Existing batch replayed via clientRunId. Nothing new was created and
            nothing new was consumed; the body is the batch's current state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Batch'
        '202':
          description: >
            Batch accepted. Every item has been attempted, so a 202 can still
            carry items that failed at start - read status and counts, and each
            item's own status, rather than assuming five runs began.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Batch'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '409':
          $ref: '#/components/responses/ClientRunIdConflict'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
components:
  schemas:
    CreateBatchRequest:
      type: object
      required:
        - clientRunId
        - items
      anyOf:
        - title: scanType set for the whole batch
          required:
            - scanType
        - title: scanType set on every item
          properties:
            items:
              items:
                required:
                  - scanType
      properties:
        clientRunId:
          type: string
          description: Idempotency id for the whole batch.
        name:
          type: string
        scanType:
          type: string
          enum:
            - starter
            - deep
          description: Default scanType applied to every item; each item may override.
        enrichmentMode:
          type: string
          enum:
            - core
            - none
        items:
          type: array
          minItems: 1
          maxItems: 5
          items:
            $ref: '#/components/schemas/BatchAnalysisItem'
    Batch:
      type: object
      properties:
        id:
          type: string
          format: uuid
        clientRunId:
          type: string
        name:
          type:
            - string
            - 'null'
        status:
          type: string
        counts:
          type: object
          additionalProperties:
            type: integer
        items:
          type: array
          items:
            $ref: '#/components/schemas/BatchItem'
    BatchAnalysisItem:
      description: >
        One item in a batch. Same fields as CreateAnalysisRequest, except that
        scanType is optional: an item without one inherits the batch's scanType,
        and only a batch that sets neither is rejected. clientRunId is still
        required per item - the derived fallback the batch uses for its own
        reservation bookkeeping does not reach the run.
      allOf:
        - $ref: '#/components/schemas/AnalysisRequestFields'
      required:
        - clientRunId
        - idea
    BatchItem:
      description: >
        One entry in a batch. Either a real run or a late-collision stub; the
        two are told apart by id, which is null only on the stub.
      oneOf:
        - $ref: '#/components/schemas/AnalysisRun'
        - $ref: '#/components/schemas/BatchItemConflict'
    ApiError:
      type: object
      required:
        - error
        - code
      description: >
        Every error, on every route, unknown paths included. Some codes add
        top-level context fields (limit, tokenBalance, resetAt, upsell, ...); a
        404 NOT_FOUND on an unknown route adds details.discovery,
        details.openapi and details.docs so the caller can recover.
      properties:
        error:
          type: string
          description: Human-readable message. Prose; it changes.
        code:
          type: string
          description: Stable machine-readable code. Branch on this.
        details:
          description: >
            Optional context. An object on most codes (limits, recovery links,
            the method and path of a 404); an array of strings where the route
            forwards a validator's messages, as enrich does on
            INVALID_FOUNDER_PROFILE. Read it, never branch on it - `code` is the
            contract.
          oneOf:
            - type: object
              additionalProperties: true
            - type: array
              items:
                type: string
        limit:
          type: integer
          description: >
            The ceiling that was hit, in the unit the code implies: bytes on
            PAYLOAD_TOO_LARGE, runs in flight on CONCURRENT_LIMIT_REACHED,
            requests in the window on a rate limit.
        remaining:
          type: integer
          description: Requests left in the current window.
        resetAt:
          type: string
          format: date-time
          description: >
            When the window that refused this request reopens. Present on
            DAILY_LIMIT_REACHED from run creation (the idea generator's rolling
            ceiling sends only limit); prefer it over the Retry-After header
            when both are present, since it is absolute.
        tokenBalance:
          type: integer
          description: >-
            Deep-scan credits left on the account. Present on
            INSUFFICIENT_TOKENS.
        scansUsed:
          type: integer
        scansRemaining:
          type: integer
        limitType:
          type: string
          description: Which ceiling refused an AppSumo-backed account.
        upgradeHint:
          type: string
          description: What to change to get past this refusal. Prose, not a contract.
        paused:
          type: boolean
          description: >
            Present only on the LLM-pause 503, where the body is exactly {
            error, code, paused }.
        upsell:
          $ref: '#/components/schemas/ExportUpsell'
    AnalysisRequestFields:
      type: object
      description: >
        Every field an analysis request can carry. Nothing is required here -
        CreateAnalysisRequest and BatchAnalysisItem each add their own required
        list, because a batch item may inherit scanType from the batch while a
        standalone create must state it.
      properties:
        clientRunId:
          type: string
          description: Caller-chosen idempotency id. Reusing it replays the existing run.
        scanType:
          type: string
          enum:
            - starter
            - deep
          description: >-
            "starter" costs nothing; "deep" runs the full paid analysis and
            consumes account quota. "free" is accepted as a legacy alias of
            "starter".
        idea:
          type: string
          description: The startup idea, in plain language.
        targetMarket:
          type: string
        targetCountry:
          type: string
          description: ISO country code, e.g. "US" or "FR".
        title:
          type: string
        stage:
          type: string
          description: >-
            Where the founder is today. Canonical values: idea, validation, mvp,
            launched, growth, scaling (free text tolerated). Optional here, but
            the MCP tools require it for deep scans; it feeds the feasibility
            analysis.
        budget:
          type: string
          description: >-
            Budget available to pursue the idea. Canonical values: bootstrap (no
            outside money), 10k, 50k, 100k, 100kPlus (over $100k), 1mPlus (over
            $1M); free text tolerated. Optional here, but the MCP tools require
            it for deep scans; it feeds the feasibility analysis.
        language:
          type: string
          default: en
        enrichmentMode:
          type: string
          enum:
            - core
            - none
          default: core
          description: >-
            "core" also generates the export-required sections; "none" returns
            the raw completion only.
        publish:
          type: boolean
          description: When true, creates a public share URL for the report.
    AnalysisRun:
      type: object
      properties:
        id:
          type: string
          format: uuid
        reportId:
          type:
            - string
            - 'null'
          format: uuid
          description: >
            Null until the report row is attached, which is why a run that
            failed before dispatch is retryable under the same clientRunId.
        clientRunId:
          type:
            - string
            - 'null'
          description: >
            The caller-chosen id from create. Null when the analysis is a
            dashboard-created report resolved by its report id: no run row
            exists, so there is no clientRunId to replay.
        batchId:
          type:
            - string
            - 'null'
          format: uuid
          description: The batch this run belongs to, or null for a standalone run.
        scanType:
          type: string
          enum:
            - starter
            - deep
          description: Your request type. Reliable from creation, even while PROCESSING.
        enrichmentMode:
          type: string
          enum:
            - core
            - none
        publish:
          type: boolean
        reportType:
          type:
            - string
            - 'null'
          enum:
            - quick
            - deep_dive
            - null
        analysisTier:
          type:
            - string
            - 'null'
          enum:
            - basic
            - advanced
            - null
          description: >-
            Derived from the report. Deep runs read "basic" until deep sections
            land.
        status:
          type: string
          enum:
            - PENDING
            - PROCESSING
            - COMPLETED
            - FAILED
        progressStep:
          type:
            - string
            - 'null'
          description: Free-text pipeline stage while PROCESSING.
        errorCode:
          type:
            - string
            - 'null'
          description: >
            Set on a FAILED run. Two internal markers are deliberately rewritten
            before they leave: an ambiguous dispatch reports
            TRIGGER_DISPATCH_FAILED and an ambiguous claim refund reports
            REPORT_CREATE_FAILED.
        errorMessage:
          type:
            - string
            - 'null'
        resetAt:
          type: string
          format: date-time
          description: >
            Present only on a batch item refused by the rolling daily cap;
            absent, not null, on every other run.
        pollAfterSeconds:
          type: integer
          description: >
            Present only while polling is still useful. Pace by this rather than
            by a fixed interval.
        pollHint:
          type: string
          description: Present alongside pollAfterSeconds.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        readyForExport:
          type: boolean
          description: >
            Whether GET /export will serve this analysis now. Always false for a
            report created on the preuve.ai dashboard, whatever state the report
            is in, because that export answers 403 REPORT_NOT_FROM_AGENT_RUN.
            Branch on exportable first.
        exportable:
          type: boolean
          description: >
            Present, and false, only on a report created on the preuve.ai
            dashboard. Absent means the ordinary contract applies and
            readyForExport is the gate. When it is false, no amount of polling
            or enriching changes it.
        exportBlockedCode:
          type: string
          enum:
            - REPORT_NOT_FROM_AGENT_RUN
          description: Present alongside exportable, naming the refusal.
        exportBlockedHint:
          type: string
          description: Present alongside exportable. Relayable prose.
        enrichment:
          $ref: '#/components/schemas/EnrichmentState'
        modules:
          type:
            - object
            - 'null'
          description: Per-module statuses. Deep reports only; null on free runs.
          properties:
            proofOfDemand:
              $ref: '#/components/schemas/ModuleStatus'
            founderFit:
              $ref: '#/components/schemas/ModuleStatus'
            playbook:
              $ref: '#/components/schemas/ModuleStatus'
            trends:
              $ref: '#/components/schemas/ModuleStatus'
            community:
              $ref: '#/components/schemas/ModuleStatus'
        reportUrl:
          type:
            - string
            - 'null'
          format: uri
          description: Null until the report row exists.
        shareUrl:
          type:
            - string
            - 'null'
          format: uri
    BatchItemConflict:
      type: object
      description: >
        Not a run. Another request claimed this item's clientRunId after the
        batch row was reserved but before the item started, so nothing was
        created and id is null. Appears in Batch.items on create, on replay and
        on poll, and its null id carries through to BatchExport.omittedItems.
      properties:
        id:
          type: 'null'
        reportId:
          type: 'null'
        clientRunId:
          type: string
        batchId:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - FAILED
        errorCode:
          type: string
          enum:
            - CLIENT_RUN_ID_CONFLICT
        errorMessage:
          type: string
    ExportUpsell:
      type: object
      description: >
        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.
      properties:
        offer:
          type: string
          enum:
            - founder_report
            - scan_pack
          description: >
            "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:
          type: string
          description: Display price text, e.g. "$59 one-time".
        guarantee:
          type: string
          description: >
            Guarantee text for offer=founder_report. Omitted for scan_pack
            top-ups; an empty string is never used.
        url:
          type: string
          format: uri
          description: Purchase or report URL, with attribution query parameters.
        locked:
          type: array
          description: >
            Section names the offer covers. Distinct from the top-level locked
            object, which names response FIELDS.
          items:
            type: string
        note:
          type: string
          description: The same facts as one sentence, for a client that relays text.
    EnrichmentState:
      type: object
      description: >
        Progress of the core export sections. The four name arrays partition
        `required`, so a poller decides what to do next by reading them rather
        than by parsing `status`: anything in processing means keep polling,
        anything in failed means call enrich again, and missing with nothing
        processing means enrichment was never started for it.
      properties:
        mode:
          type: string
          enum:
            - core
            - none
        status:
          type: string
          enum:
            - not_required
            - pending
            - processing
            - partial
            - completed
            - failed
          description: >
            Rolled up from the arrays below. "partial" means some sections
            landed and others did not; "not_required" is mode "none", or a
            report with no required sections.
        required:
          type: array
          description: >-
            Every section this report and mode need. The other four arrays
            partition it.
          items:
            type: string
        completed:
          type: array
          items:
            type: string
        missing:
          type: array
          description: Required, not generated, and not currently being generated.
          items:
            type: string
        processing:
          type: array
          description: >-
            Generation is in flight. Keep polling rather than calling enrich
            again.
          items:
            type: string
        failed:
          type: array
          description: >-
            Generation was attempted and failed. Retryable by calling enrich
            again.
          items:
            type: string
        sections:
          type: object
          description: >
            Per-section detail, keyed by section name, for every entry in
            required. Each value carries at least a status matching the array
            the section is in, plus completedAt on a completed section and
            whatever the last attempt recorded on a failed one.
          additionalProperties:
            type: object
            properties:
              status:
                type: string
                enum:
                  - missing
                  - processing
                  - completed
                  - failed
                  - pending
              completedAt:
                type:
                  - string
                  - 'null'
                format: date-time
            additionalProperties: true
    ModuleStatus:
      type: object
      properties:
        status:
          type: string
          enum:
            - not_generated
            - generating
            - failed
            - completed
  responses:
    BadRequest:
      description: >
        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.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    Unauthorized:
      description: >
        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.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    InsufficientScope:
      description: >
        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.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    ClientRunIdConflict:
      description: >
        One of the batch's item clientRunIds is already owned by an existing run
        (CLIENT_RUN_ID_CONFLICT), so the batch was not created. Not retryable
        unchanged: read the run that owns the id, or give that item a new one. A
        per-item failure inside an accepted batch is not this - it comes back as
        a FAILED item in the 200 body.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    PayloadTooLarge:
      description: >
        The request body is over the 1 MB limit (PAYLOAD_TOO_LARGE, with the
        ceiling in `limit`). Counted while the body streams in and refused the
        moment it is exceeded, before any quota, token, or scan is consumed. Not
        retryable unchanged: send less, or split the work across requests.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    RateLimited:
      description: >
        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.
      headers:
        Retry-After:
          description: Seconds until the per-minute window resets (RATE_LIMITED only).
          schema:
            type: integer
            minimum: 1
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    InternalError:
      description: >
        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.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
    ServiceUnavailable:
      description: >
        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.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
  securitySchemes:
    preuveKey:
      type: apiKey
      in: header
      name: x-preuve-key
      description: >
        Your API key (prv_...), sent as-is. It is the only credential the API
        needs - see the Authentication guide.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.