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

# Ingest a batch of content items

> Queue many content items in one call and get one accept envelope back.

Every valid item becomes its own durable ingest job, so a slow or failing
item cannot hold up the rest of the batch; poll each one with
`GET /v1/ingest/{jobId}/status` or list them with `GET /v1/jobs`. Items
that cannot be queued (empty content, oversized content, a non-object
`metadata`) are reported in `results` with a reason and counted in
`skipped`; they never fail the batch.

Jobs are written to the effective subject's partition: the key owner by
default, or a provisioned subject selected with the `X-Exo-Subject` header
(an unprovisioned selector is refused by the shared subject seam before this
handler runs). An `Idempotency-Key` makes the accept replay-safe: a retry
with the same key and body returns the original envelope, with the original
job ids, without queueing the batch a second time.

## Authorization

This route requires the `write` scope.

A missing or invalid credential returns 401 [`authentication_error`](/errors/authentication_error). A valid credential without the scope returns 403 [`permission_denied`](/errors/permission_denied), and the problem body names the exact scope required.

## Headers

These are request conventions the contract does not declare as parameters, so they do not appear in the schema tables below.

| Header            | Applies  | What it does                                                                                                                                           |
| ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `X-Exo-API-Key`   | Required | Your Exo API key, `exo_<env>_<token>`. `Authorization: Bearer exo_...` is accepted as an alternate.                                                    |
| `X-Exo-Subject`   | Optional | Acts for a provisioned subject instead of the key owner. An unprovisioned id returns 403 [`subject_not_provisioned`](/errors/subject_not_provisioned). |
| `Idempotency-Key` | Honoured | Makes a retry safe. The same key with the same body replays the original response instead of acting twice. See [Idempotency](/platform/idempotency).   |
| `X-Request-Id`    | Response | Returned on every response, including `204`s. Quote it in a support request.                                                                           |

## Success responses

| Status | Meaning                                           |
| ------ | ------------------------------------------------- |
| `202`  | Every valid item was durably queued for ingestion |

## Errors

| Status | Code                                                   | When                                                           |
| ------ | ------------------------------------------------------ | -------------------------------------------------------------- |
| `400`  | [`invalid_request`](/errors/invalid_request)           | Malformed request, for example an invalid cursor.              |
| `401`  | [`authentication_error`](/errors/authentication_error) | Missing or invalid credentials.                                |
| `403`  | [`permission_denied`](/errors/permission_denied)       | Missing scope, or an unprovisioned subject selector.           |
| `404`  | [`not_found`](/errors/not_found)                       | The resource does not exist, or is not visible to this caller. |
| `409`  | [`conflict`](/errors/conflict)                         | A conflict with the current state of the resource.             |
| `422`  | [`validation_error`](/errors/validation_error)         | The request was understood but failed field validation.        |
| `429`  | [`rate_limit_exceeded`](/errors/rate_limit_exceeded)   | Rate limit exceeded. Retry after the Retry-After interval.     |
| `503`  | [`service_degraded`](/errors/service_degraded)         | A dependency is temporarily unavailable. Safe to retry.        |

Beyond the shared statuses, this route can answer with [`idempotency_in_flight`](/errors/idempotency_in_flight), [`idempotency_key_reuse`](/errors/idempotency_key_reuse). Every problem body names its own `code`, and the `type` URI always resolves to the matching page.

Every error is an RFC 9457 `application/problem+json` body carrying a stable `code`, a `requestId`, and a `suggestedAction` where Exo has one. See [Errors](/platform/errors).

## Notes

* **One call, many jobs.** Every valid item becomes its own durable job, so one slow item cannot hold up the rest. `jobIds` lists them in the order they were queued.
* **Unqueueable items are reported, not fatal.** Empty content, oversized content and a non-object `metadata` are counted in `skipped` and explained in `results`. The batch still returns 202.
* **A replayed `Idempotency-Key` returns the original envelope**, including the original job ids, without queueing anything a second time.


## OpenAPI

````yaml openapi.json POST /v1/ingest/batch
openapi: 3.1.0
info:
  contact:
    name: Exo
    url: https://get-exo.com/
  description: >-
    The Exo public developer API: ingest content, retrieve identity-conditioned
    context, and read the knowledge graph. Authenticate every request with an
    Exo API key, either as the X-Exo-API-Key header or as Authorization: Bearer
    exo_... . Errors follow RFC 9457 problem+json with a stable snake_case code
    on every body.
  title: Exo API
  version: 1.0.0
servers:
  - url: https://api.get-exo.com
security:
  - ApiKeyHeader: []
  - BearerAuth: []
tags:
  - name: retrieve
    x-group: Retrieval
  - name: search
    x-group: Search
  - name: ingest
    x-group: Ingestion
  - name: jobs
    x-group: Jobs
  - name: subjects
    x-group: Subjects
  - name: recall
    x-group: Recall
  - name: graph
    x-group: Graph
  - name: insights
    x-group: Contradictions and proposals
  - name: condition
    x-group: Condition
  - name: brain
    x-group: Brain
  - name: sessions
    x-group: Coding sessions
  - name: connectors
    x-group: Connectors
  - name: events
    x-group: Events and webhooks
  - name: meta
    x-group: Identity
  - name: keys
    x-group: Keys and sandbox
  - name: usage
    x-group: Usage
  - name: settings
    x-group: Settings
  - name: exports
    x-group: Exports
  - name: team
    x-group: Team
  - name: admin
    x-group: Account and purge
