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

# Confirmar que el archivo ya está subido

> Cierra el ciclo que abrió el presign: la API va al almacén, **mira los bytes** y decide
si el archivo vale. Hasta que esto ocurre, lo subido no es utilizable por nadie.

Comprueba dos cosas que el cliente no puede acreditar por sí mismo: cuánto ocupa el
objeto de verdad —de ahí que `sizeBytes` pueda no coincidir con `declaredSizeBytes`—, y
que su **contenido** corresponde a su extensión. Un `.pdf` que no empieza por `%PDF` se
rechaza aunque el nombre diga otra cosa.

Es **idempotente sobre lo que salió bien**: confirmar un archivo que ya está `ready`
devuelve `200` con el mismo cuerpo y no vuelve a tocar el almacén. Lo es a propósito,
porque el cliente reintenta esta llamada cuando la red le falla a mitad.

Esa red de seguridad **no cubre el rechazo**. Un `rejected` es terminal: no admite un
segundo `confirm`, así que si el `422` se perdió por el camino, el reintento no vuelve a
dártelo. Para saber cómo acabó una subida cuyo `confirm` no llegó a contestar, usa el
`GET`, que responde el estado y el `rejectedReason`.

Un rechazo **se persiste antes de responder**: el archivo queda en `rejected` con su
`rejectedReason`, no se pierde el motivo. Por eso responde `422` y no `400` — la
petición era válida; lo que no valía era el contenido.




## OpenAPI

````yaml /openapi/public-api.yaml post /v1/uploads/{uploadId}/confirm
openapi: 3.1.0
info:
  title: Agents Studio API
  version: 1.0.0
  description: >
    API del backend de Agents Studio. Expone servicios multi-tenant para
    gestionar agentes

    (texto y voz), catálogo de herramientas, habilitación de workspaces y
    operaciones de

    telefonía. Todos los endpoints requieren autenticación Bearer y respetan los
    contratos

    documentados en `docs/modules`.
  license:
    name: Proprietary
    url: https://getsupervisor.ai/legal/terms
servers:
  - url: https://api-prod.studio.getsupervisor.ai
    description: Producción
  - url: https://sandbox.agents.studio.getsupervisor.ai
    description: Sandbox
security:
  - BearerAuth: []
