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

# Upload an attachment

> Upload one file to a conversation as multipart form data.

## Behavior

This route takes `multipart/form-data` with the fields `end_user` and `file`, and the body may be up to the per-file limit plus 1 MiB. To send a file straight from a browser to storage, use [reserve](/api-reference/conversations/attachments/reserve-attachment), a PUT to the returned URL, then [finalize](/api-reference/conversations/attachments/finalize-attachment). Formats, limits, and the full flow are on [Attachments](/conversations/attachments).


## OpenAPI

````yaml POST /api/v1/conversation-instances/{instance_slug}/conversations/{conversation_id}/attachments
openapi: 3.1.0
info:
  title: Nexio API
  version: '1.0'
  description: |
    The Nexio public API. Submit runs to configured engines and read their
    results, hold conversations with configured assistants, make authorized
    reads over the planes and families of a connected system of record, and
    manage webhooks and environments.

    Six engine types are registered: comparison, matching, entity_analysis,
    diligence, triage, and opportunity.

    Every route under /api/v1/ requires `Authorization: Bearer <key>`, except
    inbound event ingest (`POST /api/v1/events/ingest/{source_key}`), which
    authenticates each request against its ingest source instead: an
    HMAC-SHA256 signature for `nexio` and `github` sources, or a shared
    authentication code for an `ams360_ons` source. Error
    bodies share the `Error` schema. Call the API from servers only: it sends
    no CORS headers.
  contact:
    email: support@usenexio.com
    url: https://docs.usenexio.com
servers:
  - url: https://api.usenexio.com
    description: >-
      Nexio API. Sandbox or live is chosen by the API key's environment, not by
      the host.
security:
  - BearerAuth: []
tags:
  - name: EngineManagement
    description: >-
      Create, configure, version and manage engines. An engine is a
      configuration of one registered engine type.
  - name: Engines
    description: Engine-scoped endpoints for submitting runs and retrieving results.
  - name: Runs
    description: Retrieve and reconcile submitted runs.
  - name: Records
    description: >-
      Typed, authorized reads over the registered planes and families of a
      connected system of record, and governed writes to the action ledger. The
      generic family read is the core contract.
  - name: Graph
    description: >-
      The organization's data graph: a read-only map of connections and the
      derivations scheduled on them. Node kinds are a closed registry of 16; the
      API serves 14.
  - name: Environments
    description: >-
      The live environment and up to 5 sandbox environments in your org.
      Organization keys only.
  - name: Webhooks
    description: >-
      Manage webhook endpoints that receive terminal run events (run.completed,
      run.failed, run.cancelled) and run corrections (run.superseded).
  - name: Events
    description: >-
      Send inbound events to your organization's event log through an ingest
      source. Source kinds are nexio, github and ams360_ons; a nexio source
      sends event types from your own vocabulary.
  - name: Converse
    description: The raw stateless conversational model turn (caller-owned state).
  - name: Conversations
    description: >-
      Conversation instances (orchestrators over engines) and their managed
      conversations, turns, annotations, evals, and exports.
  - name: Platform
    description: Service health. No key needed.