paths:
  /v1/ingest/batch:
    post:
      tags:
        - ingest
      summary: Ingest a batch of content items
      description: >-
        Queue many content items in one call and get one accept envelope back.


        Every valid item becomes its own durable ingest job, so a slow or
        failing

        item cannot hold up the rest of the batch; poll each one with

        ``GET /v1/ingest/{jobId}/status`` or list them with ``GET /v1/jobs``.
        Items

        that cannot be queued (empty content, oversized content, a non-object

        ``metadata``) are reported in ``results`` with a reason and counted in

        ``skipped``; they never fail the batch.


        Jobs are written to the effective subject's partition: the key owner by

        default, or a provisioned subject selected with the ``X-Exo-Subject``
        header

        (an unprovisioned selector is refused by the shared subject seam before
        this

        handler runs). An ``Idempotency-Key`` makes the accept replay-safe: a
        retry

        with the same key and body returns the original envelope, with the
        original

        job ids, without queueing the batch a second time.
      operationId: ingestBatch
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchIngestRequest'
        required: true
      responses:
        '202':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchIngestResponse'
          description: Every valid item was durably queued for ingestion
        '400':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: Malformed request, for example an invalid cursor.
        '401':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: Missing or invalid credentials
        '403':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: Missing scope or unprovisioned subject
        '404':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: The requested resource does not exist.
        '409':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: Conflict, for example a duplicate in-flight idempotent request.
        '422':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: The batch exceeds the item cap or is malformed
        '429':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: Rate limit exceeded
        '503':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          description: A dependency is unavailable
components:
  schemas:
    BatchIngestRequest:
      description: >-
        Body for ``POST /v1/ingest/batch``.


        ``items`` keeps the legacy free-form ``{content, source_type?,
        metadata?}``

        element shape (plus the ``title`` the MCP already sends) so existing
        callers

        need no change; per-item validation is reported inside ``results``
        rather

        than failing the whole batch. The reserved ``subject`` selector (spec
        4.2) is

        consumed by the shared subject seam from the raw body and is
        deliberately NOT

        declared here, matching every other public request model.
      properties:
        items:
          description: >-
            Content items to ingest. Each is an object with 'content' (required,
            up to 100000 characters) plus optional 'title', 'source_type' and
            'metadata'. An item that fails validation is reported in 'results'
            and does not fail the batch.
          items:
            additionalProperties: true
            type: object
          maxItems: 500
          title: Items
          type: array
      required:
        - items
      title: BatchIngestRequest
      type: object
    BatchIngestResponse:
      description: >-
        202 envelope for a batch accept.


        The first four fields are the legacy wire contract, unchanged and in
        their

        original order; ``ingested`` / ``skipped`` / ``jobIds`` are additive.
      properties:
        failed:
          description: total - succeeded. Includes skipped items (legacy arithmetic).
          title: Failed
          type: integer
        ingested:
          description: Alias of 'succeeded' for batch-ingest clients.
          title: Ingested
          type: integer
        jobIds:
          description: Job ids for the accepted items; poll GET /v1/ingest/{jobId}/status.
          items:
            type: string
          title: Jobids
          type: array
        results:
          description: >-
            Per-item outcome, in request order: accepted items carry
            {status:'accepted', node_id:'', chunks:0, jobId}; rejected items
            carry {status:'skipped', reason}.
          items:
            additionalProperties: true
            type: object
          title: Results
          type: array
        skipped:
          description: Items rejected before enqueue.
          title: Skipped
          type: integer
        succeeded:
          description: Items accepted for ingestion.
          title: Succeeded
          type: integer
        total:
          description: Items received.
          title: Total
          type: integer
      required:
        - total
        - succeeded
        - failed
        - results
        - ingested
        - skipped
        - jobIds
      title: BatchIngestResponse
      type: object
    Problem:
      description: |-
        RFC 9457 problem+json envelope (spec D9), documented in OpenAPI.

        Invariant: ``code`` equals the tail of the ``type`` URI
        (``https://docs.get-exo.com/errors/<code>``).
      properties:
        code:
          title: Code
          type: string
        detail:
          title: Detail
          type: string
        documentationUrl:
          anyOf:
            - type: string
            - type: 'null'
          title: Documentationurl
        errors:
          anyOf:
            - items:
                $ref: '#/components/schemas/FieldViolation'
              type: array
            - type: 'null'
          title: Errors
        requestId:
          default: ''
          title: Requestid
          type: string
        status:
          title: Status
          type: integer
        suggestedAction:
          anyOf:
            - type: string
            - type: 'null'
          title: Suggestedaction
        title:
          title: Title
          type: string
        type:
          title: Type
          type: string
      required:
        - type
        - title
        - status
        - detail
        - code
      title: Problem
      type: object
    FieldViolation:
      description: A single field-level violation inside a Problem ``errors[]`` list.
      properties:
        code:
          title: Code
          type: string
        field:
          title: Field
          type: string
        message:
          title: Message
          type: string
        pointer:
          title: Pointer
          type: string
      required:
        - pointer
        - field
        - code
        - message
      title: FieldViolation
      type: object
  securitySchemes:
    ApiKeyHeader:
      description: An Exo API key (exo_...) sent as the X-Exo-API-Key header.
      in: header
      name: X-Exo-API-Key
      type: apiKey
    BearerAuth:
      description: The same Exo API key (exo_...) sent as an Authorization bearer token.
      scheme: bearer
      type: http

````