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

# Add library documents to a batch

> Add existing library documents to a batch.

Attaches existing library documents and enqueues extraction at the **batch** page rate. Destination must not be canceled or a schema-test batch.


## OpenAPI

````yaml POST /api/v2/batches/{batch_id}/documents/
openapi: 3.1.0
info:
  title: Abacus Docs API
  version: 1.0.0
  description: |
    REST + SSE surface for Abacus Docs on the same host as the web app
    (`https://abacusdocs.com`). Paths are `/api/v2/…`.

    Authenticate with an organisation API key (`sk-abacus-*`) as a Bearer
    token, or an Auth0 access token. Cross-tenant reads return **404**, not
    403. Page-based lists use `?page=` and `?page_size=`.

    Extraction, reprocess, schema tests, and chat turns spend credits.
    See the help centre Credits pages for rates.
  contact:
    name: Abacus Docs
    url: https://abacusdocs.com/contact
servers:
  - url: https://abacusdocs.com
    description: Production (Extract)
security: []
paths:
  /api/v2/batches/{batch_id}/documents/:
    post:
      tags:
        - v2/batches
      summary: Add existing library documents to a batch
      description: >-
        Appends ``ChatAttachment`` rows from the user's document library to an
        existing batch. Each added doc receives a pinned ``StructVersion``
        (request-level override > per-doc override > batch default).


        Already-in-batch and unowned doc ids are skipped (returned in the
        ``skipped`` array) — the request as a whole still returns 201 with
        whatever was added.


        Side effects:

        - Creates one ``BatchDocument`` per added doc, pinned to   the resolved
        version.

        - Bumps ``Batch.total_files`` by the number of added rows so  
        ``progress_percent`` stays accurate.

        - Flips a COMPLETED/FAILED batch back to PROCESSING so the   Celery
        worker reprocesses on the next ``process_batch``   invocation.
        Already-COMPLETED docs are not re-extracted   (the worker only picks up
        PENDING rows).

        - Enqueues ``process_batch`` so the new PENDING docs get   extracted
        promptly.


        Validation:

        - Batch must already have a ``default_struct_version``   (no-schema
        batches return 400 — there's no parent struct   to resolve overrides
        against).

        - Every override version must belong to the same struct as   the batch's
        default version (400 otherwise).

        - Deprecated versions are rejected (400).
      operationId: v2_batches_documents_create
      parameters:
        - in: path
          name: batch_id
          schema:
            type: integer
          required: true
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchAddDocumentsRequestRequest'
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchAddDocumentsResponse'
          description: ''
        '400':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: {}
          description: ''
        '404':
          content:
            application/json:
              schema:
                type: object
                additionalProperties: {}
          description: ''
      security:
        - extractApiKey: []
        - bearerAuth: []
        - cookieAuth: []