paths:
  /api/v1/conversation-instances/{instance_slug}/conversations/{conversation_id}/attachments:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: conversation_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags:
        - Conversations
      summary: Attach a file to a conversation
      description: >
        Uploads one file against a conversation, so a later turn can carry it

        with `attachment_ids`.


        To send a file from a browser straight to storage, use

        the reserve, PUT, finalize path instead

        (`POST .../attachments/reserve`).


        The platform stores the bytes and hands them to the model on the turn

        that names them. Apart from opening a zip or email into its files (an

        email's From, To, Cc, Date, and Subject headers and its body text are

        kept as a `message.txt` file), it does not parse the document and

        stores no extracted text; what the model reads is the file itself.


        The instance's PUBLISHED `attachments` config governs what is

        accepted: whether uploads are enabled at all, how many files ride one

        message, the per-file size ceiling, an optional narrowing of the

        accepted media types, whether containers are opened, and how long

        bytes are kept. An instance with no `attachments` block, or with

        `enabled: false`, takes none and answers `attachments_not_enabled`.


        Accepted formats are a closed set: PDF, Word (including `.odt` and

        `.rtf`), PowerPoint, Excel, CSV, TSV, plain text, Markdown, JSON, XML,

        HTML, PNG, JPEG, WebP, GIF, and (when the instance opens containers)

        `.eml` and `.zip`. A file is classified by its extension (by its

        declared type only when the name has no extension), closed against

        that set, and then checked to confirm

        its contents agree; a mismatch is refused. Outlook `.msg` is not

        accepted, and its refusal says to attach the file inside the message

        or save it as `.eml`.


        Some formats are read with a limit worth knowing. A spreadsheet is

        read to the first 1,000 rows per sheet, and text is read out of Office

        files while images and charts inside them are not. When a limit

        applies, the response carries a `notice` naming it in plain words. Show

        that notice: an answer drawn from part of a schedule is not an answer

        about the schedule.


        A `.zip` or `.eml` container (accepted only when the instance sets

        `unwrap_archives: true`) is opened at upload, one level deep. Each

        file whose name the platform does not accept, and each container inside

        the container, is skipped without being extracted. The rest are

        extracted (at most 200; accepted names past that are omitted) and

        checked like a file sent on its own; files that are oversized, refused

        by the instance's policy, or whose bytes disagree with their name are

        skipped, and they still count toward the 200. At most 25 accepted files
        are kept and each is

        stored as its own attachment; accepted files past the 25th are omitted

        and the upload still succeeds. When anything was omitted or skipped,

        the container's `notice` counts the files read, names up to 20

        omitted files and counts the rest, and counts the skipped ones. A

        container that cannot be opened, or with a file inside that cannot be

        read or decompressed, or from which no file could be extracted, answers

        `400 attachment_rejected`. A container whose extracted files all fail

        the per-file checks answers `415 attachment_rejected`. A turn that names
        the container carries the

        files inside it, not the container. See

        [How a zip or email is
        opened](/conversations/attachments#how-a-zip-or-email-is-opened).


        One conversation holds at most 100 live attachments, files found

        inside containers included. This is a platform ceiling, not a config

        knob. An upload past it, or a container whose files would take the

        conversation past it, is refused whole with `400 attachment_rejected`

        and a message that says so; deleting attachments, or letting retention

        close them, makes room. Re-attaching the same bytes under the same file

        name, while the instance's policy still accepts them, returns the

        existing record and does not count against the ceiling.


        Scope is org, conversation, AND end user. A conversation belonging to

        another org, or to another end user, is `404`, never `403`.
      operationId: createConversationAttachment
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - end_user
                - file
              properties:
                end_user:
                  type: string
                  description: |
                    The asserted end-user identity the conversation must
                    belong to. A mismatch is a 404.
                file:
                  type: string
                  format: binary
                  description: The file to attach.
      responses:
        '201':
          description: The stored attachment.
          content:
            application/json:
              schema:
                type: object
                required:
                  - attachment
                properties:
                  attachment:
                    $ref: '#/components/schemas/ConversationAttachment'
        '400':
          description: |
            The request or the file itself is malformed. Covers an empty
            instance slug (`missing_instance_slug`), a body that is not
            multipart form data or is missing the `file` part or `end_user`
            (`invalid_request`), an instance whose published config has no
            attachments block or sets `enabled: false`
            (`attachments_not_enabled`), and a file refused for its own shape
            (`attachment_rejected`): an empty file, a container that could not
            be opened, a zip that lists more than 10,000 entries or expands past
            100 MiB, a file inside a container that could not be read, a
            container from which no file could be extracted, a conversation
            already at its 100-attachment ceiling, or a container whose files
            would take the conversation past it. Distinct from 415 on purpose:
            converting a malformed file to another type would not fix it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: attachment_rejected
                message: The file is empty.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >-
            A scoped key lacks the capability (`insufficient_capability`), or
            the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: >-
            The instance (`instance_not_found`) or the conversation
            (`conversation_not_found`) is not in scope. A conversation in
            another organization, under another end user, or created with a key
            from another environment answers the same way.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: >-
            The instance has no published version, so no upload policy is in
            force (`instance_not_published`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: |
            The file is larger than the instance accepts
            (`attachment_too_large`). Distinct from 415 on purpose: a size
            problem and a type problem call for different things from the
            person who sent the file.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '415':
          description: >
            The file's type was refused (`attachment_rejected`): it is outside

            the accepted set, the instance narrowed the accepted types past

            it, its contents disagree with its name, or it is a container

            whose extracted files all fail those checks. A container from

            which no file could be extracted is `400` instead. The message names
            the

            reason in words a person can act on.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: >-
            The published config cannot be parsed (`instance_config_invalid`) or
            the upload failed (`internal_error`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/AttachmentsUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
components:
  parameters:
    InstanceSlug:
      name: instance_slug
      in: path
      required: true
      description: Conversation instance identifier slug (e.g. `platform-assistant`).
      schema:
        type: string
  schemas:
    ConversationAttachment:
      type: object
      description: |
        One file attached to a conversation. There is no text field: the
        platform does not parse the document, it hands the bytes to the model.
      required:
        - id
        - filename
        - media_type
        - size_bytes
        - status
        - delivery
        - created_at
      properties:
        id:
          type: string
          format: uuid
        filename:
          type: string
        media_type:
          type: string
        size_bytes:
          type: integer
          format: int64
        status:
          type: string
          enum:
            - storing
            - unwrapping
            - ready
            - rejected
            - failed
          description: |
            `ready` can ride a turn. A multipart upload answers `ready`. A
            reserved upload is `storing` until finalize; a folder container
            is `unwrapping` until every member is finalized. A finalize that
            finds no bytes leaves the record `storing`. A finalize that refuses
            the file for any other reason marks it `rejected` and removes it at
            once, so it no longer lists or reads. `failed` is reserved and not
            currently set. Only `ready` records can ride a turn or be served.
        delivery:
          type: string
          enum:
            - file
            - image
            - unwrap
          description: |
            How the file reaches the model. `file` and `image` are provider
            inputs. `unwrap` is a container, which never reaches the model
            itself; a turn naming it carries the files inside it.

            A folder container also has delivery `unwrap`.
        notice:
          type: string
          description: >
            How this format is read, when there is a limit worth knowing. On a

            zip or email container it says how many files were read and what

            was left out; on a folder container it says how many files the

            folder holds. A spreadsheet is read to the first 1,000 rows per
            sheet; text is

            read out of Office files while images and charts inside them are

            not. Absent when the file is read whole. Show it: an answer drawn

            from part of a schedule is not an answer about the schedule.
        error:
          type: string
          description: Why the record was rejected or failed. Absent otherwise.
        container_kind:
          type: string
          enum:
            - zip
            - eml
            - folder
          description: |
            What a container is. `zip` and `eml` are opened by the platform;
            `folder` is opened by the client and its files uploaded one by
            one. Set on a folder, and on a zip or email stored through
            finalize. Absent on a zip or email stored by a multipart upload,
            on a plain file, and on a file inside a container.
        parent_attachment_id:
          type: string
          format: uuid
          description: The container this file was found inside, when it was.
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
          description: |
            When access to the attachment ends. From this moment it is left
            out of lists, answers `404` when read or downloaded, and cannot be
            named on a turn. A later retention sweep deletes the record and
            removes the bytes. Absent when the attachment follows its
            conversation's own retention.
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          description: >
            Stable snake_case error identifier. Safe to match programmatically.

            New codes are added over time; treat an unknown code by its HTTP
            status.


            Known codes include:

            `invalid_request`, `invalid_input`, `unauthorized`,

            `auth_unavailable`, `rate_limited`, `missing_run_id`,

            `invalid_run_id`, `run_not_found`, `invalid_offerings`,

            `missing_input`, `queue_unreachable`, `engine_not_found`,

            `engine_slug_conflict`, `engine_archived`, `instance_not_found`,

            `instance_slug_conflict`, `instance_archived`,

            `instance_follows_canonical`, `instance_not_published`,

            `invalid_engine_type`, `validation_error`, `webhook_not_found`,

            `webhook_limit_exceeded`, `invalid_url`, `invalid_events`,

            `invalid_description`, `invalid_auth_token`,

            `missing_endpoint_id`, `invalid_endpoint_id`,

            `missing_delivery_id`, `invalid_delivery_id`,

            `delivery_not_found`, `delivery_not_resendable`,

            `insufficient_capability`, `engine_version_required`,

            `engine_version_exact_required`, `engine_version_not_found`,

            `engine_version_invalid_format`,

            `engine_version_draft_requires_sandbox_key`,

            `engine_version_none_released`, `test_scenario_sandbox_only`,

            `test_scenario_forbidden`, `test_scenario_exact_version_required`,

            `test_scenario_version_not_supported`, `invalid_test_scenario`,

            `request_bound_exceeded`, `run_cap_exceeded`,

            `scoped_key_required`, `engine_binding_forbidden`,

            `request_timeout`, `internal_error`, `idempotency_key_reused`,

            `invalid_idempotency_key`, `acting_principal_mismatch`,

            `run_requires_acting_principal`, `environment_slug_invalid`,

            `environment_slug_reserved`, `environment_slug_taken`,

            `environment_name_invalid`, `environment_limit_reached`,

            `environment_live_immutable`, `environment_not_found`,

            `environment_in_use`, `environment_operation_failed`,

            `list_environments_failed`, `engine_release_unservable`,

            `engine_config_hash_inconsistent`,

            `engine_config_hash_unavailable`,

            `engine_config_version_unavailable`,

            `engine_version_resolve_failed`,

            `engine_config_version_load_failed`,

            `engine_version_publish_mismatch`,

            `engine_version_schema_change_requires_major`,

            `engine_config_hash_missing`, `engine_config_changed`,

            `invalid_engine_config`, `cold_start_gate_not_met`,

            `cold_start_gate_failed`, `cold_start_gate_regressed`,

            `cancel_run_failed`, `event_id_reused`, `invalid_event_id`,

            `missing_event_id`, `invalid_event_type`, `invalid_payload`,

            `reason_text_too_long`, `reason_taxonomy_version_unknown`,

            `invalid_rating`, `missing_comment`, `invalid_target`,

            `invalid_time_on_task`, `scoped_annotation_required`,

            `missing_instance_slug`, `invalid_instance_config`,

            `invalid_eval_waiver`, `instance_config_hash_missing`,

            `config_changed_during_publish`,

            `scenario_set_changed_during_publish`,

            `conversation_eval_regressed`,

            `conversation_eval_execution_failed`,

            `conversation_eval_gate_unavailable`, `publish_failed`,

            `conversation_not_found`, `conversation_archived`,

            `invalid_cursor`, `message_not_found`, `invalid_turn_request`,

            `message_too_long`, `invalid_tool_result`, `invalid_confirmation`,

            `invalid_edit_target`, `turn_in_progress`, `pending_turn`,

            `turn_state_conflict`, `confirmation_environment_unpinned`,

            `instance_config_missing`, `instance_config_invalid`,

            `instance_model_unavailable`, `guardrail_evaluation_failed`,

            `guardrail_config_invalid`, `invalid_conversation_history`,

            `streaming_unsupported`, `converse_unavailable`, `provider_error`,

            `provider_unavailable`, `unknown_model`, `too_many_messages`,

            `too_many_tools`, `invalid_max_tokens`, `invalid_message_role`,

            `invalid_message`, `invalid_content_block`, `invalid_tool`,

            `attachments_not_enabled`, `attachments_unavailable`,

            `attachment_rejected`, `attachment_too_large`,

            `attachments_too_large`, `too_many_attachments`,

            `attachment_not_accepted`, `attachment_not_found`,

            `attachment_changed`, `attachment_read_failed`,

            `attachment_member_delete`, `attachment_not_reservable`,

            `invalid_offset`, `invalid_turn_id`, `invalid_comment`,

            `invalid_reason`, `invalid_feedback_key`, `turn_not_found`,

            `annotation_not_found`, `annotation_not_promotable`,

            `invalid_eval_scenario`, `conversation_eval_scenario_not_found`,

            `conversation_eval_scenario_cap`,

            `conversation_eval_run_not_found`,

            `conversation_eval_baseline_not_found`,

            `conversation_eval_unavailable`,

            `instance_version_invalid_format`, `instance_version_not_found`,

            `document_class_not_open`, `presign_failed`,

            `markets_directory_unavailable`, `book_unavailable`,

            `book_connection_ambiguous`, `identity_unmapped`,

            `identity_needs_review`, `identity_suspended`, `identity_stale`,

            `scope_unavailable`, `assertion_invalid`, `assertion_stale`,

            `cursor_filter_mismatch`, `cursor_expired`,

            `action_schema_unknown`, `action_payload_invalid`,

            `action_out_of_scope`, `action_list_too_large`,

            `overlay_read_only`, `unsupported_node_type`,

            `acting_principal_required`, `action_denied`,

            `appetite_read_denied`, `approval_required`,

            `catalog_access_denied`, `catalog_connection_ambiguous`,

            `create_webhook_failed`, `dataset_denied`, `delivery_id_reused`,

            `document_type_not_servable`, `document_unclassified`,

            `egress_manifest_version_mismatch`,

            `egress_manifest_version_required`, `event_type_not_allowed`,

            `event_type_reserved`, `execution_confirm_required`,

            `execution_prohibited`, `ingest_source_disabled`,

            `ingest_source_misconfigured`, `ingest_source_not_found`,

            `invalid_acting_assertion`, `invalid_active`,

            `invalid_credentials`, `invalid_payload_mode`,

            `invalid_signature`, `load_run_failed`, `load_solutions_failed`,

            `load_work_items_failed`, `missing_engine_slug`, `not_found`,

            `object_store_unconfigured`, `provider_not_approved`,

            `resolve_retry_exhausted`, `run_lookup_failed`,

            `service_identity_unknown`, `stale_timestamp`, `surface_denied`,

            `unresolved_market_question`, `unsupported_market_filter`.


            The full list with causes and fixes is at

            https://docs.usenexio.com/reference/errors.
        message:
          type: string
          description: Human-readable error message. May change between versions.
        details:
          description: |
            Optional request-specific details. Request-bound failures use the
            `RequestBoundDetails` object. Validation failures may use an array
            of field issues or another documented object.

            `409 environment_in_use` is the one exception to this envelope: it
            carries a top-level `blockers` object (see `EnvironmentInUseError`)
            instead of `details`.
  responses:
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: unauthorized
            message: Missing or invalid API key
    RateLimited:
      description: Rate limit exceeded. Retry after the window resets.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: rate_limited
            message: Rate limit exceeded
    AttachmentsUnavailable:
      description: >
        `attachments_unavailable`: file attachments are not available in this

        environment because the platform has no attachment storage wired. That

        cause is not retryable by the caller; the instance's operator has to

        enable attachment storage.


        Also `auth_unavailable`: API key authentication was briefly unavailable
        before the route ran. That cause is transient; retry it with backoff.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: attachments_unavailable
            message: File attachments are not available in this environment.
    RequestTimeout:
      description: >-
        `request_timeout`: the 30 second route timeout expired before the
        handler answered.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: request_timeout
            message: Request timed out
  headers:
    RetryAfter:
      description: Whole seconds to wait before retrying, at least 1.
      schema:
        type: integer
        minimum: 1
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: >
        Send the key as `Authorization: Bearer <key>`. Two kinds of key exist.


        Organization keys are issued in the portal (Settings, then API keys),

        each bound to one environment, shaped `nx_<environment slug>_<64 hex>`.
        They carry

        no capabilities and pass every capability check, with one exception:

        routes under /api/v1/records, /api/v1/engines/{id}/opportunities,

        /api/v1/graph and /api/v1/catalog/documents accept an

        organization key only when its environment is `live`, and refuse any

        other with 403 `scoped_key_required`. Revocation takes effect within 60

        seconds.


        Scoped keys are issued by Nexio on request, shaped

        `nxsk_v1_<24 hex key id>_<43 character secret>`. Each is bound to one

        org, one environment, a set of engines and a set of capabilities. A

        malformed, unknown or revoked `nxsk_` key fails with 401 and is never

        retried as an organization key. Revocation takes effect on the next

        request. A scoped key without a route's capability gets 403

        `insufficient_capability`; a scoped key not bound to the engine gets 403

        `engine_binding_forbidden`.


        Key-grantable capabilities: `engines:read`, `runs:write`, `runs:read`,

        `runs:defensibility:read`, `runs:test`, `catalog:read`,

        `catalog:documents:read`, `webhooks:manage`, `conversations:use`,

        `conversations:export`, `records:read`, `records:opportunities:run`,

        `actions:write`, `actions:read`, `graph:read`, `records:analyze`.


        Routes that accept organization keys only (every scoped key gets 403

        `insufficient_capability`): environment management, engine create,

        update, configuration and publish, and conversation instance

        authoring. Each operation description names the capability a scoped

        key needs.

````