openapi: 3.0.3
info:
  title: Quantian Compliance — API pública v1
  version: "1.0.0"
  description: |
    API servidor-a-servidor para crear y avanzar legajos de apertura de cuenta
    (personas humanas y jurídicas) sin pasar por la pantalla del operador.

    El cliente HTTP es siempre un sistema controlado por su organización,
    autenticado con un token de API. El contrato de campos de `human`/`company`
    es **dinámico por organización**: el bulk POST valida en runtime contra la
    configuración de Campos configurables. Ver la guía "Contrato de campos".
servers:
  - url: https://compliance.quantian.ar/api
    description: Base URL única de la API pública.
tags:
  - name: Accounts
    description: Legajos de apertura de cuenta.
  - name: Documents
    description: Documentos adjuntos a un legajo.
  - name: Hosted Flow
    description: Entrega del cierre del legajo al consumidor final vía magic-link.
  - name: Catalogs
    description: Catálogos globales de referencia.
  - name: Schema
    description: Contrato del payload del bulk POST como JSON Schema.
security:
  - bearerAuth: []
paths:
  /v1/accounts:
    get:
      tags: [Accounts]
      operationId: listAccounts
      summary: Listar legajos
      description: |
        Lista los legajos de su organización, paginados, ordenados por
        `created_at` descendente. **Ability requerida:** `accounts:read`.
      parameters:
        - { name: per_page, in: query, required: false, schema: { type: integer, default: 20, maximum: 100 } }
        - { name: page, in: query, required: false, schema: { type: integer, default: 1 } }
      responses:
        "200":
          description: Página de legajos.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/Account" } }
                  links: { $ref: "#/components/schemas/PaginationLinks" }
                  meta: { $ref: "#/components/schemas/PaginationMeta" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Accounts]
      operationId: createAccount
      summary: Crear legajo (bulk create)
      description: |
        Crea un legajo completo con todas sus
        secciones anidadas en una sola llamada, dentro de una transacción: si
        falla la validación, no se crea nada. **Ability requerida:** `accounts:write`.

        El required-ness de los campos de `human`/`company` lo define la
        configuración de Campos configurables de su organización y se valida en
        runtime — un campo requerido faltante devuelve `422` con la clave en
        dot-notation (ej. `human.tax_id_number`). Ver la guía "Contrato de campos".
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AccountCreate" }
      responses:
        "201":
          description: Legajo creado.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Account" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationError" }
  /v1/accounts/{account}:
    parameters:
      - { name: account, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [Accounts]
      operationId: getAccount
      summary: Detalle de un legajo
      description: "Detalle de un legajo. **Ability requerida:** `accounts:read`."
      responses:
        "200":
          description: El legajo.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Account" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/accounts/{account}/checklist:
    parameters:
      - { name: account, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [Accounts]
      operationId: getAccountChecklist
      summary: Estado de completitud del legajo
      description: |
        Mismo checklist que ve el operador en el backoffice: grupos de campos,
        documentos requeridos, completos/faltantes. La forma sigue al servicio
        interno de checklist y refleja Requerimientos Documentales + Campos
        configurables de su organización. **Ability requerida:** `accounts:read`.
      responses:
        "200":
          description: El checklist.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/accounts/{account}/submit:
    parameters:
      - { name: account, in: path, required: true, schema: { type: string, format: uuid } }
    post:
      tags: [Accounts]
      operationId: submitAccount
      summary: Enviar legajo al circuito de firma/validación
      description: |
        Envía el legajo según la entrega de firma de su flujo (`signature_mode`:
        `email`, `api_link` o `none`, heredado del flujo elegido al crear el
        legajo). **Ability requerida:** `accounts:write`.

        Pre-condiciones: el legajo en `draft` o `in_progress`; titular y
        firmantes con email cargado.

        **Idempotencia:** si el legajo ya está en `pending_signature` o más
        avanzado, devuelve `200` con el estado actual sin re-disparar la firma.
      requestBody:
        required: false
        content:
          application/json:
            schema: { type: object }
      responses:
        "200":
          description: |
            Resultado del envío. `signature.urls` aparece sólo en modo `api_link`;
            `signature.status` sólo en `email`. `investor_profile_renewal` es null
            salvo que se hayan disparado renovaciones de perfil de inversor.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AccountSubmitted" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationError" }
        "502":
          description: "Sólo modo `api_link`: el proveedor de firma electrónica falló. El estado revierte."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
  /v1/accounts/{account}/documents:
    parameters:
      - { name: account, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [Documents]
      operationId: listDocuments
      summary: Listar documentos del legajo
      description: "Documentos subidos al legajo, paginados, ordenados por `created_at` desc. **Ability requerida:** `accounts:read`."
      parameters:
        - { name: per_page, in: query, required: false, schema: { type: integer, default: 50, maximum: 100 } }
        - { name: page, in: query, required: false, schema: { type: integer, default: 1 } }
      responses:
        "200":
          description: Página de documentos.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/Document" } }
                  links: { $ref: "#/components/schemas/PaginationLinks" }
                  meta: { $ref: "#/components/schemas/PaginationMeta" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      tags: [Documents]
      operationId: uploadDocument
      summary: Subir un documento
      description: |
        Sube un documento al legajo. Se valida el MIME declarado **y** los
        magic-bytes del archivo. **Ability requerida:** `accounts:write`.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: "Tipos: pdf, png, jpg, jpeg, gif, bmp, tiff. Máx 50 MB."
                target:
                  type: string
                  enum: [human, company]
                  description: "Si no se pasa, se infiere por el tipo del legajo."
                document_classification_id: { type: string, format: uuid }
                description: { type: string, maxLength: 500 }
                expiration_date: { type: string, format: date }
      responses:
        "201":
          description: El documento creado.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Document" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationError" }
  /v1/accounts/{account}/documents/{document}:
    parameters:
      - { name: account, in: path, required: true, schema: { type: string, format: uuid } }
      - { name: document, in: path, required: true, schema: { type: string, format: uuid } }
    delete:
      tags: [Documents]
      operationId: deleteDocument
      summary: Borrar un documento
      description: |
        Borra el documento y su archivo del storage. **Ability requerida:** `accounts:write`.
        Devuelve `404` si el documento no pertenece al legajo (no se distingue de
        "no existe", para no filtrar la existencia de documentos en otros legajos).
      responses:
        "204": { description: "Borrado, sin cuerpo." }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/accounts/{account}/hosted-flow/link:
    parameters:
      - { name: account, in: path, required: true, schema: { type: string, format: uuid } }
    post:
      tags: [Hosted Flow]
      operationId: createHostedFlowLink
      summary: Emitir magic-link para el consumidor
      description: |
        Emite un magic-link para que el consumidor cierre el legajo en el portal de onboarding.
        **Ability requerida:** `accounts:write`.

        Pre-condiciones: el legajo creado por API (`created_via = "api"`); en
        estado `draft` o `in_progress`; si ya tiene consumidor vinculado, el
        `email` debe coincidir (case-insensitive).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email, maxLength: 255 }
                expires_in_hours: { type: integer, minimum: 1, maximum: 168, default: 24 }
      responses:
        "201":
          description: El magic-link emitido.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      url: { type: string, format: uri }
                      expires_at: { type: string, format: date-time }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationError" }
  /v1/onboarding-flows:
    get:
      tags: [Onboarding Flows]
      operationId: listOnboardingFlows
      summary: Listar los flujos de apertura activos
      description: "Flujos de apertura activos de su empresa. Usá el `slug` como valor de `onboarding_flow` al crear un legajo. **Ability requerida:** `accounts:read`."
      responses:
        "200":
          description: Flujos de apertura activos de su empresa.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        slug: { type: string, example: "apertura-estandar" }
                        name: { type: string, example: "Apertura estándar" }
                        is_default: { type: boolean }
                        requires_liveness: { type: boolean }
                        signature_mode: { type: string, nullable: true, enum: [email, api_link, none], example: null }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /v1/catalogs/{catalog}:
    parameters:
      - name: catalog
        in: path
        required: true
        schema:
          type: string
          enum: [countries, condicion_iva, condicion_ganancias, tipo_societario, cnv_entity_types]
    get:
      tags: [Catalogs]
      operationId: getCatalog
      summary: Listar un catálogo global
      description: "Valores de un catálogo global de referencia. **Ability requerida:** `accounts:read`."
      responses:
        "200":
          description: |
            Los valores del catálogo. La forma de cada item depende del catálogo:
            `countries` → {id, name, iso2}; `condicion_iva` y `condicion_ganancias`
            → {id, name}; `tipo_societario` → {id, sigla, long_name}; `cnv_entity_types`
            → {id, key, label, legal_reference}.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { type: object }, example: [] }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/payload-schema/{accountType}:
    parameters:
      - name: accountType
        in: path
        required: true
        schema: { type: string, enum: [ph, pj] }
    get:
      tags: [Schema]
      operationId: getPayloadSchema
      summary: Contrato del bulk POST como JSON Schema
      description: |
        Devuelve el contrato de campos del bulk POST (`POST /v1/accounts`) de su
        organización como **JSON Schema Draft 2020-12**, uno por tipo de cuenta
        (`ph` o `pj`). Los campos que la organización exige para completar el
        legajo se enumeran en `x-quantian-required-for-completion` (dentro de
        `human`/`company`); el schema no usa `required` a nivel de entidad porque
        el bulk POST acepta payloads parciales (los datos faltantes se completan
        luego en el onboarding); los campos `*_id` referencian catálogos globales
        vía la extensión `x-quantian-catalog`; las dependencias condicionales se
        expresan con `if/then` y se anotan con `x-quantian-depends-on`.

        La respuesta es el JSON Schema crudo (no envuelto en `{data}`), pensado para
        enviarse directamente a un validador y chequear el payload localmente antes de
        enviarlo. **Ability requerida:** `accounts:read`.
      responses:
        "200":
          description: El JSON Schema del payload para el tipo de cuenta.
          content:
            application/json:
              schema:
                type: object
                description: Un documento JSON Schema Draft 2020-12.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "Token de API en el header Authorization: Bearer <token>."
  responses:
    Unauthorized:
      description: Token ausente o inválido.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: El token no tiene la ability requerida.
      content:
        application/json:
          schema:
            type: object
            properties:
              message: { type: string }
              required:
                type: array
                items: { type: string }
                description: "La ability o abilities que el token no posee."
    NotFound:
      description: El recurso no existe o no pertenece a su organización.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    ValidationError:
      description: Validación fallida. Las claves de `errors` usan dot-notation.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error422" }
  schemas:
    Account:
      type: object
      properties:
        id: { type: string, format: uuid }
        type: { type: integer, enum: [1, 2], description: "1 = persona humana, 2 = persona jurídica." }
        type_label: { type: string, enum: [persona_humana, persona_juridica] }
        status: { type: string, description: "draft, in_progress, pending_signature, pending_due_diligence, ..." }
        denomination: { type: string, nullable: true }
        created_via: { type: string, description: "api cuando el legajo se creó por esta API." }
        risk_level: { type: string, nullable: true }
        submitted_at: { type: string, format: date-time, nullable: true }
        opened_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        links:
          type: object
          properties:
            self: { type: string, format: uri }
    Document:
      type: object
      properties:
        id: { type: string, format: uuid }
        documentable_type: { type: string }
        documentable_id: { type: string, format: uuid }
        original_filename: { type: string }
        mime_type: { type: string }
        file_size: { type: integer }
        description: { type: string, nullable: true }
        document_classification_id: { type: string, format: uuid, nullable: true }
        expiration_date: { type: string, format: date-time, nullable: true, description: "Se ingresa como fecha (YYYY-MM-DD) y se devuelve en ISO 8601 a medianoche UTC (ej. 2026-12-31T00:00:00+00:00)." }
        created_at: { type: string, format: date-time }
    AccountSubmitted:
      type: object
      properties:
        data: { $ref: "#/components/schemas/Account" }
        signature:
          type: object
          nullable: true
          properties:
            mode: { type: string, enum: [email, api_link, none] }
            status: { type: string, description: "Sólo en modo email (ej. dispatched)." }
            urls:
              type: array
              description: "Sólo en modo api_link."
              items:
                type: object
                properties:
                  signer_id: { type: string, format: uuid }
                  signer_name: { type: string }
                  url: { type: string, format: uri }
        investor_profile_renewal:
          type: object
          nullable: true
          description: "null si no se disparó nada."
          properties:
            sent: { type: array, items: { type: object }, example: [] }
            already_pending: { type: array, items: { type: object }, example: [] }
    AccountCreate:
      type: object
      required: [type]
      properties:
        type: { type: integer, enum: [1, 2], description: "1 = PH (requiere human), 2 = PJ (requiere company)." }
        denomination: { type: string, maxLength: 255, nullable: true }
        onboarding_flow: { type: string, maxLength: 255, nullable: true, description: "Slug del flujo de apertura (ver GET /v1/onboarding-flows). Si se omite, se usa el flujo por defecto. Si se envía un slug que no corresponde a un flujo activo de su empresa, se rechaza con 422." }
        require_liveness: { type: boolean, deprecated: true, description: "Deprecado e ignorado; la biometría la define el flujo de apertura." }
        human: { $ref: "#/components/schemas/EntityPayload" }
        company: { $ref: "#/components/schemas/EntityPayload" }
        beneficial_ownership:
          type: array
          description: "UBOs (sólo PJ)."
          items: { type: object }
          example: []
        cbus:
          type: array
          description: "Cuentas CBU del titular."
          items: { type: object }
          example: []
        related_humans:
          type: array
          description: "Cotitulares (PH) o apoderados/firmantes (PJ). Cada item tiene la forma de human."
          items: { $ref: "#/components/schemas/EntityPayload" }
    EntityPayload:
      type: object
      description: |
        Campos del titular/empresa. **El required-ness lo define la configuración
        de Campos configurables de su organización y se valida en runtime** — ver
        la guía "Contrato de campos". Además de los campos planos, acepta las
        sub-secciones anidadas listadas abajo.
      properties:
        residential_address: { type: object, description: "Sólo human." }
        work_address: { type: object, description: "Sólo human." }
        spouse: { type: object, description: "Sólo human." }
        fiscal_address: { type: object, description: "Sólo company." }
        real_address: { type: object, description: "Sólo company." }
        fatca: { type: object }
        source_of_funds: { type: object }
        investor_questionnaire: { type: object }
    PaginationLinks:
      type: object
      properties:
        first: { type: string, nullable: true }
        last: { type: string, nullable: true }
        prev: { type: string, nullable: true }
        next: { type: string, nullable: true }
    PaginationMeta:
      type: object
      properties:
        current_page: { type: integer }
        from: { type: integer, nullable: true }
        last_page: { type: integer }
        per_page: { type: integer }
        to: { type: integer, nullable: true }
        total: { type: integer }
    Error:
      type: object
      properties:
        message: { type: string }
    Error422:
      type: object
      properties:
        message: { type: string }
        errors:
          type: object
          additionalProperties:
            type: array
            items: { type: string }