tags:
  - name: Agents
    description: >
      Gestión general de agentes y sus metadatos.

      Scopes requeridos: `agents:read` para consultas y `agents:write` para
      creación, actualización y operaciones sobre teléfonos.
  - name: Agent Versions
    description: Versionado y mantenimiento de versiones publicadas de los agentes.
  - name: Agent Schedules
    description: >-
      Configuración de horarios regulares y excepciones puntuales para controlar
      la disponibilidad de los agentes.
  - name: Campaigns
    description: >
      Gestión de campañas masivas basadas en CSV para ejecutar agentes de forma
      batch.

      Scopes requeridos: `campaigns:read` para consultas y `campaigns:write`
      para creación.
  - name: Calls
    description: |
      Consultas de llamadas (Speech Analytics) disponibles para el workspace.
      Scopes requeridos: `calls:read`.
  - name: Agent Instructions
    description: >
      Operaciones sobre instrucciones (prompt) gestionadas por los usuarios
      finales para

      guiar y corregir el comportamiento del agente. Las instrucciones se
      aplican a la

      versión activa del agente, se mantienen como texto libre sin formato (1 a
      500

      caracteres) y su prioridad se controla mediante el campo `order`.

      Scopes requeridos: `agent-instructions:read` para consultas y
      `agent-instructions:write` para creación o edición.
  - name: Catalogs
    description: >
      Gestión centralizada de catálogos (idiomas, estilos de mensaje, tonos,
      etiquetas, voces y eventos de

      webhook) con alcance global o específico por workspace. Las voces del
      workspace se consultan aquí, con

      `GET /v1/catalogs/items?type=voice`. Scopes requeridos: `catalogs:read`
      para consultas y

      `catalogs:write` para creación y modificaciones.
  - name: Catalog Templates
    description: >
      Catálogo de plantillas de agente publicadas. Sirve para descubrir qué
      plantillas hay disponibles y con

      qué `agentVersionId` clonarlas mediante `POST /v1/agents/from-template`.

      Scopes requeridos: `catalogs:read`.
  - name: Agent Phones
    description: Conexión y desconexión de teléfonos asignados a agentes.
  - name: API Keys
    description: Gestión de credenciales de acceso programático y sus permisos.
  - name: Agent Blueprints
    description: >
      Gestión del blueprint (personalidad) asociado a cada versión de agente.

      Scopes requeridos: `agent-blueprints:read` para lectura y
      `agent-blueprints:write` para cambios en blueprint.
  - name: Blueprint Stages
    description: >
      Autoría de stages de un blueprint del agente, abarcando orden, prompts y
      validaciones previas a la

      publicación. Todas las rutas cuelgan del blueprint
      (`/v1/agents/{agentId}/blueprints/{blueprintId}/stages`),

      así que hay que resolver primero el `blueprintId` con `GET
      /v1/agents/{agentId}/blueprints`.

      Scopes requeridos: `blueprint-stages:read` para consultas y
      `blueprint-stages:write` para creación y edición.

      La validación del grafo se ejecuta automáticamente en cada mutación y la
      sincronización con el proveedor

      sucede mediante jobs internos tras los cambios.
  - name: Stage Triggers
    description: >
      Gestión detallada de triggers que conectan stages dentro del blueprint y
      definen las transiciones

      conversacionales. Scopes requeridos: `stage-triggers:read` y
      `stage-triggers:write`.
  - name: Workspaces
    description: Gestión multi-tenant y habilitación de credenciales por workspace.
  - name: Tools
    description: >
      Catálogo y ejecución de tools disponibles para los agentes.

      Scopes requeridos: `tools:read` para listar y `tools:execute` para
      ejecutar tools.
  - name: Webhooks
    description: >
      Gestión de webhooks por workspace para suscribirse a eventos del dominio y
      recibir notificaciones HTTP.

      Scopes requeridos: `webhooks:read` para consultas y `webhooks:write` para
      crear, actualizar o eliminar webhooks y suscripciones.
  - name: SIP Trunks
    description: >
      Provisionamiento y gestión de SIP trunks para conectar agentes de voz con
      carriers PSTN

      (Twilio, ccc2.uno, etc.) via Kamailio. Genera credenciales digest
      automaticamente.

      Scopes requeridos: `sip:read` para consultas y `sip:write` para creación,
      actualización y eliminación.
  - name: Documents
    description: >
      Gestión de documentos asociados al workspace. Permite obtener URLs
      presignadas

      para subir archivos que luego se referencian al crear o personalizar
      agentes.

      Scopes requeridos: `documents:write`.
  - name: Message Templates
    description: >
      Gestión de message templates de WhatsApp (Kapso). Permite crear, listar y
      eliminar templates,

      así como consultar el estado de la conexión con el número de teléfono
      configurado.

      Scopes requeridos: `agents:read` para consultas y `agents:write` para
      creación y eliminación.
  - name: Billing
    description: |
      Consulta del balance de consumo del workspace sobre el plan contratado.
      Scopes requeridos: `billing:read`.
  - name: Usage
    description: |
      Consultas de consumo de recursos agregado por agente.
      Scopes requeridos: `usage:read`.
  - name: Uploads
    description: >
      Subida de archivos por URL prefirmada: la API reserva el sitio y devuelve
      la URL, y

      el cliente sube contra el almacén sin que los bytes pasen por aquí.

      Toda escritura exige `uploads:write`, y la firma exige **además** el scope
      que decide el

      namespace de la ruta: `agent-briefs` y `avatars` exigen `agents:write`, y

      `knowledge-sources` exige `knowledge-bases:write`.


      Cada namespace declara además qué acepta. `avatars` sólo admite PNG, JPEG
      y WEBP —y

      **no** SVG: se sirven en línea, y un SVG es texto ejecutable—, con un
      archivo por

      lote y 1 MiB como máximo.
  - name: Health
    description: >
      Sonda de disponibilidad del servicio. No requiere autenticación ni
      `X-Workspace-Id`.
  - name: Knowledge Bases
    description: >
      Bases de conocimiento del workspace: el material que el agente consulta
      durante la

      conversación.


      El prefijo nombra el recurso y no la tool que lo implementa, así que
      renombrar la

      tool `knowledge` no mueve estas rutas.


      Las sirve un plugin instalable, y eso tiene una consecuencia visible: si
      la tool no

      está abierta para el workspace, **todas** responden `404` —no `403`—. Una
      feature

      que no está disponible para un cliente no existe para él.


      Scopes requeridos: `knowledge-bases:read` para consultas y
      `knowledge-bases:write`

      para el alta, la baja y el mantenimiento de sus documentos.