components:
  schemas:
    BatchAddDocumentsRequestRequest:
      type: object
      description: |-
        Body for POST /api/v2/batches/{batch_id}/documents/.

        Adds existing library documents (referenced by ``ChatAttachment.pk``) to
        a batch. The batch's parent struct is implicit via
        ``Batch.default_struct_version`` — every per-doc version override must
        refer to the same struct.

        Fields:
            document_ids: ChatAttachment PKs of docs in the user's library to
                attach to the batch. At least one is required. Order is
                preserved when assigning ``BatchDocument.processing_order``.
            version_number: Optional override for the batch's
                ``default_struct_version`` for *all* added docs in this request.
                ``None`` means "use the batch's current default version".
            per_doc_versions: Optional fine-grained per-doc override map
                ``{document_id: version_number}``. Keys must be a subset of
                ``document_ids``. A doc id present in this map takes precedence
                over the request-level ``version_number``.
      properties:
        document_ids:
          type: array
          items:
            type: integer
            minimum: 1
          description: ChatAttachment PKs of library docs to add to the batch.
          maxItems: 1024
        version_number:
          type:
            - integer
            - 'null'
          minimum: 1
          description: >-
            Override version number applied to every added doc unless a per-doc
            override is set. Defaults to the batch's ``default_struct_version``.
        per_doc_versions:
          type: object
          additionalProperties:
            type: integer
            minimum: 1
          description: >-
            Map of ``{document_id: version_number}`` overriding the
            request-level ``version_number`` for individual docs.
      required:
        - document_ids
    BatchAddDocumentsResponse:
      type: object
      description: |-
        Response shape for POST /api/v2/batches/{batch_id}/documents/.

        ``added`` lists the new ``BatchDocument`` rows in creation order.
        ``skipped`` lists docs that were not added with a human-readable
        reason — the request as a whole still returns 201.
      properties:
        added:
          type: array
          items:
            $ref: '#/components/schemas/BatchDocumentSummary'
        skipped:
          type: array
          items:
            $ref: '#/components/schemas/BatchAddDocumentsSkipped'
      required:
        - added
        - skipped
    BatchDocumentSummary:
      type: object
      description: >-
        Compact per-doc projection for the add-documents response.


        Mirrors the relevant slice of ``BatchDocument`` plus the

        ``StructVersion.version_number`` so the SPA can render the row without a

        follow-up GET.


        The three ``*_field_count`` / ``extraction_status`` fields surface

        extraction quality so abacus-spa renders the same colored status chip

        + "Show problems only" filter that the legacy HTML grid does (Task 3 of

        the extraction-failure-surfacing plan). They are ``None`` / ``0`` for

        freshly-added docs (PENDING — the worker hasn't classified them yet)

        and populated for COMPLETED docs whose
        ``ChatAttachment.extraction_result``

        has been written.
      properties:
        id:
          type: integer
        document_id:
          type: integer
          description: ChatAttachment.pk for the library doc backing this row.
        original_filename:
          type: string
        status:
          type: string
        processing_order:
          type: integer
        version_number:
          type:
            - integer
            - 'null'
          description: The pinned StructVersion.version_number for this doc's extraction.
        extraction_status:
          type:
            - string
            - 'null'
          description: >-
            Mirrors ``lynx_core.attachments.schema.ExtractionStatus`` — one of
            ``success`` / ``partial`` / ``schema_mismatch`` / ``ocr_failed``.
            ``null`` for PENDING / PROCESSING docs whose extraction has not
            produced a payload yet (clients fall back to ``status`` for the
            lifecycle pill in that case).
        extracted_field_count:
          type: integer
          minimum: 0
          description: >-
            Number of populated fields in the doc's
            ``ChatAttachment.extraction_result.fields``. ``0`` for PENDING /
            PROCESSING docs.
        expected_field_count:
          type:
            - integer
            - 'null'
          minimum: 0
          description: >-
            Number of fields declared by the doc's pinned
            ``StructVersion.schema_json.fields``. ``null`` for
            transcription-only docs (no schema attached).
      required:
        - document_id
        - id
        - original_filename
        - processing_order
        - status
        - version_number
    BatchAddDocumentsSkipped:
      type: object
      description: |-
        Per-doc skip record for the add-documents response.

        Surfaced inline (no failure) so the SPA can render a partial-success
        toast: "Added 3 docs, skipped 2 (1 not owned, 1 already in batch)".

        Possible ``reason`` values:
        - ``"already in batch"`` — the doc is already attached to this batch.
        - ``"already in another batch"`` — the doc is attached to a different
          batch. ``ChatAttachment.extraction_result`` is singular, so a doc
          cannot live in two batches simultaneously.
        - ``"not found or not owned"`` — the doc id doesn't belong to the
          requesting user (or the underlying chat is soft-deleted).
        - ``"duplicate content"`` — another attachment with the same SHA-256
          bytes is already in this batch (AD-421). ``duplicate_of`` is the
          existing ``BatchDocument.pk`` so the grid can flash that row.
      properties:
        document_id:
          type: integer
        reason:
          type: string
        duplicate_of:
          type:
            - integer
            - 'null'
          description: >-
            BatchDocument pk of the existing row when reason is duplicate
            content.
      required:
        - document_id
        - reason
  securitySchemes:
    extractApiKey:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: |
        Organisation API key from Settings → API / `/api/keys/`. The secret is
        shown once. Send `Authorization: Bearer sk-abacus-…`. One active key
        per organisation. Ingest (`/api/v2/ingest/*`) refuses API keys — that
        surface is Auth0-only with `ingest:read` / `ingest:write` scopes.
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >
        Auth0 access token for a signed-in user. Send `Authorization: Bearer
        <token>`.

        The SPA and desktop agent use this. Prefer an organisation API key for

        server-to-server integrations.
    cookieAuth:
      type: apiKey
      in: cookie
      name: sessionid
      description: |
        Django session cookie from a browser login. Present so the web app can
        call `/api/v2` without a header. Do not rely on this for integrations.

````