> ## 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.

# Finalize an attachment

> Verify the bytes uploaded to a reservation and mark the attachment ready.

## Behavior

Finalize reads the bytes that arrived, measures their size, and checks their type against their content; what was declared at reserve time is not trusted. It is safe to repeat. When a standalone reservation's bytes and filename are identical to a `ready` attachment this conversation already holds for the same end user, the response is that existing record, with its own `id`, and the new reservation is retired; so use the `id` from this response. A folder member is never reused this way: two identical files at two paths in one folder stay two attachments. See [Attachments](/conversations/attachments#upload-a-large-file-reserve-put-finalize).


## OpenAPI

````yaml POST /api/v1/conversation-instances/{instance_slug}/conversations/{conversation_id}/attachments/{attachment_id}/finalize
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/{attachment_id}/finalize:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: conversation_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: attachment_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags:
        - Conversations
      summary: Finalize a reserved attachment
      description: >
        Step three of the direct upload path. Reads the bytes that arrived

        on the reservation, measures their size, resolves the media type from

        the name and the full content, hashes them, and marks the record

        `ready`, or refuses it. When the bytes never arrived, the refusal is

        `400 attachment_rejected` and the record stays `storing`, so you can

        PUT the bytes and finalize again; the cleanup pass removes a record

        still `storing` after an hour. Any other refusal removes the record

        from lists at once. Nothing declared at reserve time is trusted. A

        zip or email is opened here by the same rules as a multipart upload

        (at most 25 accepted files kept; see

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


        Safe to repeat: finalizing a record that is already `ready` returns

        it again. When the bytes are identical to a ready file the

        conversation already holds under the same name, the reservation is

        retired and the existing record is returned with its own `id`; use

        the `id` from this response. Files inside a folder are exempt from

        that reuse. Requires `conversations:use` for scoped keys.
      operationId: finalizeConversationAttachment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - end_user
              properties:
                end_user:
                  type: string
                  description: >-
                    The asserted end-user identity the conversation must belong
                    to.
            example:
              end_user: u_dana_ortiz
      responses:
        '200':
          description: The ready attachment.
          content:
            application/json:
              schema:
                type: object
                required:
                  - attachment
                properties:
                  attachment:
                    $ref: '#/components/schemas/ConversationAttachment'
        '400':
          description: |
            The instance slug is empty (`missing_instance_slug`), the body is
            unreadable or has an unknown field (`invalid_request`), `end_user`
            is missing (`invalid_request`), the attachment id segment is empty
            (`invalid_request`), the published config does not
            enable attachments (`attachments_not_enabled`), or the file is
            refused for its shape (`attachment_rejected`): the bytes never
            arrived, the file is empty, a container could not be opened or a
            file inside it could not be read, no file could be extracted
            from a container, or a container holds more files than the
            conversation has room for under its limit of 100 attachments.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '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`), the conversation
            (`conversation_not_found`) or the attachment
            (`attachment_not_found`) is not in scope, or the attachment was
            deleted or refused.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: >
            The instance has no published version (`instance_not_published`),

            or the record exists but is not waiting for bytes, for example it

            is a folder container or a container still unwrapping

            (`attachment_not_reservable`). A deleted or refused attachment

            answers 404 `attachment_not_found`. A record that is already `ready`
            returns

            200 with the record, so a retried finalize is safe.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: >-
            The measured size is over the per-file limit
            (`attachment_too_large`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '415':
          description: >-
            The type is not accepted, the content disagrees with the name, or
            every file extracted from a container fails those checks
            (`attachment_rejected`). A container from which no file could be
            extracted, or with a file inside that cannot be read, is `400`.
          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 file could not be saved (`internal_error`). An `attachment_id`
            that is not a UUID also answers 500 `internal_error`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/AttachmentsUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
      security:
        - BearerAuth: []
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.

````