paths:
  /v1/uploads/{uploadId}/confirm:
    post:
      tags:
        - Uploads
      summary: Confirmar que el archivo ya está subido
      description: >
        Cierra el ciclo que abrió el presign: la API va al almacén, **mira los
        bytes** y decide

        si el archivo vale. Hasta que esto ocurre, lo subido no es utilizable
        por nadie.


        Comprueba dos cosas que el cliente no puede acreditar por sí mismo:
        cuánto ocupa el

        objeto de verdad —de ahí que `sizeBytes` pueda no coincidir con
        `declaredSizeBytes`—, y

        que su **contenido** corresponde a su extensión. Un `.pdf` que no
        empieza por `%PDF` se

        rechaza aunque el nombre diga otra cosa.


        Es **idempotente sobre lo que salió bien**: confirmar un archivo que ya
        está `ready`

        devuelve `200` con el mismo cuerpo y no vuelve a tocar el almacén. Lo es
        a propósito,

        porque el cliente reintenta esta llamada cuando la red le falla a mitad.


        Esa red de seguridad **no cubre el rechazo**. Un `rejected` es terminal:
        no admite un

        segundo `confirm`, así que si el `422` se perdió por el camino, el
        reintento no vuelve a

        dártelo. Para saber cómo acabó una subida cuyo `confirm` no llegó a
        contestar, usa el

        `GET`, que responde el estado y el `rejectedReason`.


        Un rechazo **se persiste antes de responder**: el archivo queda en
        `rejected` con su

        `rejectedReason`, no se pierde el motivo. Por eso responde `422` y no
        `400` — la

        petición era válida; lo que no valía era el contenido.
      operationId: confirmUpload
      parameters:
        - $ref: '#/components/parameters/XWorkspaceId'
        - $ref: '#/components/parameters/UploadId'
      responses:
        '200':
          description: >
            Archivo verificado y utilizable. También es la respuesta cuando ya
            lo estaba.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Upload'
              examples:
                ready:
                  summary: Confirmado, con el tamaño real medido
                  value:
                    uploadId: 7f3a1b2c-9d4e-4f6a-8b1c-2d3e4f5a6b7c
                    namespace: knowledge-sources
                    status: ready
                    originalFileName: manual-cobranza.pdf
                    contentType: application/pdf
                    declaredSizeBytes: 348172
                    sizeBytes: 348172
                    rejectedReason: null
                    createdBy: usr_2f1c
                    createdAt: '2026-09-04T22:30:00.000Z'
                    confirmedAt: '2026-09-04T22:31:12.000Z'
        '403':
          description: A la petición le falta el scope `uploads:write`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: >
            No hay ninguna subida con ese id en este workspace — o el carril
            está cerrado para

            él. Las dos cosas responden igual a propósito: distinguirlas diría
            si el id existe.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: >
            Se miró el objeto y no vale: no cuadra el tamaño (`size-mismatch`),
            o su contenido

            no corresponde a la extensión (`content-mismatch`).


            **Ninguno de los dos se arregla repitiendo esta llamada**: la fila
            queda `rejected`,

            que es terminal. Si el archivo era el que se quería, hay que pedir
            un presign nuevo;

            si no lo era, hay que cambiar de archivo.


            El motivo viaja en `rejectedReason` **sólo si el workspace tiene
            abierto el

            interruptor que lo publica**. Con el interruptor cerrado responde el
            manejador por

            defecto del framework: mismo `message`, pero sin `rejectedReason`
            —ni `path` ni

            `timestamp`—, y el motivo hay que leerlo con `GET
            /v1/uploads/{uploadId}`. Un cliente

            que quiera decidir con el motivo tiene que tolerar los dos carriles.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadRejectedResponse'
              examples:
                contenido:
                  summary: Dice ser un PDF y por dentro no lo es
                  value:
                    statusCode: 422
                    error: Unprocessable Entity
                    message: 'Upload rejected: the bytes are not a valid pdf.'
                    rejectedReason: content-mismatch
                tamaño:
                  summary: Lo que se subió no mide lo que se declaró
                  value:
                    statusCode: 422
                    error: Unprocessable Entity
                    message: 'Upload rejected: declared 348172 bytes, stored 91204.'
                    rejectedReason: size-mismatch
                interruptorCerrado:
                  summary: >-
                    El mismo rechazo en un workspace que aún no publica el
                    motivo
                  value:
                    statusCode: 422
                    error: Unprocessable Entity
                    message: 'Upload rejected: the bytes are not a valid pdf.'
components:
  parameters:
    XWorkspaceId:
      name: x-workspace-id
      in: header
      required: true
      description: Identificador del workspace multi-tenant.
      schema:
        type: string
        format: uuid
    UploadId:
      name: uploadId
      in: path
      required: true
      description: >
        La fila que dejó el presign. El namespace **no** viaja en estas rutas:
        sale de la fila,

        porque consultarlo antes de autorizar exigiría leer la base para decidir
        el permiso, y

        eso filtraría si un id existe a quien no debería saberlo.
      schema:
        type: string
        format: uuid
  schemas:
    Upload:
      type: object
      description: Lo que se sabe de un archivo subido, mirado desde fuera.
      properties:
        uploadId:
          type: string
          format: uuid
        namespace:
          $ref: '#/components/schemas/UploadNamespace'
        status:
          $ref: '#/components/schemas/UploadStatus'
        originalFileName:
          type: string
          description: El nombre con el que el cliente lo mandó, sin normalizar.
        contentType:
          type: string
        declaredSizeBytes:
          type: integer
          description: El tamaño que el cliente **dijo** al pedir el presign.
        sizeBytes:
          type:
            - integer
            - 'null'
          description: >
            El tamaño real del objeto, medido al confirmar. Es `null` mientras
            nadie lo haya

            mirado — está aparte de `declaredSizeBytes` precisamente porque
            pueden no coincidir.
        rejectedReason:
          type:
            - string
            - 'null'
          description: >
            Por qué se rechazó, cuando `status` es `rejected`. Si no, `null`.


            Es un **código**, no una frase: hoy `size-mismatch` (lo medido no
            cuadra con lo

            declarado) o `content-mismatch` (los bytes no son del formato que
            dice la

            extensión). El detalle legible va en el mensaje del `422`, no aquí.
          example: content-mismatch
        createdBy:
          type: string
        createdAt:
          type: string
          format: date-time
        confirmedAt:
          type:
            - string
            - 'null'
          format: date-time
      required:
        - uploadId
        - namespace
        - status
        - originalFileName
        - contentType
        - declaredSizeBytes
        - sizeBytes
        - rejectedReason
        - createdBy
        - createdAt
        - confirmedAt
    ErrorResponse:
      type: object
      properties:
        code:
          type: string
          description: Código principal del error (alto nivel).
        message:
          type: string
          description: Mensaje legible para humanos.
        details:
          type: object
          description: >
            Información adicional específica del error.

            Cuando aplique, `details.subcode` provee un identificador estable
            para que el frontend decida qué UI mostrar.
          properties:
            subcode:
              type: string
              description: >-
                Identificador específico del error (p.ej.
                `WORKSPACE_NOT_PROVISIONED`).
            workspaceId:
              type: string
              format: uuid
              description: Identificador de workspace relacionado cuando aplica.
          additionalProperties: true
      required:
        - code
        - message
    UploadRejectedResponse:
      type: object
      description: >
        El `422` de un archivo que se miró y no vale.


        Lleva **`rejectedReason`**, que es lo único comparable sin leer prosa:
        el `message` es

        para humanos y puede cambiar de redacción o de idioma.


        El campo aparece cuando el workspace tiene abierto el interruptor que lo
        publica.

        Mientras esté cerrado, el `422` sale con el mismo `message` pero sin el
        campo, y el

        motivo hay que leerlo con `GET /v1/uploads/{uploadId}`. Cuando el
        interruptor deje de

        existir, `rejectedReason` pasa a ser obligatorio aquí.


        **Con el interruptor cerrado no desaparece sólo `rejectedReason`.** Ese
        carril no pasa

        por el filtro del módulo: responde el manejador por defecto del
        framework, que trae

        `statusCode`, `error` y `message` y **nada más** — tampoco `path` ni
        `timestamp`. Por eso

        los tres únicos campos obligatorios aquí son los que llegan en los dos
        carriles; trata

        `path` y `timestamp` como opcionales mientras el interruptor exista.
      properties:
        statusCode:
          type: integer
          example: 422
        error:
          type: string
          example: Unprocessable Entity
        message:
          type: string
          description: >-
            Detalle legible para humanos. No es contrato; no decidas la UI con
            esto.
          example: 'Upload rejected: the bytes are not a valid pdf.'
        rejectedReason:
          $ref: '#/components/schemas/UploadRejectionReason'
        path:
          type: string
          description: >
            La ruta que se pidió, tal cual llegó. Sólo viaja en el carril con el
            interruptor

            abierto.
        timestamp:
          type: string
          format: date-time
          description: Sólo viaja en el carril con el interruptor abierto.
      required:
        - statusCode
        - error
        - message
    UploadNamespace:
      type: string
      enum:
        - agent-briefs
        - knowledge-sources
      description: >
        Destino del archivo. Es un conjunto cerrado y versionado en el código:
        cada namespace

        trae su propia política de formatos, tamaños y permisos.
    UploadStatus:
      type: string
      enum:
        - pending
        - ready
        - rejected
        - expired
        - discarded
      description: >
        En qué punto del ciclo está el archivo.


        `pending` es la fila que dejó el presign: existe antes de que se suba un
        solo byte, y

        caduca a las 24 h si nadie la confirma. `ready` es el archivo verificado
        —sigue siendo

        operable: se puede reclamar y se puede descartar—. `rejected`, `expired`
        y `discarded`

        son estados de los que ya no se sale.
    UploadRejectionReason:
      type: string
      enum:
        - size-mismatch
        - content-mismatch
      description: >
        Qué comprobación falló al mirar el archivo subido. Nombra la
        comprobación y no la causa,

        porque la causa no se puede afirmar desde el servidor.


        - **`content-mismatch`** — los bytes no son del formato que declaraba la
        extensión. Un
          `.pdf` que no empieza por `%PDF` cae aquí, y suele ser un archivo renombrado o un
          export que no terminó. Volver a subir el mismo archivo da exactamente lo mismo: hay
          que cambiar de archivo.
        - **`size-mismatch`** — lo que mide el objeto en el almacén no es lo que
        se declaró al
          pedir la URL. Es un backstop: el tamaño va **dentro de la URL firmada**, así que el
          propio almacén rechaza un cuerpo de otro tamaño y una transferencia cortada no llega
          a dejar objeto —esa cae en `404`—. Si aparece, lo que se subió no es lo que se
          declaró, y repetirlo no ayuda: hay que pedir una URL nueva con el tamaño real.

        En los dos casos la fila queda `rejected`, que es **terminal**. No
        vuelvas a llamar a

        `confirm` sobre ella: pide un presign nuevo.
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

````