# Copyright (c) 2026 Sentinex Risk Technologies. All rights reserved.
# Licensed under the Sentinex Risk Commercial License.
#
# Contrato do cliente KYT. Arquivo GERADO por scripts/sync-openapi-public.mjs
# a partir do contrato canônico e do manifesto de operações. Não edite à mão.

openapi: 3.0.3
info:
  title: Sentinex Risk Crypto - API de KYT
  description: |
    Contrato público da integração de KYT (monitoramento de transações) da
    Sentinex Risk Crypto. Ele lista só as operações que a API de produção serve
    a uma integração autenticada por chave de API.

    ## Autenticação

    Toda operação deste contrato usa a chave de API da empresa no header
    `X-API-Key`. A chave identifica a empresa e carrega os escopos; nenhum header
    extra de empresa é necessário. Em produção, o grupo `/vasp` aceita apenas chave
    do ambiente `live`. A chave é criada no painel, na área de desenvolvedores, por
    um administrador da empresa.

    ## Recomendação e decisão

    `POST /vasp/screenings` devolve a recomendação `ALLOW`, `REVIEW`, `HOLD` ou
    `DENY`, com o detalhamento do score e das regras acionadas. A decisão
    operacional é do cliente e é registrada em
    `PATCH /vasp/screenings/{id}/client-decision`.

    ## Convenções da integração

    Campos JSON usam `snake_case`. Enums de recomendação, ação e estado de
    negócio usam caixa alta. Datas e horas usam RFC 3339 em UTC. `amount`
    representa quantidade decimal do ativo; campos `_brl` representam reais,
    não centavos; campos `_ms` representam milissegundos; scores KYT usam 0 a
    100. Listagens `/vasp` usam `limit`, com default 50 e teto 200; alertas
    paginam com `offset` (as leituras de ledger também, fora do contrato do
    cliente KYT), e clientes com o cursor `cursor_before`, devolvido em
    `next_cursor`. Depois da autenticação valem o limite da empresa, 2.000 por
    minuto somando todas as chaves e usuários, e o limite por minuto da API key,
    que usa 60 por minuto quando a credencial não define outro valor e é
    informado por headers. O laudo KYT acrescenta 120 emissões por hora,
    contadas por IP de origem. Rota inexistente sob `/vasp`, com chave válida,
    responde `404` com `error.code` `route_not_found`.

    ## Versão

    `info.version` é a versão deste contrato do cliente. Ela é própria: não acompanha
    a especificação interna nem os SDKs, e só sobe quando este contrato muda. A
    política de versão e o changelog estão em
    https://docs-crypto.sentinexrisk.com/concepts/versionamento-e-changelog/.

    `info.x-snx-congelamento` diz em que fase a linha está. Com `pre-integracao`, ainda
    não há integrador em produção, e uma operação que não serve ao produto KYT pode sair
    do contrato, sempre com a operação nomeada no changelog. Quando o primeiro integrador
    recebe a chave, o valor passa a `congelado desde dd/mm/aaaa`, e daí em diante a linha
    só recebe mudança compatível.

    ## Suporte

    - dev-support@sentinexrisk.com
    - Documentação: https://docs-crypto.sentinexrisk.com
  version: 8.5.1
  x-snx-congelamento: pre-integracao
  termsOfService: https://sentinexrisk.com/termos
  contact:
    name: Sentinex Risk Technologies
    url: https://sentinexrisk.com
    email: dev-support@sentinexrisk.com
  license:
    name: Licença Comercial Sentinex Risk
    url: https://sentinexrisk.com/licenca
servers:
  - url: https://crypto-api.sentinexrisk.com/api/v1
    description: Produção. Os caminhos deste contrato são relativos a /api/v1.
tags:
  - name: VASP - Screening
    description: |
      Decisão KYT em tempo real (POST /vasp/screenings), consulta de
      decisões e feedback 2-way. Endpoint principal da integração.
  - name: VASP - Clientes
    description: |
      Enrollment de clientes finais (PF/PJ) com identificadores
      pseudonimizados (SHA-256) e idempotência via Idempotency-Key.
  - name: VASP - Carteiras
    description: |
      Enrollment de carteiras de clientes finais com screening
      fail-closed (sync ou async).
  - name: VASP - Alertas
    description: Poll de alertas no escopo da empresa (complementa o webhook outbound).
  - name: VASP - Webhooks
    description: Self-service de subscriptions de webhook outbound.
  - name: VASP - Compliance
    description: SARs pendentes (read-only) e relatórios DeCripto da empresa.
paths:
  /vasp/customers:
    post:
      operationId: createVASPCustomer
      summary: Cadastra cliente final do VASP (PF ou PJ)
      description: |
        Enrollment de cliente final. O documento entra em `document` (CPF ou
        CNPJ em texto, que o servidor canoniza e confere) ou em `document_hash`
        (SHA-256 do documento, que continua aceito); envie um dos dois. Se
        vierem os dois e divergirem, a resposta é 422 `DOCUMENT_HASH_MISMATCH`, e
        `document` tem precedência no cálculo do índice.

        O servidor não guarda o documento em claro (LGPD). Com `document`, guarda
        só um índice cego e a cifra do valor. Só com `document_hash`, o servidor
        não tem o documento para cifrar e, por isso, não consegue exibi-lo.
        `legal_name_hash` segue em SHA-256.

        Suporta replay idempotente via header Idempotency-Key. Escopo exigido:
        `vasp:customers:write`.
      tags:
        - VASP - Clientes
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VASPCustomerCreateRequest'
      responses:
        '200':
          description: Replay idempotente (mesma Idempotency-Key + mesmo corpo)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  idempotent_replay:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/VASPCustomer'
        '201':
          description: Cliente criado
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/VASPCustomer'
        '400':
          description: |
            Corpo JSON ilegível (texto em `error`), ou campos inválidos
            (`invalid_customer`, `invalid_client_id`, `invalid_idempotency_key`).
            Respondem `invalid_customer` o corpo sem `document` nem
            `document_hash` e o `document` com dígito verificador inválido.
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/ErroTexto'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegado'
        '404':
          $ref: '#/components/responses/ErroRotaNaoEncontrada'
        '409':
          description: >-
            external_id ou Idempotency-Key já existem (customer_already_exists,
            KYT_IDEMPOTENCY_CONFLICT)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseCoded'
        '413':
          $ref: '#/components/responses/ErroCorpoAcimaDoLimite'
        '422':
          description: |
            `client_id` não identifica cliente desta empresa (`client_not_found`),
            ou `document` e `document_hash` vieram juntos e divergem
            (`DOCUMENT_HASH_MISMATCH`). Nada foi gravado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseCoded'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: |
            API de cadastro ou transação da requisição indisponível; nenhum cliente foi
            criado. As camadas anteriores à operação também respondem 503, nada
            processado: limitador por IP ou da empresa sem contagem (`ErroTexto`), estado da conta
            (`empresa_ACCESS_STATE_UNAVAILABLE`) e limitador da chave
            (`rate_limit_unavailable`) e verificação da chave
            (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
    get:
      operationId: listVASPCustomers
      summary: Lista clientes finais da empresa (paginação por cursor)
      description: |
        Filtros não sensíveis por risk_classification e kyc_tier. Filtros
        fiscais são aceitos apenas no corpo de POST /vasp/customers/search
        para não expor identificadores em URL. A paginação é por cursor, e não
        por offset: `limit` (default 50, teto 200; valor zero ou negativo vale
        50) e `cursor_before` com o `next_cursor` da página anterior.
        `next_cursor` volta vazio quando a página traz menos itens que
        `limit`; com a página cheia ele vem preenchido, e a página seguinte
        pode vir vazia. Esta rota tem rate limit anti-enumeração adicional.
        Escopo exigido: `vasp:customers:read`.
      tags:
        - VASP - Clientes
      security:
        - ApiKeyAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 200
        - name: cursor_before
          in: query
          schema:
            type: string
          description: Cursor opaco retornado em next_cursor da página anterior.
        - name: risk_classification
          in: query
          schema:
            type: string
            enum:
              - LOW
              - MEDIUM
              - HIGH
              - CRITICAL
        - name: kyc_tier
          in: query
          schema:
            type: string
            enum:
              - SIMPLIFIED
              - STANDARD
              - ENHANCED
      responses:
        '200':
          description: Página de clientes
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/VASPCustomer'
                  next_cursor:
                    type: string
                    description: Vazio quando a página trouxe menos itens que `limit`.
        '400':
          description: Filtro ou cursor malformado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseCoded'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegado'
        '404':
          $ref: '#/components/responses/ErroRotaNaoEncontrada'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: >
            API de cadastro de clientes indisponível. As camadas anteriores à operação

            também respondem 503, nada processado: limitador por IP ou da empresa sem contagem

            (`ErroTexto`), estado da conta (`empresa_ACCESS_STATE_UNAVAILABLE`) e limitador da chave
            (`rate_limit_unavailable`) e verificação da chave (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
  /vasp/customers/search:
    post:
      operationId: searchVASPCustomers
      summary: Busca clientes finais por identificador pseudonimizado
      description: |
        Aceita cpf_hash ou cnpj_hash no corpo JSON, nunca em URL. O hash
        SHA-256 sem sal é um identificador pseudonimizado e continua sendo
        tratado como dado pessoal. Escopo exigido: `vasp:customers:read`.
      tags:
        - VASP - Clientes
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                cpf_hash:
                  type: string
                  pattern: ^(sha256:)?[a-fA-F0-9]{64}$
                cnpj_hash:
                  type: string
                  pattern: ^(sha256:)?[a-fA-F0-9]{64}$
                risk_classification:
                  type: string
                  enum:
                    - LOW
                    - MEDIUM
                    - HIGH
                    - CRITICAL
                kyc_tier:
                  type: string
                  enum:
                    - SIMPLIFIED
                    - STANDARD
                    - ENHANCED
                limit:
                  type: integer
                  default: 50
                  maximum: 200
                cursor_before:
                  type: string
      responses:
        '200':
          description: Página de clientes
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/VASPCustomer'
                  next_cursor:
                    type: string
        '400':
          description: Corpo, filtro ou cursor malformado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseCoded'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegado'
        '404':
          $ref: '#/components/responses/ErroRotaNaoEncontrada'
        '413':
          $ref: '#/components/responses/ErroCorpoAcimaDoLimite'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: >
            API de cadastro de clientes indisponível. As camadas anteriores à operação

            também respondem 503, nada processado: limitador por IP ou da empresa sem contagem

            (`ErroTexto`), estado da conta (`empresa_ACCESS_STATE_UNAVAILABLE`) e limitador da chave
            (`rate_limit_unavailable`) e verificação da chave (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
  /vasp/customers/{id}:
    get:
      operationId: getVASPCustomer
      summary: Busca cliente final por id
      description: |
        O id do cliente é um UUID, o mesmo devolvido por POST /vasp/customers.
        Id que não é UUID não identifica cliente nenhum e responde 404.
        Escopo exigido: `vasp:customers:read`.
      tags:
        - VASP - Clientes
      security:
        - ApiKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            maxLength: 80
          example: 3f2a1bc0-9d8e-47f6-a5b4-c3d2e1f00a9b
      responses:
        '200':
          description: Cliente da empresa
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/VASPCustomer'
        '400':
          description: |
            `id` vazio ou com mais de 80 caracteres (`invalid_customer_id`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseCoded'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegado'
        '404':
          description: |
            Cliente inexistente nesta empresa, inclusive quando o `id` não é UUID (texto em
            `error`). Com chave válida, `route_not_found` diz que o caminho ou o método
            não corresponde a operação servida (`ErrorResponseCoded`).
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/ErroTexto'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: >
            API de cadastro de clientes indisponível. As camadas anteriores à operação

            também respondem 503, nada processado: limitador por IP ou da empresa sem contagem

            (`ErroTexto`), estado da conta (`empresa_ACCESS_STATE_UNAVAILABLE`) e limitador da chave
            (`rate_limit_unavailable`) e verificação da chave (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
  /vasp/customers/batch:
    post:
      operationId: createVASPCustomersBatch
      summary: Cadastra clientes finais do VASP em lote (até 1.000 por chamada)
      description: |
        Matrícula em lote de clientes finais (PF ou PJ). Cada item de `customers`
        tem o formato do corpo de `POST /vasp/customers`, inclusive `document`
        (CPF ou CNPJ em texto, que o servidor canoniza) e `document_hash`, que
        continua aceito. Se `document` e `document_hash` vierem juntos no mesmo
        item e divergirem, o item é recusado com `DOCUMENT_HASH_MISMATCH`. O
        servidor não guarda o documento em claro: com `document`, guarda só um
        índice cego e a cifra do valor; só com `document_hash`, não tem o
        documento para cifrar e não consegue exibi-lo.

        Cada item é processado por conta própria: um item recusado não impede os
        outros. O resultado de cada item é `CREATED` (titular novo), `UPDATED`
        (o `external_id` já existia na empresa, e o titular é atualizado pelo
        próprio `external_id`; campo ausente no item não apaga o valor anterior)
        ou `REJECTED`, com o motivo de validação. `type` e `client_id` do titular
        não mudam por esta via: item que traga `type` diferente do titular
        existente, ou `client_id` diferente do vínculo existente (ou um
        `client_id` para titular que não tinha vínculo), volta `REJECTED` com
        `invalid_customer`. O resultado não revela se o documento já existe sob
        outro `external_id`: o mesmo documento sob `external_id` diferentes é
        aceito, sem aviso e sem recusa.

        A resposta é `207` sempre que o lote foi lido e autorizado, mesmo que
        todos os itens tenham sido recusados. O resultado de cada item vem em
        `results`, na ordem do envio, e os totais em `summary`.

        Limites. Além do limite da chave de API, esta operação tem limites
        próprios: por lote (de 1 a 1.000 titulares, e o corpo inteiro respeita o
        teto de 1 MiB da API; se o lote não couber, divida-o), por ator (a chave
        de API que envia o lote) e por empresa, e um orçamento de itens por janela
        de tempo, também por empresa. Ao esgotar qualquer um deles, a resposta é
        429, com `Retry-After`, e nada é processado.

        O mesmo `external_id` mais de uma vez no lote recusa o lote inteiro
        (422 `DUPLICATE_EXTERNAL_ID`), sem gravar nada. Corpo ilegível, campo
        desconhecido na raiz do corpo e `customers` fora de 1 a 1.000 itens
        também respondem 422, com `INVALID_BATCH`. `Idempotency-Key` é
        obrigatória: a repetição com a mesma chave e o mesmo corpo devolve o
        resultado original, sem gravar de novo (`idempotent_replay: true`), e a
        mesma chave com corpo diferente responde 409.

        Matrícula básica de KYT, autenticada por API key com o escopo
        `vasp:customers:write`. Funciona com o cruzamento desligado e não exige
        o adicional de cruzamento de dados do titular.
      tags:
        - VASP - Clientes
      security:
        - ApiKeyAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema:
            type: string
            minLength: 1
            maxLength: 255
          example: lote-clientes-01
          description: >-
            Obrigatória. Mesma chave + corpo igual = replay; corpo diferente = 409. O prefixo
            `snx-derived:` é RESERVADO ao servidor em TODAS as rotas desta API: uma
            `Idempotency-Key` que comece com ele é recusada com 400.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VASPCustomerBatchRequest'
            example:
              customers:
                - external_id: cliente-0001
                  type: PF
                  document: 123.456.789-01
                - external_id: cliente-0002
                  type: PJ
                  document: 11.222.333/0001-82
                  trade_name: Empresa Exemplo
      responses:
        '207':
          description: |
            Lote lido e autorizado. O status vale para o lote; o resultado de cada
            item, criado, atualizado ou recusado, está em `results`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VASPCustomerBatchResponse'
              example:
                success: true
                idempotent_replay: false
                summary:
                  total: 3
                  created: 1
                  updated: 1
                  rejected: 1
                results:
                  - index: 0
                    external_id: cliente-0001
                    status: CREATED
                    data:
                      external_id: cliente-0001
                      type: PF
                  - index: 1
                    external_id: cliente-0002
                    status: UPDATED
                    data:
                      external_id: cliente-0002
                      type: PJ
                  - index: 2
                    status: REJECTED
                    error:
                      code: invalid_customer
                      message: Item recusado por validação.
        '400':
          description: |
            `Idempotency-Key` ausente ou inválida (`invalid_idempotency_key`):
            vazia depois do trim, com mais de 255 bytes, com caractere de
            controle ou com o prefixo reservado `snx-derived:`. Corpo ilegível e
            `customers` fora de 1 a 1.000 itens não caem aqui (são 422
            `INVALID_BATCH`), e item inválido volta recusado em `results`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseCoded'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegado'
        '404':
          $ref: '#/components/responses/ErroRotaNaoEncontrada'
        '409':
          description: Idempotency-Key reusada com corpo diferente (KYT_IDEMPOTENCY_CONFLICT)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseCoded'
        '413':
          description: |
            Corpo acima de 1 MiB. O escudo de borda recusa antes da
            autenticação, com `BODY_TOO_LARGE`; a própria operação repete o teto
            com `BATCH_PAYLOAD_TOO_LARGE`. Nada foi gravado. Lote com mais de
            1.000 titulares dentro do teto de tamanho é 422 `INVALID_BATCH`.
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTextoComCodigo'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
        '422':
          description: |
            Lote recusado por inteiro, sem gravar nada: `INVALID_BATCH` (corpo
            ilegível, campo desconhecido na raiz, ou `customers` fora de 1 a
            1.000 itens) ou `DUPLICATE_EXTERNAL_ID` (o lote repete um
            `external_id`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseCoded'
        '429':
          description: |
            Limite de requisições atingido; nada foi processado. Valem as formas
            de `ErroLimiteDeRequisicoes` (limite da chave, limite global e escudo
            de borda) e os limites próprios do lote: por ator
            (`BATCH_RATE_LIMIT_ACTOR`) e por empresa (`BATCH_RATE_LIMIT_TENANT`),
            ambos por hora e com `Retry-After: 3600` (`ErroLimiteDoLote`), e o
            orçamento de itens da empresa (`BATCH_ITEM_BUDGET_EXCEEDED`,
            `ErrorResponseCoded`, também com `Retry-After: 3600`). Espere o
            `Retry-After` antes de repetir.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            X-RateLimit-Limit:
              description: Teto da janela do limitador que respondeu, quando ele informa.
              schema:
                type: integer
            X-RateLimit-Remaining:
              description: Requisições restantes na janela, quando o limitador informa.
              schema:
                type: integer
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/ErroLimiteDoLote'
                  - $ref: '#/components/schemas/ErroLimite'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: |
            Matrícula em lote indisponível: chaves da empresa, proteção do lote,
            recibo do lote ou banco (`ErroTexto`); nenhum item do lote ficou
            gravado. Sem contador de limite de lote, a rota recusa em vez de
            deixar passar (`BATCH_RATE_LIMIT_UNAVAILABLE`). As camadas anteriores
            à operação também respondem 503, nada processado: limitador por IP ou
            da empresa sem contagem (`ErroTexto`), estado da conta
            (`empresa_ACCESS_STATE_UNAVAILABLE`), limitador da chave
            (`rate_limit_unavailable`) e verificação da chave
            (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/ErroTextoComCodigo'
  /vasp/wallets:
    post:
      operationId: enrollVASPWallet
      summary: Cadastra carteira de cliente final com screening fail-closed
      description: |
        Vincula uma carteira a um cliente já cadastrado. `mode=sync`
        (default) responde 201 com o resultado do screening; `mode=async`
        responde 202 com status PENDING (fail-closed até o resultado).
        Carteira já conhecida da empresa responde 200 com o resultado
        armazenado. Suporta Idempotency-Key. Escopo exigido:
        `vasp:wallets:write`.

        O endereço é validado pela rede (checksum EIP-55, bech32, base58 e
        afins) e gravado só cifrado, com o HMAC da empresa para igualdade; o
        endereço em claro e o prefixo não são armazenados. A busca da carteira
        existente cobre também as carteiras cadastradas antes da cifra, que são
        convertidas na mesma linha, sem duplicar. Carteira de cliente nunca é
        anexada a carteira institucional do mesmo endereço (409
        wallet_is_institutional). `custody_type` é opcional; ausente fica
        registrado como não informado.
      tags:
        - VASP - Carteiras
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - name: mode
          in: query
          schema:
            type: string
            enum:
              - sync
              - async
            default: sync
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VASPWalletEnrollRequest'
      responses:
        '200':
          description: Carteira já existente vinculada, ou replay idempotente
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  idempotent_replay:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/VASPWalletEnrollment'
        '201':
          description: Carteira nova cadastrada (mode=sync)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/VASPWalletEnrollment'
        '202':
          description: Carteira aceita para screening assíncrono (mode=async)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/VASPWalletEnrollment'
        '400':
          description: |
            Corpo JSON ilegível (texto em `error`), ou campos, `mode` ou `Idempotency-Key`
            inválidos (`invalid_wallet`, `invalid_mode`, `invalid_idempotency_key`).
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/ErroTexto'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegado'
        '404':
          description: |
            Cliente da carteira inexistente nesta empresa (texto em `error`). Com chave
            válida, `route_not_found` diz que o caminho ou o método não corresponde a
            operação servida (`ErrorResponseCoded`).
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/ErroTexto'
        '409':
          description: |
            Endereço, external_id ou Idempotency-Key já existem
            (wallet_already_exists, KYT_IDEMPOTENCY_CONFLICT); o endereço é de
            carteira institucional da empresa (wallet_is_institutional); ou o
            endereço casa mais de uma carteira antiga (wallet_duplicate_legacy,
            com `error.wallet_ids`: desative as duplicatas por DELETE e reenvie).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VASPWalletErrorResponse'
        '413':
          $ref: '#/components/responses/ErroCorpoAcimaDoLimite'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: |
            API de cadastro indisponível ou cifra do endereço indisponível; nenhuma
            carteira foi gravada. As camadas anteriores à operação também respondem 503,
            nada processado: limitador por IP ou da empresa sem contagem (`ErroTexto`), estado da
            conta (`empresa_ACCESS_STATE_UNAVAILABLE`) e limitador da chave
            (`rate_limit_unavailable`) e verificação da chave
            (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
  /vasp/wallets/{id}:
    get:
      operationId: getVASPWalletEnrollment
      summary: Consulta o enrollment e o status de screening de uma carteira
      description: 'Escopo exigido: `vasp:read`.'
      tags:
        - VASP - Carteiras
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: Enrollment da carteira
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/VASPWalletEnrollment'
        '400':
          description: |
            `id` que não é UUID (`invalid_wallet_id`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseCoded'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegado'
        '404':
          description: |
            Carteira inexistente nesta empresa (texto em `error`). Com chave válida,
            `route_not_found` diz que o caminho ou o método não corresponde a operação
            servida (`ErrorResponseCoded`).
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/ErroTexto'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: >
            API de cadastro de carteiras indisponível. As camadas anteriores à operação

            também respondem 503, nada processado: limitador por IP ou da empresa sem contagem

            (`ErroTexto`), estado da conta (`empresa_ACCESS_STATE_UNAVAILABLE`) e limitador da chave
            (`rate_limit_unavailable`) e verificação da chave (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
    delete:
      operationId: deactivateVASPWallet
      summary: Desativa a carteira (desativação lógica, o wallet_id continua válido)
      description: |
        Desativação LÓGICA: a linha fica, o `wallet_id` continua válido no GET
        (com `status: deactivated`), e alertas, casos, SAR e transações antigos
        continuam apontando para ela. A carteira sai do monitoramento contínuo
        e da coleta on-chain. Recusa com 409 `wallet_has_open_process`, com
        `error.case_ids` e `error.sar_ids`, quando a carteira está ligada a caso
        aberto ou a SAR não enviado, pelos alertas e transações da carteira.
        A segunda chamada devolve 200 sem nova escrita. Emite o evento
        `wallet.deactivated`. Para voltar a monitorar, use PATCH com
        `{"status":"active"}`: o `wallet_id` é o mesmo. Escopo exigido:
        `vasp:wallets:write`.
      tags:
        - VASP - Carteiras
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: Carteira desativada (ou já estava)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/VASPWalletEnrollment'
        '400':
          description: |
            `id` que não é UUID (`invalid_wallet_id`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseCoded'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegado'
        '404':
          description: |
            Carteira inexistente nesta empresa (texto em `error`). Com chave válida,
            `route_not_found` diz que o caminho ou o método não corresponde a operação
            servida (`ErrorResponseCoded`).
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/ErroTexto'
        '409':
          description: |
            Caso aberto ou SAR não enviado (wallet_has_open_process, com
            `error.case_ids` e `error.sar_ids`), ou carteira institucional, que
            tem rota própria de administração (wallet_is_institutional).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VASPWalletErrorResponse'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: >
            Ciclo de vida de carteira, verificação de processos ou trilha de auditoria

            indisponível; nada foi alterado. As camadas anteriores à operação também

            respondem 503, nada processado: limitador por IP ou da empresa sem contagem

            (`ErroTexto`), estado da conta (`empresa_ACCESS_STATE_UNAVAILABLE`) e limitador da chave
            (`rate_limit_unavailable`) e verificação da chave (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
    patch:
      operationId: updateVASPWallet
      summary: Reativa a carteira, reenvia o endereço ou declara a custódia
      description: |
        Até três alterações na mesma chamada, gravando só o que muda:
        `status: active` reativa a carteira desativada mantendo o MESMO
        `wallet_id` e emite `wallet.linked` de novo; `address` com
        `blockchain` reenvia o endereço de carteira cadastrada antes da
        cifra, que passa a ser gravado cifrado (422 `address_mismatch` se o
        endereço não é o desta carteira); `custody_type` declara a custódia.
        A chamada repetida não grava nada, nem trilha nem evento. 409
        `wallet_address_active_elsewhere`, com `error.wallet_id`, quando
        outra carteira ativa da empresa passou a ter o mesmo endereço enquanto
        esta estava desativada. Escopo exigido: `vasp:wallets:write`.
      tags:
        - VASP - Carteiras
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ResourceID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VASPWalletPatchRequest'
      responses:
        '200':
          description: Estado atual da carteira
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/VASPWalletEnrollment'
        '400':
          description: |
            Corpo JSON ilegível (texto em `error`), ou `id` e campos inválidos
            (`invalid_wallet_id`, `invalid_wallet`).
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/ErroTexto'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegado'
        '404':
          description: |
            Carteira inexistente nesta empresa (texto em `error`). Com chave válida,
            `route_not_found` diz que o caminho ou o método não corresponde a operação
            servida (`ErrorResponseCoded`).
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/ErroTexto'
        '409':
          description: |
            Outra carteira ativa com o mesmo endereço
            (wallet_address_active_elsewhere, com `error.wallet_id`), endereço
            de outra carteira ativa (wallet_duplicate_legacy) ou carteira
            institucional (wallet_is_institutional).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VASPWalletErrorResponse'
        '413':
          $ref: '#/components/responses/ErroCorpoAcimaDoLimite'
        '422':
          description: O endereço enviado não é o desta carteira (address_mismatch)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponseCoded'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: |
            Ciclo de vida de carteira, cifra ou trilha de auditoria indisponível; nada foi
            alterado. As camadas anteriores à operação também respondem 503, nada
            processado: limitador por IP ou da empresa sem contagem (`ErroTexto`), estado da conta
            (`empresa_ACCESS_STATE_UNAVAILABLE`) e limitador da chave
            (`rate_limit_unavailable`) e verificação da chave
            (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
  /vasp/screenings:
    post:
      operationId: screenTransaction
      summary: Recomendação de risco para transação em tempo real (KYT)
      description: |
        Submete uma transação pré ou pós-broadcast e recebe a recomendação
        síncrona `ALLOW`, `REVIEW`, `HOLD` ou `DENY`, com score de risco,
        sanções, regras disparadas e sinais internos de possível aplicabilidade.
        `DENY` somente com base em sanção (contraparte em lista de sanções ou
        com categoria de sanção) ou por regra de bloqueio que a própria empresa
        configurou no painel. Os demais riscos (misturador, darknet, valor,
        velocidade e outros) saem `REVIEW`, com `risk_level` preservado e o
        motivo do rebaixamento em `reasons`. Exceção: quando a empresa escolhe,
        no painel, bloquear automaticamente a proibição legal do misturador
        (Res. BCB nº 520/2025, art. 90, III, e parágrafo único), a contraparte
        misturador sai `DENY` com o motivo "política do cliente: proibição
        legal misturador" em `reasons`. Sem essa escolha, que por padrão fica
        desligada, ela segue `REVIEW` com o rótulo da proibição.
        A recomendação é análise auxiliar. A decisão operacional e o desfecho de
        suspeição pertencem ao cliente e são registrados em `client_decision`.
        Os campos legados em `obligations` não confirmam obrigação, envio ou
        homologação de Travel Rule, COAF ou DeCripto.
        Evidência necessária indisponível ou incompleta (contraparte não
        triável, provedor indisponível, timeout ou resposta incompleta) conclui
        a análise em `HOLD`: HTTP 200, `fail_closed: true` e o motivo em
        `reasons`, com a orientação de reprocessar com a mesma
        Idempotency-Key. `HOLD` é retenção temporária, não bloqueio, e nunca
        vira `ALLOW`; `DENY` por sanção ou regra do cliente prevalece sobre ele.
        Sem resultado do motor, ou com falha de persistência, auditoria ou
        infraestrutura, a rota responde 503 e nada é registrado nem cobrado.
        Replay idempotente via header Idempotency-Key.

        `customer_id` (opcional) identifica a matrícula do titular na empresa
        autenticado: é o `id` UUID devolvido por `POST /vasp/customers`, com a
        mesma semântica do `customer_id` de `POST /vasp/wallets`. Quando
        enviado, toda resposta de sucesso inclui `customer_id` e
        `holder_resolution`, mesmo com o cruzamento desligado. `RESOLVED`
        informa que a matrícula pertence aa empresa; não afirma enriquecimento
        nem triagem negativa. `NOT_FOUND` cobre UUID desconhecido ou de outro
        empresa, sem alterar score, recomendação ou cobrança, e as regras que
        dependem do titular ficam fora da avaliação. Com o cruzamento
        desligado, nenhuma fonte declarada ou de KYC é consultada; somente as
        fontes autorizadas pelo contrato podem contribuir para a análise. Os
        sinais cruzados seguem a regra de `DENY` desta operação: só sanção, ou
        a política de bloqueio da própria empresa, bloqueia. Sem `customer_id`,
        `holder_resolution` e `customer_id` não vêm na resposta.

        `customer_id` inválido (vazio, nulo, UUID zero, fora do formato
        canônico ou de outro tipo) responde 422 `KYT_CUSTOMER_ID_INVALID`, e a
        análise não roda. Falha de contrato ou de fonte do titular responde 503
        `KYT_HOLDER_UNAVAILABLE` antes da análise, sem registro nem cobrança. A
        repetição idempotente conserva a resolução registrada no snapshot
        original; snapshot gravado antes desta resolução, cuja requisição
        trazia `customer_id`, responde 500 `KYT_SNAPSHOT_INVALID` e não
        reexecuta a análise. Dado não informado nunca vira "não".
        `customer_ref` não muda: continua sendo a referência opaca do seu lado.

        Com `customer_ref` informado, quando a leitura da identidade (chave de
        pseudonimização da referência) está indisponível, a resposta é 503
        `KYT_IDENTITY_UNAVAILABLE`, com `data.recommendation` `HOLD` e
        `data.fail_closed` `true`: nenhuma análise é criada, nenhuma regra é
        avaliada e nada é cobrado. A decisão continua pertencendo ao cliente.

        A sanção é medida na data da operação. Operação posterior ou igual à
        publicação vigente da lista é decidida pela versão publicada,
        informada em `sanctions.coverage`. Operação anterior à publicação
        depende do histórico oficial de designações e, sem ele, retorna
        `HOLD` com `fail_closed` verdadeiro. Fonte obrigatória vencida, não
        publicada ou não atestada também retorna `HOLD`, nunca `ALLOW`.

        Nesta versão, exposição on-chain (exposures, graph e typologies) e
        blocklist de rede sem base de golpes carregada não são analisadas. A
        base atual cobre ETH e as redes EVM que a compartilham (BASE, ARB, BSC,
        MATIC, AVAX, OP e FTM). Essas dimensões saem
        em `not_covered`, no topo da resposta, nunca dentro de `coverage`.
        `ALLOW` com entradas em `not_covered` significa que elas não foram
        analisadas. Triagens gravadas antes desta versão não trazem o campo.
      tags:
        - VASP - Screening
      security:
        - ApiKeyAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            maxLength: 255
          description: >-
            Mesma chave + corpo igual = replay; corpo diferente = 409. Opcional: quando ausente e
            `client_transaction_id` presente, o servidor DERIVA a chave de (rota, corpo, janela), e
            a repetição idêntica vira replay em vez de segundo screening cobrado. O alcance da
            derivação é de no mínimo 10 e no máximo 20 minutos, porque a consulta prova o bucket
            corrente e o anterior. Sem `client_transaction_id` não há deduplicação possível e duas
            chamadas iguais continuam sendo duas operações. O prefixo `snx-derived:` é RESERVADO ao
            servidor em TODAS as rotas desta API: uma `Idempotency-Key` que comece com ele é
            recusada com 400.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - asset
              properties:
                client_transaction_id:
                  type: string
                customer_ref:
                  type: string
                  maxLength: 512
                  description: >-
                    Referência opaca do cliente. Ocorrência inequívoca de CPF, CNPJ, e-mail ou
                    telefone é substituída por hash determinístico antes de persistir, alertar ou
                    notificar.
                customer_id:
                  type: string
                  format: uuid
                  nullable: false
                  description: |
                    Opcional. UUID não nulo do titular matriculado: o `id` devolvido
                    por `POST /vasp/customers`, com a mesma semântica do
                    `customer_id` de `POST /vasp/wallets`. Omitir quando não houver
                    titular. Valor vazio, `null`, UUID zero, fora do formato
                    canônico de 36 caracteres ou de outro tipo responde 422
                    `KYT_CUSTOMER_ID_INVALID`. UUID bem formado que não identifica
                    titular desta empresa não muda a decisão: a resposta traz
                    `holder_resolution` `NOT_FOUND`. `customer_ref` continua como
                    está.
                  example: 00000000-0000-4000-8000-000000000001
                direction:
                  type: string
                  enum:
                    - DEPOSIT
                    - WITHDRAWAL
                    - INTERNAL
                asset:
                  type: string
                  example: USDT
                chain:
                  type: string
                  example: tron
                  description: >-
                    Rede on-chain. OPCIONAL nesta rota, e continua opcional: o screening não grava a
                    transação em histórico consultado por rede, então a ausência da chain não torna
                    nada ilegível depois. Ela é, porém, necessária para triar a contraparte: sem
                    `chain`, nem sanções nem blocklist consultam `counterparty_address`, e a decisão
                    cai no fail-closed desta rota, voltando como `HOLD` com `fail_closed: true` e a
                    razão declarada em `reasons`. Para consultar a contraparte nos dois caminhos,
                    envie `chain` sempre que enviar `counterparty_address`. A rota irmã POST
                    /transactions/analyze, que GRAVA a transação, é mais estrita: lá `blockchain` é
                    obrigatória junto de qualquer endereço on-chain e a ausência devolve 400.
                counterparty_address:
                  type: string
                tx_hash:
                  type: string
                  description: vazio = intenção pré-broadcast
                operation_at:
                  type: string
                  format: date-time
                  description: |
                    Opcional. Data e hora da operação, em RFC 3339. É o instante em
                    que a sanção da contraparte é medida contra o intervalo de
                    designação da lista. A ausência NUNCA responde 400, nem com
                    `tx_hash` preenchido: sem este campo o servidor usa a data do
                    bloco da transação, quando já a mediu, e senão o instante em
                    que recebeu a requisição. A origem da data usada volta em
                    `operation_time_source`. Valor que não é data RFC 3339
                    responde 400 `KYT_REQUEST_INVALID`. Esta data não atesta
                    obrigação legal e não altera a decisão final, que
                    permanece do cliente.
                amount:
                  type: number
                  format: double
                  nullable: true
                  minimum: 0
                  description: >-
                    Quantidade do ativo indicado em `asset`, na unidade decimal nativa desse ativo.
                    A omissão (ou `null`) impede a valoração no servidor e resulta em `HOLD`
                    temporário. Para `asset` BRL o valor em reais é o próprio `amount`, inclusive
                    zero informado. Valor negativo responde 400 `KYT_AMOUNT_INVALID`.
                  example: 1500.25
                fiat_value_brl:
                  type: number
                  format: double
                  nullable: true
                  description: >-
                    Valor em reais declarado pelo integrador, não em centavos. É apenas preservado e
                    devolvido em `declared_fiat_value_brl`: não substitui a cotação do servidor e
                    não tem efeito decisório. Valor não finito responde 400
                    `KYT_FIAT_VALUE_INVALID`.
                  example: 7500.5
                counterparty_vasp:
                  type: string
                travel_rule:
                  nullable: true
                  oneOf:
                    - type: string
                      enum:
                        - UNKNOWN
                        - REQUIRED
                        - NOT_REQUIRED
                    - type: boolean
                  description: >-
                    Estado informado de Travel Rule. Os booleanos `true` e `false` correspondem a
                    `REQUIRED` e `NOT_REQUIRED`. Omissão ou `null` corresponde a `UNKNOWN`, sem
                    `HOLD` por essa ausência. Outro valor responde 400 `KYT_REQUEST_INVALID`.
      responses:
        '200':
          description: Recomendação de screening e estado da decisão do cliente
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  idempotent_replay:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/VASPScreeningDecision'
        '400':
          description: |
            Corpo, campo ou `Idempotency-Key` inválida (`KYT_REQUEST_INVALID`,
            `KYT_ASSET_REQUIRED`, `KYT_AMOUNT_INVALID`, `KYT_IDEMPOTENCY_INVALID` e os
            demais `KYT_*_INVALID`), ou `operation_at` mais de 5 minutos adiante do
            relógio do servidor (`KYT_OPERATION_AT_IN_FUTURE`), recusado antes de
            qualquer análise, persistência ou cobrança. Até 5 minutos é aceito como
            folga de relógio; a tolerância é parâmetro operacional, não prazo de norma.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KYTPublicErrorResponse'
              examples:
                operation_at_no_futuro:
                  summary: operation_at além da tolerância de 5 minutos
                  value:
                    success: false
                    error:
                      code: KYT_OPERATION_AT_IN_FUTURE
                      http_status: 400
                      message: operation_at está no futuro além da tolerância de 5 minutos.
                      action: Envie a data real da operação (RFC3339) ou omita o campo.
                      retriable: false
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegadoModuloKYT'
        '404':
          $ref: '#/components/responses/ErroRotaNaoEncontrada'
        '409':
          description: |
            `Idempotency-Key` reusada com corpo diferente (`KYT_IDEMPOTENCY_CONFLICT`) ou
            análise idêntica em processamento (`KYT_IDEMPOTENCY_IN_PROGRESS`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KYTPublicErrorResponse'
        '413':
          $ref: '#/components/responses/ErroCorpoAcimaDoLimite'
        '422':
          description: >
            Transação que o motor não consegue analisar com os dados enviados

            (`KYT_INPUT_UNSCREENABLE`), ou `customer_id` inválido

            (`KYT_CUSTOMER_ID_INVALID`: vazio, nulo, UUID zero, fora do formato

            canônico ou de outro tipo); nos dois casos a análise não roda. No sandbox do
            desenvolvedor

            (`sandbox.api.crypto.sentinexrisk.com`), também endereço de contraparte

            que não é uma das fixtures do catálogo `sandbox-fixtures-v1`

            (`SANDBOX_FIXTURE_NOT_SUPPORTED`, no mesmo envelope `KYTPublicErrorResponse`,

            com `action` apontando para `/api/v1/sandbox/fixtures`): nenhuma consulta

            de DNS ou de provider é feita nem cobrança. A produção nunca emite este

            código.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KYTPublicErrorResponse'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          description: |
            Falha inesperada (`ErroTexto`) ou análise gravada que não pôde ser lida na
            repetição (`KYT_SNAPSHOT_INVALID`).
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/KYTPublicErrorResponse'
        '503':
          description: >
            Evidência essencial, persistência, auditoria, medição ou motor indisponível

            (`KYT_*_UNAVAILABLE`, incluindo `KYT_HOLDER_UNAVAILABLE` quando a

            resolução ou a evidência do titular falha), ou leitura do módulo

            contratado indisponível (`MODULE_STORE_UNAVAILABLE`). A identidade

            indisponível (`KYT_IDENTITY_UNAVAILABLE`) usa o envelope público de

            erro acrescido de `data.recommendation` `HOLD` e `data.fail_closed`

            `true`, sem criar análise, avaliar regras ou registrar consumo. As

            camadas anteriores à operação também

            respondem 503, nada processado: limitador por IP ou da empresa sem contagem

            (`ErroTexto`), estado da conta (`empresa_ACCESS_STATE_UNAVAILABLE`) e limitador da chave
            (`rate_limit_unavailable`) e verificação da chave (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/ErroTextoComCodigo'
                  - $ref: '#/components/schemas/KYTPublicErrorResponse'
                  - $ref: '#/components/schemas/KYTIdentityUnavailableResponse'
  /vasp/screenings/{id}:
    get:
      operationId: getScreening
      summary: Busca uma recomendação de screening anterior
      tags:
        - VASP - Screening
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: Recomendação de screening e decisão registrada pelo cliente
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VASPScreeningGetResponse'
        '400':
          description: |
            `id` que não é UUID (`KYT_SCREENING_ID_INVALID`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KYTPublicErrorResponse'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegado'
        '404':
          description: |
            Análise inexistente nesta empresa (`KYT_SCREENING_NOT_FOUND`). Com chave
            válida, `route_not_found` diz que o caminho ou o método não corresponde a
            operação servida (`ErrorResponseCoded`).
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/KYTPublicErrorResponse'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          description: |
            Falha inesperada (`ErroTexto`) ou análise gravada que não pôde ser lida
            (`KYT_SNAPSHOT_INVALID`).
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/KYTPublicErrorResponse'
        '503':
          description: >
            Análise indisponível para consulta (`KYT_SERVICE_UNAVAILABLE` ou

            `KYT_PERSISTENCE_UNAVAILABLE`). As camadas anteriores à operação também

            respondem 503, nada processado: limitador por IP ou da empresa sem contagem

            (`ErroTexto`), estado da conta (`empresa_ACCESS_STATE_UNAVAILABLE`) e limitador da chave
            (`rate_limit_unavailable`) e verificação da chave (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/KYTPublicErrorResponse'
  /vasp/screenings/{id}/decision:
    patch:
      operationId: submitScreeningDecision
      summary: Registra a decisão do cliente pelo alias legado
      description: |
        Alias de compatibilidade para `/vasp/screenings/{id}/client-decision`.
        Aceita temporariamente `resolution` e `reason`, conforme o mapa publicado
        na documentação de API. A resposta usa somente `client_decision`.
      deprecated: true
      tags:
        - VASP - Screening
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ResourceID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - resolution
              properties:
                resolution:
                  type: string
                  enum:
                    - CLEARED
                    - CONFIRMED_FRAUD
                    - FALSE_POSITIVE
                    - ESCALATED_COAF
                reason:
                  type: string
                  description: Justificativa textual opcional.
      responses:
        '200':
          description: Desfecho registrado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KYTClientDecisionResponse'
        '400':
          description: |
            Identificador, corpo ou combinação de decisão inválida
            (`KYT_SCREENING_ID_INVALID`, `KYT_REQUEST_INVALID`,
            `KYT_CLIENT_DECISION_INVALID`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KYTPublicErrorResponse'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegadoModuloKYT'
        '404':
          description: |
            Análise inexistente nesta empresa (`KYT_SCREENING_NOT_FOUND`). Com chave
            válida, `route_not_found` diz que o caminho ou o método não corresponde a
            operação servida (`ErrorResponseCoded`).
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/KYTPublicErrorResponse'
        '409':
          description: |
            Decisão do cliente já registrada para esta análise
            (`KYT_CLIENT_DECISION_ALREADY_RECORDED`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KYTPublicErrorResponse'
        '413':
          $ref: '#/components/responses/ErroCorpoAcimaDoLimite'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: >
            Persistência, auditoria ou transação da requisição indisponível

            (`KYT_PERSISTENCE_UNAVAILABLE`, `KYT_AUDIT_UNAVAILABLE`, ou texto em `error`

            com `Retry-After: 1` quando a transação conflitou), ou leitura do módulo

            contratado indisponível (`MODULE_STORE_UNAVAILABLE`). As camadas anteriores à

            operação também respondem 503, nada processado: limitador por IP ou da empresa sem

            contagem (`ErroTexto`), estado da conta (`empresa_ACCESS_STATE_UNAVAILABLE`) e

            limitador da chave (`rate_limit_unavailable`) e verificação da chave
            (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/ErroTextoComCodigo'
                  - $ref: '#/components/schemas/KYTPublicErrorResponse'
  /vasp/screenings/{id}/client-decision:
    patch:
      operationId: submitScreeningClientDecision
      summary: Registra a decisão operacional pertencente ao cliente
      description: |
        Registra uma única vez a ação operacional e o desfecho de suspeição
        atribuídos ao cliente. A recomendação da Sentinex não é alterada.
        `operational_action` e `suspicion_outcome` são dimensões independentes.
        A projeção para o vocabulário legado é documentada no contrato KYT v1.
      tags:
        - VASP - Screening
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ResourceID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/KYTClientDecisionRequest'
            example:
              client_decision:
                operational_action: HOLD
                suspicion_outcome: SUSPICIOUS
                rationale: Evidência encaminhada para análise reforçada do cliente.
      responses:
        '200':
          description: Decisão do cliente registrada de forma imutável
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KYTClientDecisionResponse'
        '400':
          description: |
            Identificador, corpo ou combinação de decisão inválida
            (`KYT_SCREENING_ID_INVALID`, `KYT_REQUEST_INVALID`,
            `KYT_CLIENT_DECISION_INVALID`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KYTPublicErrorResponse'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegadoModuloKYT'
        '404':
          description: |
            Análise inexistente nesta empresa (`KYT_SCREENING_NOT_FOUND`). Com chave
            válida, `route_not_found` diz que o caminho ou o método não corresponde a
            operação servida (`ErrorResponseCoded`).
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/KYTPublicErrorResponse'
        '409':
          description: |
            Decisão do cliente já registrada para esta análise
            (`KYT_CLIENT_DECISION_ALREADY_RECORDED`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KYTPublicErrorResponse'
        '413':
          $ref: '#/components/responses/ErroCorpoAcimaDoLimite'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: >
            Persistência, auditoria ou transação da requisição indisponível

            (`KYT_PERSISTENCE_UNAVAILABLE`, `KYT_AUDIT_UNAVAILABLE`, ou texto em `error`

            com `Retry-After: 1` quando a transação conflitou), ou leitura do módulo

            contratado indisponível (`MODULE_STORE_UNAVAILABLE`). As camadas anteriores à

            operação também respondem 503, nada processado: limitador por IP ou da empresa sem

            contagem (`ErroTexto`), estado da conta (`empresa_ACCESS_STATE_UNAVAILABLE`) e

            limitador da chave (`rate_limit_unavailable`) e verificação da chave
            (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/ErroTextoComCodigo'
                  - $ref: '#/components/schemas/KYTPublicErrorResponse'
  /vasp/screenings/{id}/tx-hash:
    patch:
      operationId: attachScreeningTxHash
      summary: Anexa o hash da transação transmitida ao screening (pré-checagem em dois passos)
      description: |
        Depois de pré-checar um saque (screening sem `tx_hash`) e transmitir a
        transação, o cliente anexa o hash ao mesmo screening. O registro liga a
        recomendação e a decisão do cliente ao fato que elas autorizaram ou
        recusaram. Não reavalia o screening, não altera a recomendação nem a
        decisão do cliente, não exige decisão prévia e não gera cobrança. Vale
        também para recomendação `HOLD` ou `DENY`: a recomendação não é ordem.

        Registro imutável e idempotente: o primeiro hash válido vence. Repetir o
        mesmo hash, em qualquer grafia canônica equivalente, devolve 200 com
        `idempotent_replay: true` e não escreve de novo; outro hash sobre um
        screening que já tem hash (do `POST` ou de anexo anterior) devolve 409.
        O mesmo hash pode constar em screenings diferentes do mesma empresa.

        O hash é declarado pelo cliente: o formato é validado conforme a rede do
        screening, mas a existência da transação na rede não é medida
        (`on_chain_verification` é sempre `NOT_MEASURED`). Mesmo escopo
        (`vasp:transactions:screen`) e mesmo portão do módulo KYT das demais
        escritas de screening; o anexo não gera evento de saída.
      tags:
        - VASP - Screening
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ResourceID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/KYTTxHashAttachRequest'
            example:
              tx_hash: <hash de 32 bytes em hex da transação transmitida>
      responses:
        '200':
          description: Hash anexado, ou repetição idêntica (`idempotent_replay`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KYTTxHashAttachResponse'
        '400':
          description: |
            Identificador, corpo ou hash inválido (`KYT_SCREENING_ID_INVALID`,
            `KYT_REQUEST_INVALID`, `KYT_TX_HASH_INVALID`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KYTPublicErrorResponse'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegadoModuloKYT'
        '404':
          description: |
            Análise inexistente nesta empresa (`KYT_SCREENING_NOT_FOUND`). Com chave
            válida, `route_not_found` diz que o caminho ou o método não corresponde a
            operação servida (`ErrorResponseCoded`).
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/KYTPublicErrorResponse'
        '409':
          description: |
            O screening já tem outro hash de transação
            (`KYT_TX_HASH_ALREADY_ATTACHED`); o registro é imutável.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KYTPublicErrorResponse'
        '413':
          $ref: '#/components/responses/ErroCorpoAcimaDoLimite'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: |
            Persistência, auditoria ou transação da requisição indisponível
            (`KYT_PERSISTENCE_UNAVAILABLE`, `KYT_AUDIT_UNAVAILABLE`), ou leitura do
            módulo contratado indisponível (`MODULE_STORE_UNAVAILABLE`); a transação
            inteira é desfeita e o hash não fica gravado sem a trilha. As camadas
            anteriores à operação também respondem 503, nada processado: limitador
            por IP ou da empresa sem contagem (`ErroTexto`), estado da conta
            (`empresa_ACCESS_STATE_UNAVAILABLE`), limitador da chave
            (`rate_limit_unavailable`) e verificação da chave
            (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/ErroTextoComCodigo'
                  - $ref: '#/components/schemas/KYTPublicErrorResponse'
  /vasp/kyt-report:
    post:
      operationId: generateVASPKYTReport
      summary: Gera laudo KYT por API key
      description: |
        Gera o laudo sobre uma carteira da própria empresa. Requer o escopo
        `vasp:transactions:screen` e o módulo KYT. `format` aceita `pdf`, `html`
        ou `json`. O JSON usa `recommendation`; a decisão operacional continua
        pendente até o cliente registrá-la.

        O par endereço/rede é validado antes de qualquer consulta externa: par
        inválido responde 400 `KYT_WALLET_INVALID` com `reason`
        `address_chain_mismatch` e nenhuma chamada externa é feita. `report.id` é o
        identificador único da emissão e `case.case_id` o agrupador estável da
        carteira, na forma canônica (veja `KYTReportJSON` para a compatibilidade
        com laudos anteriores). Valor em reais sem cotação completa não sai no
        JSON (`scope.brl_status` `nao_cotado`), e o monitoramento só é afirmado
        quando uma fonte o comprova (`monitoring`).
      tags:
        - VASP - Screening
      security:
        - ApiKeyAuth: []
      parameters:
        - name: Accept
          in: header
          required: false
          description: >-
            application/vnd.sentinex.kyt.v2+json seleciona a projeção documental canônica;
            application/json preserva v1.
          schema:
            type: string
        - name: format
          in: query
          schema:
            type: string
            enum:
              - pdf
              - html
              - json
            default: pdf
        - name: profile
          in: query
          schema:
            type: string
            enum:
              - operational
              - complete
              - regulatory
            default: operational
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/KYTReportRequest'
            example:
              profile: operational
              wallet:
                address: '0x1111111111111111111111111111111111111111'
                blockchain: ethereum
      responses:
        '200':
          description: Laudo gerado e emissão registrada na auditoria
          headers:
            Content-Disposition:
              description: >
                PDF canônico, quando o snapshot está presente: `KYT-OPERACIONAL-<report-id>.pdf`,

                `KYT-COMPLETO-<report-id>.pdf` ou `KYT-REGULATORIO-<report-id>.pdf`. Sem snapshot

                canônico permanece o formato legado
                `kyt-{perfil}-{emitido-em-UTC}-{report-id}.{formato}`

                (por exemplo, `kyt-operational-20261002T120000Z-<report-id>.pdf`).

                `attachment` no PDF; `inline` no HTML e no JSON. Exposto ao navegador

                pelo CORS.
              schema:
                type: string
            X-Report-Format:
              description: |
                `html` ou `json` quando o artefato é HTML ou JSON; ausente no PDF. Exposto
                ao navegador pelo CORS.
              schema:
                type: string
                enum:
                  - html
                  - json
            X-Report-Audit-Trail:
              description: |
                `recorded` quando a emissão foi gravada no histórico imutável da empresa,
                `not-recorded` quando não foi.
              schema:
                type: string
                enum:
                  - recorded
                  - not-recorded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KYTReportJSON'
            application/vnd.sentinex.kyt.v2+json:
              schema:
                $ref: '#/components/schemas/KYTReportDocumentV2'
            application/pdf:
              schema:
                type: string
                format: binary
            text/html:
              schema:
                type: string
        '400':
          description: |
            Carteira, rede, perfil ou formato inválido (`KYT_REQUEST_INVALID`,
            `KYT_WALLET_REQUIRED`, `KYT_WALLET_INVALID`, `KYT_REPORT_PROFILE_INVALID`,
            `KYT_REPORT_FORMAT_INVALID`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KYTPublicErrorResponse'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegadoModuloKYT'
        '404':
          $ref: '#/components/responses/ErroRotaNaoEncontrada'
        '413':
          $ref: '#/components/responses/ErroCorpoAcimaDoLimite'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '502':
          description: |
            Evidência on-chain ou renderização do laudo falhou no provedor
            (`KYT_EVIDENCE_UNAVAILABLE`, `KYT_REPORT_RENDER_FAILED`,
            `KYT_REPORT_SERIALIZATION_FAILED`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KYTPublicErrorResponse'
        '503':
          description: >
            Dependência essencial, auditoria, renderização ou transação da requisição

            indisponível (`KYT_SERVICE_UNAVAILABLE`, `KYT_EVIDENCE_UNAVAILABLE`,

            `KYT_AUDIT_UNAVAILABLE`, `KYT_REPORT_RENDER_UNAVAILABLE`, ou texto em

            `error`), ou leitura do módulo contratado indisponível

            (`MODULE_STORE_UNAVAILABLE`). As camadas anteriores à operação também

            respondem 503, nada processado: limitador por IP ou da empresa sem contagem

            (`ErroTexto`), estado da conta (`empresa_ACCESS_STATE_UNAVAILABLE`) e limitador da chave
            (`rate_limit_unavailable`) e verificação da chave (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/ErroTextoComCodigo'
                  - $ref: '#/components/schemas/KYTPublicErrorResponse'
  /vasp/sars:
    get:
      operationId: listVASPPendingSARs
      summary: Lista SARs pendentes da empresa VASP
      description: |
        Retorna apenas a projeção enxuta dos SARs pendentes da própria empresa
        autenticado por API-key. A lista não expõe narrative, dados pessoais do
        cliente final nem UUIDs internos de preparador, revisor ou aprovador.
      tags:
        - VASP - Compliance
      security:
        - ApiKeyAuth: []
      responses:
        '200':
          description: SARs pendentes
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        sar_number:
                          type: string
                        report_type:
                          type: string
                        status:
                          type: string
                          enum:
                            - DRAFT
                            - PENDING_REVIEW
                            - PENDING_APPROVAL
                            - SUBMITTED
                            - ACKNOWLEDGED
                            - REJECTED
                        filing_deadline:
                          type: string
                          format: date-time
                          nullable: true
                        created_at:
                          type: string
                          format: date-time
                        filed_at:
                          type: string
                          format: date-time
                          nullable: true
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegado'
        '404':
          $ref: '#/components/responses/ErroRotaNaoEncontrada'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: >
            API regulatória indisponível (`regulatory_unavailable`). As camadas anteriores

            à operação também respondem 503, nada processado: limitador por IP ou da empresa sem

            contagem (`ErroTexto`), estado da conta (`empresa_ACCESS_STATE_UNAVAILABLE`) e

            limitador da chave (`rate_limit_unavailable`) e verificação da chave
            (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
  /vasp/sars/{id}:
    get:
      operationId: getVASPSAR
      summary: Busca detalhe de um SAR da empresa VASP
      description: |
        Retorna o detalhe read-only de um SAR no escopo da empresa. Não expõe campos
        internos de preparador, revisor ou aprovador e não permite submissão ao
        SISCOAF.
      tags:
        - VASP - Compliance
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '200':
          description: SAR da empresa
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      sar_number:
                        type: string
                      report_type:
                        type: string
                      status:
                        type: string
                        enum:
                          - DRAFT
                          - PENDING_REVIEW
                          - PENDING_APPROVAL
                          - SUBMITTED
                          - ACKNOWLEDGED
                          - REJECTED
                      filing_deadline:
                        type: string
                        format: date-time
                        nullable: true
                      created_at:
                        type: string
                        format: date-time
                      filed_at:
                        type: string
                        format: date-time
                        nullable: true
                      risk_indicators:
                        type: array
                        items:
                          type: string
                      narrative:
                        type: string
        '400':
          description: |
            `id` de SAR que não é UUID.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErroTexto'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegado'
        '404':
          description: |
            SAR inexistente nesta empresa (texto em `error`). Com chave válida,
            `route_not_found` diz que o caminho ou o método não corresponde a operação
            servida (`ErrorResponseCoded`).
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/ErroTexto'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: >
            API regulatória indisponível (`regulatory_unavailable`). As camadas anteriores

            à operação também respondem 503, nada processado: limitador por IP ou da empresa sem

            contagem (`ErroTexto`), estado da conta (`empresa_ACCESS_STATE_UNAVAILABLE`) e

            limitador da chave (`rate_limit_unavailable`) e verificação da chave
            (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
  /vasp/decripto/monthly:
    get:
      operationId: generateVASPDeCriptoMonthly
      summary: Prepara artefato DeCripto mensal interno
      description: Layout e transporte externos não homologados por default.
      tags:
        - VASP - Compliance
      security:
        - ApiKeyAuth: []
      parameters:
        - name: year
          in: query
          required: true
          schema:
            type: integer
            minimum: 2020
            maximum: 2100
            example: 2026
        - name: month
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
            maximum: 12
            example: 5
      responses:
        '200':
          description: Artefato mensal interno preparado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericObjectResponse'
        '400':
          description: |
            Parâmetro `year` ou `month` inválido.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErroTexto'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegado'
        '404':
          $ref: '#/components/responses/ErroRotaNaoEncontrada'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: |
            API regulatória indisponível (`regulatory_unavailable`) ou cotação PTAX
            indisponível para converter volumes (texto em `error`); nenhum relatório
            gerado. As camadas anteriores à operação também respondem 503, nada
            processado: limitador por IP ou da empresa sem contagem (`ErroTexto`), estado da conta
            (`empresa_ACCESS_STATE_UNAVAILABLE`) e limitador da chave
            (`rate_limit_unavailable`) e verificação da chave
            (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
  /vasp/decripto/annual:
    get:
      operationId: generateVASPDeCriptoAnnual
      summary: Prepara artefato DeCripto anual interno
      description: Layout e transporte externos não homologados por default.
      tags:
        - VASP - Compliance
      security:
        - ApiKeyAuth: []
      parameters:
        - name: year
          in: query
          required: true
          schema:
            type: integer
            minimum: 2020
            maximum: 2100
            example: 2026
      responses:
        '200':
          description: Artefato anual interno preparado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericObjectResponse'
        '400':
          description: |
            Parâmetro `year` inválido.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErroTexto'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegado'
        '404':
          $ref: '#/components/responses/ErroRotaNaoEncontrada'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: |
            API regulatória indisponível (`regulatory_unavailable`) ou cotação PTAX
            indisponível para converter volumes (texto em `error`); nenhum relatório
            gerado. As camadas anteriores à operação também respondem 503, nada
            processado: limitador por IP ou da empresa sem contagem (`ErroTexto`), estado da conta
            (`empresa_ACCESS_STATE_UNAVAILABLE`) e limitador da chave
            (`rate_limit_unavailable`) e verificação da chave
            (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
  /vasp/alerts:
    get:
      operationId: listClientAlerts
      summary: Lista alertas da empresa (poll server-to-server)
      description: |
        Complementa o webhook outbound. Retorna alertas no escopo da empresa com
        projeção LGPD-safe (sem identidades internas de operador).

        A resposta traz a lista de alertas em `data` e a paginação no bloco
        `pagination`, com `total`, `limit` e `offset`; não há bloco `meta`.
        `limit` fora de 1 a 200 é ajustado para o extremo mais próximo e
        `offset` negativo vale 0, sem erro.
      tags:
        - VASP - Alertas
      security:
        - ApiKeyAuth: []
      parameters:
        - name: status
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 200
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Lista de alertas em `data` e paginação em `pagination`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VASPAlertListResponse'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegado'
        '404':
          $ref: '#/components/responses/ErroRotaNaoEncontrada'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: |
            API de alertas indisponível. As camadas anteriores à operação também respondem
            503, nada processado: limitador por IP ou da empresa sem contagem (`ErroTexto`), estado
            da conta (`empresa_ACCESS_STATE_UNAVAILABLE`) e limitador da chave
            (`rate_limit_unavailable`) e verificação da chave
            (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
  /vasp/webhooks:
    post:
      operationId: createVASPWebhookSubscription
      summary: Cadastra subscription de webhook outbound
      description: >
        Permite que a empresa VASP, autenticado por API-key, cadastre uma URL

        HTTPS pública para receber eventos outbound da Sentinex Risk Crypto.

        O cliente gera o secret HMAC, envia no corpo e o servidor nunca o ecoa.


        Cada entrega traz no header `X-Sentinex-Event-Type` um dos eventos

        assinados, ou `webhook.test` quando disparada por

        `POST /vasp/webhooks/{id}/test`. `webhook.test` não pode ser assinado

        em `events`.


        Situação de cada evento aceito em `events`, numa integração de KYT:


        | Evento | Situação |

        |---|---|

        | `customer.created` | emitido |

        | `customer.updated` | aceito na assinatura, ainda não emitido |

        | `customer.deleted` | aceito na assinatura, ainda não emitido |

        | `wallet.linked` | emitido |

        | `wallet.deactivated` | emitido |

        | `kyc.completed` | aceito na assinatura, ainda não emitido |

        | `kyc.provider_status_changed` | aceito na assinatura; emitido só pelo produto de KYC, não
        numa integração de KYT |

        | `kyc.provider_decision_conflict` | aceito na assinatura; emitido só pelo produto de KYC,
        não numa integração de KYT |

        | `kyc.provider_evidence_refreshed` | aceito na assinatura; emitido só pelo produto de KYC,
        não numa integração de KYT |

        | `kyc.assessment_finalized` | aceito na assinatura; emitido só pelo produto de KYC, não
        numa integração de KYT |

        | `kyc.decision_proposal_created` | aceito na assinatura; emitido só pelo produto de KYC,
        não numa integração de KYT |

        | `kyc.decision_approval_recorded` | aceito na assinatura; emitido só pelo produto de KYC,
        não numa integração de KYT |

        | `kyc.decision_finalized` | aceito na assinatura; emitido só pelo produto de KYC, não numa
        integração de KYT |

        | `kyc.rekyc_scheduled` | aceito na assinatura; emitido só pelo produto de KYC, não numa
        integração de KYT |

        | `kyc.rekyc_started` | aceito na assinatura; emitido só pelo produto de KYC, não numa
        integração de KYT |

        | `kyc.rekyc_failed` | aceito na assinatura; emitido só pelo produto de KYC, não numa
        integração de KYT |

        | `kyc.partner_admitted` | aceito na assinatura; emitido só pelo produto de KYC, não numa
        integração de KYT |

        | `kyc.partner_relationship_ended` | aceito na assinatura; emitido só pelo produto de KYC,
        não numa integração de KYT |

        | `kyc.actor_invitation.expired` | aceito na assinatura; emitido só pelo produto de KYC, não
        numa integração de KYT |

        | `kyc.actor_invitation.reminder_rotated` | aceito na assinatura; emitido só pelo produto de
        KYC, não numa integração de KYT |

        | `kyt.screening.completed` | emitido |

        | `kyt.screening.sanction_hit` | emitido |

        | `kyt.case.client_decision.recorded` | emitido |

        | `alert.created` | emitido |

        | `alert.escalated` | emitido |

        | `alert.resolved` | emitido |
      tags:
        - VASP - Webhooks
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - target_url
                - secret
              properties:
                target_url:
                  type: string
                  format: uri
                  pattern: ^https://
                  example: https://vasp.example/webhooks/sentinex
                  description: URL HTTPS pública que receberá os eventos.
                events:
                  type: array
                  description: >-
                    Lista de eventos. Array vazio assina todos. A situação de cada evento (emitido,
                    ou aceito na assinatura e ainda não emitido numa integração de KYT) está na
                    tabela da descrição da operação.
                  items:
                    type: string
                    enum:
                      - customer.created
                      - customer.updated
                      - customer.deleted
                      - wallet.linked
                      - wallet.deactivated
                      - kyc.completed
                      - kyc.provider_status_changed
                      - kyc.provider_decision_conflict
                      - kyc.provider_evidence_refreshed
                      - kyc.assessment_finalized
                      - kyc.decision_proposal_created
                      - kyc.decision_approval_recorded
                      - kyc.decision_finalized
                      - kyc.rekyc_scheduled
                      - kyc.rekyc_started
                      - kyc.rekyc_failed
                      - kyc.partner_admitted
                      - kyc.partner_relationship_ended
                      - kyc.actor_invitation.expired
                      - kyc.actor_invitation.reminder_rotated
                      - kyt.screening.completed
                      - kyt.screening.sanction_hit
                      - kyt.case.client_decision.recorded
                      - alert.created
                      - alert.escalated
                      - alert.resolved
                description:
                  type: string
                  description: Descrição operacional da subscription.
                secret:
                  type: string
                  minLength: 32
                  maxLength: 128
                  writeOnly: true
      responses:
        '201':
          description: Subscription criada sem ecoar o secret HMAC
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  id:
                    type: string
                    format: uuid
                  target_url:
                    type: string
                    format: uri
                  events:
                    type: array
                    items:
                      type: string
        '400':
          description: |
            Corpo ilegível, `target_url` ausente ou não HTTPS, `secret` fora de 32 a 128
            bytes ou evento inválido.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErroTexto'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegado'
        '404':
          $ref: '#/components/responses/ErroRotaNaoEncontrada'
        '413':
          $ref: '#/components/responses/ErroCorpoAcimaDoLimite'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: >
            Webhooks de saída indisponíveis ou cofre do segredo indisponível. As camadas

            anteriores à operação também respondem 503, nada processado: limitador por IP ou da
            empresa sem contagem (`ErroTexto`), estado da conta

            (`empresa_ACCESS_STATE_UNAVAILABLE`) e limitador da chave

            (`rate_limit_unavailable`) e verificação da chave

            (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
    get:
      operationId: listVASPWebhookSubscriptions
      summary: Lista subscriptions de webhook outbound da empresa
      tags:
        - VASP - Webhooks
      security:
        - ApiKeyAuth: []
      responses:
        '200':
          description: Lista no escopo da empresa de subscriptions ativas
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenericObjectResponse'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegado'
        '404':
          $ref: '#/components/responses/ErroRotaNaoEncontrada'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: >
            Webhooks de saída indisponíveis. As camadas anteriores à operação também

            respondem 503, nada processado: limitador por IP ou da empresa sem contagem

            (`ErroTexto`), estado da conta (`empresa_ACCESS_STATE_UNAVAILABLE`) e limitador da chave
            (`rate_limit_unavailable`) e verificação da chave (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
  /vasp/webhooks/{id}/test:
    post:
      operationId: testVASPWebhookSubscription
      summary: Enfileira um evento de teste para a subscription
      description: |
        Enfileira `webhook.test` pelo pipeline real de assinatura HMAC, retry,
        proteção SSRF e DLQ. A subscription é resolvida dentro da empresa da
        API key. No sandbox, somente este tipo de evento pode atravessar o gate
        de efeitos externos; eventos operacionais e regulatórios continuam
        bloqueados.
      tags:
        - VASP - Webhooks
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '202':
          description: Evento de teste aceito para entrega assíncrona
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - subscription_id
                  - event_id
                  - status
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  subscription_id:
                    type: string
                    format: uuid
                  event_id:
                    type: string
                    format: uuid
                  status:
                    type: string
                    enum:
                      - pending
        '400':
          description: |
            `id` que não é UUID.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErroTexto'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegado'
        '404':
          description: |
            Assinatura ativa inexistente nesta empresa (texto em `error`). Com chave
            válida, `route_not_found` diz que o caminho ou o método não corresponde a
            operação servida (`ErrorResponseCoded`).
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/ErroTexto'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: >
            Fila de entrega ou auditoria indisponível. As camadas anteriores à operação

            também respondem 503, nada processado: limitador por IP ou da empresa sem contagem

            (`ErroTexto`), estado da conta (`empresa_ACCESS_STATE_UNAVAILABLE`) e limitador da chave
            (`rate_limit_unavailable`) e verificação da chave (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
  /vasp/webhooks/{id}:
    delete:
      operationId: deleteVASPWebhookSubscription
      summary: Remove uma subscription de webhook outbound
      description: Remove logicamente uma subscription ativa da própria empresa.
      tags:
        - VASP - Webhooks
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/ResourceID'
      responses:
        '204':
          description: Subscription removida
        '400':
          description: |
            `id` que não é UUID.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErroTexto'
        '401':
          $ref: '#/components/responses/ErroChaveNaoAutenticada'
        '402':
          $ref: '#/components/responses/TenantAccessPaymentRequired'
        '403':
          $ref: '#/components/responses/ErroAcessoNegado'
        '404':
          description: |
            Assinatura inexistente nesta empresa (texto em `error`). Com chave válida,
            `route_not_found` diz que o caminho ou o método não corresponde a operação
            servida (`ErrorResponseCoded`).
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorResponseCoded'
                  - $ref: '#/components/schemas/ErroTexto'
        '429':
          $ref: '#/components/responses/ErroLimiteDeRequisicoes'
        '500':
          $ref: '#/components/responses/ErroInterno'
        '503':
          description: >
            Webhooks de saída indisponíveis. As camadas anteriores à operação também

            respondem 503, nada processado: limitador por IP ou da empresa sem contagem

            (`ErroTexto`), estado da conta (`empresa_ACCESS_STATE_UNAVAILABLE`) e limitador da chave
            (`rate_limit_unavailable`) e verificação da chave (`api_key_verification_unavailable`).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErroTexto'
                  - $ref: '#/components/schemas/TenantAccessRefusalResponse'
                  - $ref: '#/components/schemas/ErrorResponseCoded'
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        Chave de API por empresa para integração máquina-a-máquina (superfície
        VASP, `/api/v1/vasp/*`). Chaves emitidas a partir da 2.16.0 têm o formato
        `snxc_<env>_<identificador>_<segredo>` (identificador de 32 caracteres e
        segredo de 64 hexadecimais, 107 caracteres no total); chaves anteriores,
        `snxc_<env>_<hex>`, continuam válidas até a expiração ou a revogação. Trate a
        chave como texto opaco. Envie no header `X-API-Key: snxc_live_...`. Se a
        plataforma não consegue verificar a chave, a resposta é 503
        `api_key_verification_unavailable`, e a requisição pode ser repetida com
        espera; nunca é tratada como chave válida. O backend também aceita
        `Authorization: Bearer snxc_...` como fallback, mas `X-API-Key` é o
        modo canônico. Escopos least-privilege por rota (ex.:
        `vasp:transactions:screen`, `vasp:webhooks:manage`, `vasp:read`,
        `kyc:read` e `kyc:write`).
        Ambiente live e sandbox são isolados (uma chave sandbox não acessa
        live e vice-versa). Rate limit por key/minuto com headers
        `X-RateLimit-Limit` / `X-RateLimit-Remaining` e `Retry-After` no 429.
        Toda requisição por chave passa, logo depois da autenticação, pelo portão
        de acesso da empresa: só segue conta operacional com o
        acesso por API liberado pelo super admin. Recusas: 402 `TRIAL_EXPIRED`,
        403 `empresa_SUSPENDED`, `empresa_NOT_OPERATIONAL`, `API_ACCESS_DISABLED` ou
        `empresa_ACCESS_DENIED`, e 503 `empresa_ACCESS_STATE_UNAVAILABLE`. Desligar a
        API não revoga a chave: vale na requisição seguinte.
  parameters:
    ResourceID:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        maxLength: 255
      description: |
        Mesma chave + corpo idêntico = replay idempotente (200 com
        idempotent_replay=true); mesma chave + corpo diferente = 409.
  responses:
    TenantAccessPaymentRequired:
      description: |
        Conta sem direito comercial (`TRIAL_EXPIRED`): trial vencido, trial sem
        data ou empresa sem contrato. A requisição NÃO foi processada; uma
        triagem recusada assim é transação não triada, nunca aprovada.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/TenantAccessRefusalResponse'
    ErroChaveNaoAutenticada:
      description: |
        A chave de API não autenticou a requisição, e nenhuma camada adiante
        rodou. `error.code` é `missing_api_key` (nenhuma chave em `X-API-Key`
        nem em `Authorization: Bearer snxc_...`) ou `invalid_api_key` (chave
        desconhecida, revogada ou expirada). Quando a plataforma não consegue
        verificar a chave, a resposta é `503` com
        `api_key_verification_unavailable`, e não `401`: repita com backoff.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponseCoded'
    ErroAcessoNegado:
      description: |
        Acesso recusado antes da operação, nesta ordem de camadas: o escudo de
        borda recusa travessia de diretório na URL (`ErroTexto`); a chave é
        recusada pelo IP de origem (`api_key_ip_not_allowed`); o ambiente da
        chave não é aceito por esta implantação (`api_key_environment_required`
        ou `api_key_environment_not_allowed`); a conta não pode usar a API
        (`TenantAccessRefusalResponse`: `empresa_SUSPENDED`,
        `empresa_NOT_OPERATIONAL`, `API_ACCESS_DISABLED` ou
        `empresa_ACCESS_DENIED`); a chave não tem o escopo da rota
        (`insufficient_scope`, com o escopo na mensagem). Não repita sem
        corrigir a causa.
      content:
        application/json:
          schema:
            anyOf:
              - $ref: '#/components/schemas/ErrorResponseCoded'
              - $ref: '#/components/schemas/TenantAccessRefusalResponse'
              - $ref: '#/components/schemas/ErroTexto'
    ErroAcessoNegadoModuloKYT:
      description: |
        Acesso recusado antes da operação. Além das recusas de
        `ErroAcessoNegado` (escudo de borda, IP da chave, ambiente, conta e
        escopo), a empresa pode não ter o módulo de KYT contratado
        (`ModuleNotEnabledResponse`, `MODULE_NOT_ENABLED`, com
        `details.module`). Não repita sem corrigir a causa.
      content:
        application/json:
          schema:
            anyOf:
              - $ref: '#/components/schemas/ErrorResponseCoded'
              - $ref: '#/components/schemas/TenantAccessRefusalResponse'
              - $ref: '#/components/schemas/ErroTexto'
              - $ref: '#/components/schemas/ModuleNotEnabledResponse'
    ErroRotaNaoEncontrada:
      description: |
        Com chave válida, o caminho ou o método não corresponde a operação
        servida por esta implantação (`error.code` `route_not_found`).
        Confira método, caminho e o prefixo `/api/v1`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponseCoded'
    ErroCorpoAcimaDoLimite:
      description: |
        Corpo acima de 1 MiB, recusado pelo escudo de borda antes da
        autenticação (`code` `BODY_TOO_LARGE`). Nada foi processado.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErroTextoComCodigo'
    ErroLimiteDeRequisicoes:
      description: |
        Limite de requisições atingido; nada foi processado. Formas, pela camada
        que recusou: limite da chave (`ErrorResponseCoded`,
        `api_key_rate_limit_exceeded`, 60 por minuto quando a chave não define
        outro valor, `Retry-After: 60`); limite global por hora (`ErroLimite`,
        `retry_after` 3600 e `Retry-After: 3600`); escudo de borda por IP
        (`ErroLimite`: `RATE_LIMIT_HARD` acima de 1.000 por minuto, com 300
        segundos; `IP_BANNED` no bloqueio temporário; e a rajada com intervalo
        menor que 50 ms, que vem sem `Retry-After` e sem `retry_after`). A
        busca de clientes tem limite próprio (`error` `rate_limited`, com
        `retry_after` e `detail`) e o laudo também (`error`
        `RATE_LIMIT_EXCEEDED`, 120 por hora por IP, `Retry-After: 3600`).
        No sandbox, chamadas VASP autenticadas também usam
        `sandbox_simulator_rate_limit_exceeded`: 60 por minuto por key, 300 por
        minuto por empresa e 10.000 por dia UTC por empresa. Essa recusa inclui
        `Retry-After`, `X-RateLimit-Limit` e `X-RateLimit-Remaining: 0`.
        Espere o `Retry-After` quando ele vier.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        X-RateLimit-Limit:
          description: Teto da janela do limitador que respondeu, quando ele informa.
          schema:
            type: integer
        X-RateLimit-Remaining:
          description: Requisições restantes na janela, quando o limitador informa.
          schema:
            type: integer
      content:
        application/json:
          schema:
            anyOf:
              - $ref: '#/components/schemas/ErrorResponseCoded'
              - $ref: '#/components/schemas/ErroLimite'
    ErroInterno:
      description: |
        Falha inesperada. A mensagem é genérica de propósito; guarde o
        `X-Request-ID` da resposta para o suporte e repita com espera.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErroTexto'
  headers:
    RetryAfter:
      description: |
        Segundos a esperar antes de repetir. Vem nos 429 dos limitadores (salvo
        a rajada do escudo) e no 503 de transação que conflitou (1 segundo).
      schema:
        type: integer
        minimum: 1
  schemas:
    ModuleNotEnabledResponse:
      type: object
      required:
        - success
        - error
        - code
      properties:
        success:
          type: boolean
          enum:
            - false
        error:
          type: string
          example: Este recurso não está habilitado no contrato atual.
        code:
          type: string
          enum:
            - MODULE_NOT_ENABLED
        details:
          type: object
          properties:
            module:
              type: string
              description: Código do módulo no catálogo.
              example: mod_crypto_sanctions
    GenericObjectResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          type: object
          additionalProperties: true
        meta:
          type: object
          additionalProperties: true
    CoafObjectivePreview:
      type: object
      description: >-
        Prévia objetiva do screening. Sem evento confirmado, o screening apenas devolve esta prévia,
        que nunca é comunicação definitiva.
      required:
        - state
        - final
        - gaps
      properties:
        state:
          type: string
          enum:
            - INDETERMINADA
        final:
          type: boolean
          enum:
            - false
        gaps:
          type: array
          items:
            type: string
    ErrorResponseCoded:
      type: object
      description: |
        Envelope de erro codificado (middlewares de API key e erros de
        negócio da superfície VASP). Trate `error.code` como contrato
        estável; `error.message` é texto livre.
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              example: missing_api_key
            message:
              type: string
              example: X-API-Key header required
    ErroTexto:
      type: object
      description: |
        Envelope de erro com a mensagem em texto no campo `error`, sem código.
        É o formato dos handlers de cadastro, carteira, webhooks, ledger e
        leituras regulatórias, da recuperação de falha inesperada (500), da
        indisponibilidade das camadas de borda (503) e da recusa do escudo de
        borda por padrão de ataque (403). Ramifique pelo status HTTP; a
        mensagem é texto livre e pode mudar.
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          enum:
            - false
        error:
          type: string
          example: Carteira não encontrada.
    ErroTextoComCodigo:
      type: object
      description: |
        Envelope de erro com a mensagem em texto em `error` e o código estável
        em `code`, na raiz do corpo. Na API de KYT aparece no corpo acima de
        1 MiB (`BODY_TOO_LARGE`, 413) e na leitura do módulo contratado que
        não pôde ser feita (`MODULE_STORE_UNAVAILABLE`, 503).
      required:
        - success
        - error
        - code
      properties:
        success:
          type: boolean
          enum:
            - false
        error:
          type: string
        code:
          type: string
          example: BODY_TOO_LARGE
    ErroLimiteDoLote:
      type: object
      additionalProperties: false
      description: |
        Recusa dos limites próprios do lote de clientes, por ator (a chave de API
        que envia) ou por empresa, contados por hora. Vem com `Retry-After: 3600`.
      required:
        - success
        - error
        - code
        - retry
        - window
        - limit
      properties:
        success:
          type: boolean
          enum:
            - false
        error:
          type: string
        code:
          type: string
          enum:
            - BATCH_RATE_LIMIT_ACTOR
            - BATCH_RATE_LIMIT_TENANT
        retry:
          type: string
          description: Orientação em texto de quando repetir.
          example: wait until next hour
        window:
          type: string
          example: 1 hour
        limit:
          type: integer
          minimum: 1
          description: Teto de lotes por hora do escopo que recusou.
    ErroLimite:
      type: object
      description: |
        Recusa de um limitador anterior à operação, com a mensagem em texto em
        `error`. `retry_after` repete, em segundos, o cabeçalho `Retry-After`
        quando o limitador o envia. `code` vem do escudo de borda
        (`RATE_LIMIT_HARD`, ou `IP_BANNED` com `reference` para citar ao
        suporte), e `detail` vem dos limites próprios da busca de clientes e
        do laudo.
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          enum:
            - false
        error:
          type: string
          example: Too many requests. Please try again later.
        retry_after:
          type: integer
          minimum: 1
          example: 3600
        code:
          type: string
          example: RATE_LIMIT_HARD
        reference:
          type: string
          description: Identificador curto do bloqueio, para citar ao suporte.
        detail:
          type: string
    VASPAlertListResponse:
      type: object
      description: |
        Resposta de GET /vasp/alerts. A lista vem em `data` e a paginação em
        `pagination`; não há bloco `meta`.
      required:
        - success
        - data
        - pagination
      properties:
        success:
          type: boolean
        data:
          type: array
          items:
            $ref: '#/components/schemas/VASPClientAlert'
        pagination:
          type: object
          required:
            - total
            - limit
            - offset
          properties:
            total:
              type: integer
              minimum: 0
              description: Total de alertas do filtro, em todas as páginas.
            limit:
              type: integer
              minimum: 1
              maximum: 200
              description: Tamanho de página efetivamente usado.
            offset:
              type: integer
              minimum: 0
              description: Deslocamento efetivamente usado.
    VASPClientAlert:
      type: object
      description: |
        Projeção do alerta para o cliente, sem identidades internas de operador,
        metadados nem evidências. Campos opcionais são omitidos quando vazios.
      required:
        - id
        - type
        - severity
        - status
        - title
        - requires_sar
        - requires_coaf
        - sla_breached
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
        severity:
          type: string
        status:
          type: string
        title:
          type: string
        description:
          type: string
        category:
          type: string
        rule_id:
          type: string
        risk_score:
          type: number
          format: double
        requires_sar:
          type: boolean
        requires_coaf:
          type: boolean
        sla_deadline:
          type: string
          format: date-time
        sla_breached:
          type: boolean
        resolved_at:
          type: string
          format: date-time
        tags:
          type: array
          items:
            type: string
        transaction_id:
          type: string
          format: uuid
        client_id:
          type: string
          format: uuid
        wallet_id:
          type: string
          format: uuid
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    VASPCustomerCreateRequest:
      type: object
      required:
        - external_id
        - type
      description: |
        Envie o documento do titular em `document` (texto) ou em `document_hash`
        (SHA-256); um dos dois é obrigatório. `document_hash` continua aceito
        para quem já integra por ele, mas, sozinho, não permite exibir o
        documento. Se vierem os dois e divergirem, a resposta é 422
        `DOCUMENT_HASH_MISMATCH`.
      properties:
        client_id:
          type: string
          format: uuid
          description: |
            Opcional. Id do cliente Sentinex que é o mesmo titular: o id devolvido por
            POST /clients, o mesmo que a análise KYT recebe como client_id. É gravado uma vez e
            não tem rota de alteração. Sem ele, o KYC externo deste cliente (convite e push)
            responde 403 client_link_required. UUID zero ou texto que não é UUID responde 400
            invalid_client_id; cliente que não é desta empresa responde 422 client_not_found.
        external_id:
          type: string
          maxLength: 128
          description: >-
            Id do cliente no seu sistema. Padrão: letras, números, ponto, underscore, dois-pontos,
            hífen.
        type:
          type: string
          enum:
            - PF
            - PJ
        document_hash:
          type: string
          description: |
            SHA-256 hex do CPF/CNPJ, com prefixo sha256 opcional. Continua
            aceito. Envie `document_hash` ou `document`; um dos dois é
            obrigatório. Só com `document_hash`, o servidor não tem o documento
            para cifrar e não consegue exibi-lo em tela nem em laudo. Se vier
            junto de `document` e divergir, a resposta é 422
            `DOCUMENT_HASH_MISMATCH`. Neste campo, nunca envie o documento em
            claro: para isso existe `document`.
        document:
          type: string
          writeOnly: true
          minLength: 11
          maxLength: 32
          description: |
            Opcional. CPF ou CNPJ do titular em texto, com ou sem pontuação (o
            CNPJ pode ser numérico ou alfanumérico). O servidor canoniza o valor
            (CPF só com os 11 dígitos; CNPJ em maiúsculas e sem pontuação) e
            confere os dígitos verificadores. O servidor não guarda o documento
            em claro: guarda só um índice cego, calculado com uma chave da empresa
            mantida fora do banco, que serve para buscar e comparar, e a cifra do
            valor, para exibição a quem tem permissão. Não é SHA-256 sem sal.
            `document` tem precedência no cálculo do índice. `document_hash`
            continua aceito; envie um dos dois. Se vierem os dois e divergirem,
            a resposta é 422 `DOCUMENT_HASH_MISMATCH`. O documento nunca é
            devolvido em resposta. Os exemplos deste contrato usam documentos
            fictícios.
          example: 123.456.789-01
        document_country:
          type: string
          description: ISO-3166 alpha-2 (default BR).
        legal_name_hash:
          type: string
          description: SHA-256 hex do nome/razão social (opcional).
        trade_name:
          type: string
          maxLength: 255
        incorporation_date:
          type: string
          format: date
          description: YYYY-MM-DD (PJ).
        country:
          type: string
          description: ISO-3166 alpha-2 (default BR).
        state:
          type: string
          maxLength: 255
        kyc_tier:
          type: string
          enum:
            - SIMPLIFIED
            - STANDARD
            - ENHANCED
          description: Default STANDARD.
        kyc_completed_at:
          type: string
          format: date-time
        external_kyc_provider:
          type: string
          maxLength: 255
        external_kyc_reference_url:
          type: string
          description: URL https absoluta (máx. 2048 chars).
        risk_classification:
          type: string
          enum:
            - LOW
            - MEDIUM
            - HIGH
            - CRITICAL
          description: Default MEDIUM.
        tags:
          type: array
          maxItems: 50
          items:
            type: string
            pattern: ^[a-z0-9_]{1,32}$
        attributes:
          type: object
          description: |
            JSON livre (máx. 16 KB). Chaves/valores com identificadores
            pessoais em claro (cpf, cnpj, email, phone, nomes) são rejeitados
            com 400.
        consent_data_share:
          type: boolean
        consent_data_share_at:
          type: string
          format: date-time
        source:
          type: string
          description: Default api.
        source_event_id:
          type: string
          maxLength: 255
    VASPCustomer:
      type: object
      description: Projeção retornada pelos endpoints /vasp/customers.
      properties:
        id:
          type: string
          description: Id do cliente, no formato UUID.
        tenant_id:
          type: string
          format: uuid
        client_id:
          type: string
          nullable: true
          description: Vínculo com o cliente Sentinex, no formato UUID; nulo quando a matrícula não o informou.
        external_id:
          type: string
        type:
          type: string
          enum:
            - PF
            - PJ
        document_country:
          type: string
        trade_name:
          type: string
        incorporation_date:
          type: string
          format: date-time
          nullable: true
        country:
          type: string
        state:
          type: string
        kyc_tier:
          type: string
          enum:
            - SIMPLIFIED
            - STANDARD
            - ENHANCED
        kyc_completed_at:
          type: string
          format: date-time
          nullable: true
        external_kyc_provider:
          type: string
        external_kyc_reference_url:
          type: string
        risk_classification:
          type: string
          enum:
            - LOW
            - MEDIUM
            - HIGH
            - CRITICAL
        tags:
          type: array
          items:
            type: string
        attributes:
          type: object
        consent_data_share:
          type: boolean
        consent_data_share_at:
          type: string
          format: date-time
          nullable: true
        screening_status:
          type: string
        receita_status:
          type: string
        source:
          type: string
        source_event_id:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    VASPCustomerBatchRequest:
      type: object
      additionalProperties: false
      required:
        - customers
      properties:
        customers:
          type: array
          minItems: 1
          maxItems: 1000
          description: |
            De 1 a 1.000 clientes finais por chamada. Cada item tem o formato do
            corpo de `POST /vasp/customers`.
          items:
            $ref: '#/components/schemas/VASPCustomerCreateRequest'
    VASPCustomerBatchItemResult:
      type: object
      description: |
        Resultado de um item do lote. `data` vem quando o status é `CREATED` ou
        `UPDATED`, e `error` vem quando é `REJECTED`. O resultado nunca revela se
        o documento do item já existe sob outro `external_id`.
      required:
        - index
        - status
      properties:
        index:
          type: integer
          minimum: 0
          description: Posição do item em `customers`, contada a partir de 0.
        external_id:
          type: string
          description: |
            O `external_id` normalizado do titular. Vem só quando o status é
            `CREATED` ou `UPDATED`; item `REJECTED` não o repete (use `index`).
        status:
          type: string
          enum:
            - CREATED
            - UPDATED
            - REJECTED
          description: |
            `CREATED` quando o titular é novo e foi gravado. `UPDATED` quando o
            `external_id` já existia na empresa e o titular foi atualizado por ele.
            `REJECTED` quando o item foi recusado por validação e nada foi
            gravado para ele.
        data:
          $ref: '#/components/schemas/VASPCustomer'
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: |
                Código estável de validação do item: `invalid_customer`,
                `invalid_client_id`, `client_not_found` ou
                `DOCUMENT_HASH_MISMATCH`. Trate código desconhecido como erro
                genérico: a lista pode crescer. Nenhum código diz que o documento
                já existe sob outro `external_id`.
            message:
              type: string
              description: Texto fixo, `Item recusado por validação.`; não detalha o campo.
    VASPCustomerBatchResponse:
      type: object
      required:
        - success
        - idempotent_replay
        - summary
        - results
      properties:
        success:
          type: boolean
          description: |
            Verdadeiro quando o lote foi lido e processado, mesmo que itens
            tenham sido recusados. Leia `results`.
        idempotent_replay:
          type: boolean
          description: |
            Sempre presente. Verdadeiro quando é o resultado original,
            devolvido na repetição com a mesma `Idempotency-Key` e o mesmo
            corpo; falso na primeira execução.
        summary:
          type: object
          required:
            - total
            - created
            - updated
            - rejected
          properties:
            total:
              type: integer
              minimum: 0
            created:
              type: integer
              minimum: 0
            updated:
              type: integer
              minimum: 0
            rejected:
              type: integer
              minimum: 0
        results:
          type: array
          description: Um resultado por item, na ordem do envio.
          items:
            $ref: '#/components/schemas/VASPCustomerBatchItemResult'
    VASPWalletEnrollRequest:
      type: object
      required:
        - customer_id
        - external_id
        - address
        - blockchain
      properties:
        customer_id:
          type: string
          description: |
            Id do cliente, no formato UUID, devolvido por POST /vasp/customers.
            Valor que não é UUID responde 400 invalid_wallet.
        external_id:
          type: string
          maxLength: 128
          description: Id da carteira no seu sistema (único por empresa).
        address:
          type: string
          maxLength: 256
          description: |
            Endereço da carteira, validado pela rede (endereço inválido
            responde 400 invalid_wallet). O backend grava só a cifra do
            endereço e o HMAC da empresa; nunca o endereço em claro.
        blockchain:
          type: string
          description: 'Aceitos: btc, eth, bsc, polygon, tron, solana, arbitrum (e aliases).'
        asset_symbol:
          type: string
          maxLength: 20
          description: Símbolo do ativo (A-Z, 0-9, ponto, underscore, hífen).
        wallet_label:
          type: string
          maxLength: 255
        custody_type:
          type: string
          enum:
            - vasp_custodied
            - self_hosted
          description: |
            Custódia declarada pela VASP: `vasp_custodied` (a VASP custodia) ou
            `self_hosted` (autocustódia do cliente). Opcional: ausente fica
            registrado como não informado e é contado como tal.
        metadata:
          type: object
          description: JSON livre (máx. 16 KB), sem identificadores pessoais em claro.
    VASPWalletPatchRequest:
      type: object
      description: Pelo menos um de `status`, `address` ou `custody_type`.
      properties:
        status:
          type: string
          enum:
            - active
          description: Reativa a carteira desativada, com o mesmo wallet_id.
        address:
          type: string
          maxLength: 256
          description: Reenvio do endereço da carteira; exige `blockchain`.
        blockchain:
          type: string
          description: >-
            Rede do endereço reenviado. Aceitos: btc, eth, bsc, polygon, tron, solana, arbitrum (e
            aliases).
        custody_type:
          type: string
          enum:
            - vasp_custodied
            - self_hosted
    VASPWalletErrorResponse:
      type: object
      description: |
        Envelope de erro da superfície de carteiras VASP. Além de `code` e
        `message`, os conflitos de carteira trazem os ids envolvidos.
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: |
                Código estável. Na superfície de carteiras: wallet_already_exists,
                KYT_IDEMPOTENCY_CONFLICT, wallet_is_institutional,
                wallet_duplicate_legacy, wallet_has_open_process,
                wallet_address_active_elsewhere. Trate código desconhecido como
                erro genérico: a lista pode crescer.
            message:
              type: string
            wallet_ids:
              type: array
              items:
                type: string
                format: uuid
              description: Carteiras antigas que casam o endereço (wallet_duplicate_legacy).
            wallet_id:
              type: string
              format: uuid
              description: A outra carteira ativa do endereço (wallet_address_active_elsewhere).
            case_ids:
              type: array
              items:
                type: string
                format: uuid
              description: Casos abertos ligados à carteira (wallet_has_open_process).
            sar_ids:
              type: array
              items:
                type: string
                format: uuid
              description: SAR não enviados ligados à carteira (wallet_has_open_process).
    VASPWalletEnrollment:
      type: object
      description: Resposta dos endpoints de enrollment de carteira.
      properties:
        wallet_id:
          type: string
          format: uuid
        customer_id:
          type: string
        external_id:
          type: string
        blockchain:
          type: string
        asset_symbol:
          type: string
        wallet_label:
          type: string
        screening_status:
          type: string
          description: PENDING, APPROVED, REVIEW, BLOCKED ou FAILED.
        decision:
          type: string
          enum:
            - APPROVE
            - REVIEW
            - BLOCK
        risk_score:
          type: number
          format: float
        risk_tier:
          type: string
        reason_codes:
          type: array
          items:
            type: string
        existing_wallet:
          type: boolean
          description: true quando a carteira já existia na base da empresa.
        status_url:
          type: string
          description: Caminho para consultar o status (GET /vasp/wallets/{id}).
        alert_id:
          type: string
        alert_created:
          type: boolean
        status:
          type: string
          enum:
            - active
            - deactivated
          description: Estado da carteira; a desativada mantém o wallet_id.
        custody_type:
          type: string
          nullable: true
          enum:
            - vasp_custodied
            - self_hosted
          description: Custódia declarada; null quando não informada.
        deactivated_at:
          type: string
          format: date-time
          nullable: true
    RuleCoverageDegradedCapability:
      type: object
      additionalProperties: false
      required:
        - rule_id
        - source
        - cause
      properties:
        rule_id:
          type: string
          maxLength: 80
        source:
          type: string
          maxLength: 80
        cause:
          type: string
          enum:
            - REQUIRED_INPUT_MISSING
            - SOURCE_UNAVAILABLE
            - SOURCE_STALE
            - MANIFEST_MISSING
            - ENGINE_UNAVAILABLE
    RuleCoverageSnapshot:
      type: object
      additionalProperties: false
      required:
        - manifest_version
        - state
        - evaluable_rules
        - degraded_rules
        - disabled_rules
        - causes
      properties:
        manifest_version:
          type: string
          enum:
            - snx-rule-capabilities/2026-07-29.v1
        state:
          type: string
          enum:
            - ACTIVE_READY
            - ACTIVE_DEGRADED
            - DISABLED
        input_assessments:
          type: object
          description: >-
            Estado das entradas `amount_brl`, `aggregate_7d` e `travel_rule` da triagem em tempo
            real, presente só na resposta de `POST /vasp/screenings`. Sem identificação do titular
            (`customer_ref` vazio), o agregado de 7 dias sai `NOT_EVALUATED` e não impõe retenção.
            Omitido em análises anteriores a esta medição.
          additionalProperties:
            type: object
            required:
              - state
            properties:
              state:
                type: string
                enum:
                  - MEASURED
                  - NOT_MEASURED
                  - NOT_EVALUATED
              reason:
                type: string
                description: Motivo, presente quando o estado não é `MEASURED`.
        evaluable_rules:
          type: array
          maxItems: 512
          uniqueItems: true
          items:
            type: string
            maxLength: 80
        degraded_rules:
          type: array
          maxItems: 512
          items:
            $ref: '#/components/schemas/RuleCoverageDegradedCapability'
        disabled_rules:
          type: array
          maxItems: 512
          uniqueItems: true
          items:
            type: string
            maxLength: 80
        causes:
          type: array
          maxItems: 16
          uniqueItems: true
          items:
            type: string
            enum:
              - REQUIRED_INPUT_MISSING
              - SOURCE_UNAVAILABLE
              - SOURCE_STALE
              - MANIFEST_MISSING
              - ENGINE_UNAVAILABLE
    KYTMeasurementState:
      type: string
      enum:
        - MEASURED
        - NOT_MEASURED
      description: >-
        Estado de medição de uma dimensão. `NOT_MEASURED` significa que a dimensão não foi medida
        nesta consulta, e nunca vale como resultado negativo, zero ou lista vazia conclusiva.
    KYTIssuerBlockLabel:
      type: object
      required:
        - source
        - issuer
        - event
        - state
        - contract
        - block_number
        - block_time
        - tx_hash
        - effect
      properties:
        source:
          type: string
          enum:
            - ISSUER_BLOCK_EVENT
        issuer:
          type: string
          enum:
            - USDT
            - USDC
        event:
          type: string
          description: Assinatura do evento do ABI público do contrato.
          example: Blacklisted(address)
        state:
          type: string
          enum:
            - BLOCKED
            - UNBLOCKED
          description: Estado pelo evento mais recente, por (bloco, índice do log).
        contract:
          type: string
          pattern: ^0x[0-9a-f]{40}$
          description: Contrato do emissor que gravou o evento.
        block_number:
          type: integer
          format: int64
          minimum: 0
        block_time:
          type: string
          format: date-time
          description: Data do bloco que contém o evento, em UTC.
        tx_hash:
          type: string
          pattern: ^0x[0-9a-f]{64}$
        effect:
          type: string
          enum:
            - LABEL_ONLY
        recommendation:
          type: string
          enum:
            - REVIEW
            - HOLD
          description: >-
            Recomendação da política da empresa quando `state` é `BLOCKED`. Nunca `DENY` e nunca
            `ALLOW`. Omitido quando `state` é `UNBLOCKED`.
    KYTMeasuredCapabilities:
      type: object
      description: >-
        Capacidade de medição de cada dimensão da triagem, independente do status do provedor.
        `MEASURED` atesta apenas a capacidade nomeada, inclusive quando o resultado foi negativo. Os
        cinco campos saem sempre, com `NOT_MEASURED` quando a dimensão não foi medida. `labels` vale
        `MEASURED` também quando há etiqueta de bloqueio de emissor para o endereço (ver
        `label_evidence`). Ausência de evento não torna a dimensão `MEASURED`.
      required:
        - sanctions
        - labels
        - exposures
        - graph
        - typologies
      properties:
        sanctions:
          $ref: '#/components/schemas/KYTMeasurementState'
        labels:
          $ref: '#/components/schemas/KYTMeasurementState'
        exposures:
          $ref: '#/components/schemas/KYTMeasurementState'
        graph:
          $ref: '#/components/schemas/KYTMeasurementState'
        typologies:
          $ref: '#/components/schemas/KYTMeasurementState'
    KYTSanctionEvidence:
      type: object
      description: >-
        Fato de sanção verificado na base autorizada, distinto de rótulo ou score. Categorias
        adversas não provam sanção. `designation_start` e `designation_end` saem omitidos quando não
        foram medidos, e a data de publicação do dataset nunca preenche o início jurídico da
        designação.
      required:
        - subject
        - address
        - list
        - list_version
        - list_date
        - match_type
        - source
        - observed_at
      properties:
        subject:
          type: string
          enum:
            - counterparty
        address:
          type: string
          description: Endereço consultado ao qual o fato se vincula.
        list:
          type: string
          example: OPENSANCTIONS
          description: Lista de origem do fato. Nos fixtures do sandbox vale `SANDBOX_FIXTURE`.
        list_version:
          type: string
          description: >-
            Versão da lista fornecida pela fonte. Nos fixtures do sandbox é o identificador do
            catálogo `sandbox-fixtures-v1`.
        list_date:
          type: string
          format: date-time
          description: Data fornecida pela lista, e não o horário da consulta.
        match_type:
          type: string
          enum:
            - EXACT_ADDRESS
        source:
          type: string
          description: >-
            Origem do fato. Em produção vale `SENTINEX_SANCTIONS_DATABASE`; nos fixtures do sandbox
            vale `SENTINEX_SANDBOX`.
        observed_at:
          type: string
          format: date-time
          description: Instante da consulta que observou o fato.
        program:
          type: string
          description: Programa da designação na fonte oficial. Omitido quando não medido.
        designation_start:
          type: string
          format: date-time
          description: >-
            Início inclusivo da designação, em UTC. Omitido enquanto a fonte não fornecer o
            intervalo; os produtores atuais ainda não o preenchem.
        designation_end:
          type: string
          format: date-time
          description: >-
            Fim exclusivo da designação, quando informado pela fonte. Omitido enquanto a fonte não
            fornecer o intervalo.
    SanctionsSourceCoverage:
      type: object
      description: >-
        Estado de uma fonte de sanções configurada na consulta. Dado não medido não publica contagem
        zero nem data zero. A cobertura de fontes é independente da cobertura do motor de regras
        (`coverage`).
      required:
        - source
        - state
      properties:
        source:
          type: string
          example: OPENSANCTIONS
        state:
          $ref: '#/components/schemas/KYTMeasurementState'
        dataset:
          type: string
        dataset_version:
          type: string
        published_at:
          type: string
          format: date-time
          description: Data de publicação informada pela fonte.
        loaded_at:
          type: string
          format: date-time
          description: Última leitura completa da fonte.
        count:
          type: integer
          format: int64
          minimum: 1
          description: Quantidade de endereços da versão. Omitido quando não medido.
        reason:
          type: string
          description: Motivo de indisponibilidade, como `stale` ou `snapshot_unavailable`.
    KYTScoreLedger:
      type: object
      description: >-
        Reconstrução do score. Comece por `base`, aplique cada item sobre o valor `before` para
        obter `after` (`effective` é `after` menos `before`), limite o resultado ao intervalo
        `minimum` a `maximum` e quantize uma única vez. O resultado é `final`, igual ao `risk_score`
        da resposta. O item de origem `deep_lane_adjustment`, quando presente, explicita a diferença
        entre o impacto total devolvido pelo avaliador de regras e as contribuições individuais
        disponíveis.
      required:
        - base
        - items
        - minimum
        - maximum
        - quantization
        - final
      properties:
        base:
          type: number
        minimum:
          type: number
          enum:
            - 0
        maximum:
          type: number
          enum:
            - 100
        quantization:
          type: string
          enum:
            - HALF_UP
        final:
          type: integer
          minimum: 0
          maximum: 100
        items:
          type: array
          items:
            type: object
            required:
              - source
              - operation
              - before
              - operand
              - effective
              - after
            properties:
              source:
                type: string
              operation:
                type: string
                enum:
                  - MAX
                  - ADD
                  - FLOOR
              before:
                type: number
              operand:
                type: number
              effective:
                type: number
              after:
                type: number
    KYTShadowObservation:
      type: object
      description: >-
        Observação de regra em modo sombra. Não entra no score nem nos hits que sustentam a
        recomendação, e leva o `screening_id` criado antes da avaliação.
      required:
        - screening_id
        - rule_id
        - rule_name
        - triggered
        - score_impact
      properties:
        screening_id:
          type: string
          format: uuid
        rule_id:
          type: string
        rule_name:
          type: string
        triggered:
          type: boolean
        score_impact:
          type: number
    VASPScreeningDecision:
      type: object
      required:
        - screening_id
        - screen_status
        - recommendation
        - client_decision
        - risk_score
        - risk_level
        - sanctions
        - rule_hits
        - coverage
        - obligations
        - amount_brl
        - fail_closed
        - latency_ms
        - evaluated_at
        - api_version
      properties:
        screening_id:
          type: string
          format: uuid
        operation_at:
          type: string
          format: date-time
          description: >-
            Data efetivamente usada na consulta de sanção por intervalo de designação, em UTC.
            Análise gravada antes desta data existir não traz o campo.
        operation_time_source:
          type: string
          enum:
            - OPERATION_AT
            - BLOCK_TIME
            - RECEIVED_AT
          description: >-
            Origem de `operation_at`, em ordem de precedência: `OPERATION_AT` (enviada pelo
            cliente), `BLOCK_TIME` (data do bloco da transação, quando o servidor já a mediu) e
            `RECEIVED_AT` (instante em que a requisição foi recebida).
        client_transaction_id:
          type: string
        customer_id:
          type: string
          format: uuid
          description: >-
            UUID informado na análise original. Presente junto de `holder_resolution`, e ausente
            quando a requisição não trouxe `customer_id`.
        screen_status:
          type: string
          enum:
            - completed
        recommendation:
          type: string
          enum:
            - ALLOW
            - REVIEW
            - HOLD
            - DENY
          description: |
            Recomendação auxiliar da Sentinex. Não é decisão operacional do cliente.
            `DENY` somente com base em sanção ou por regra de bloqueio configurada
            pela própria empresa; os demais riscos saem `REVIEW`.
          example: REVIEW
        client_decision:
          $ref: '#/components/schemas/KYTClientDecision'
        risk_score:
          type: integer
          minimum: 0
          maximum: 100
          example: 70
          description: >-
            Score final limitado a 0 a 100 e arredondado uma única vez, com meio para cima. É o
            mesmo valor de `score_ledger.final` e do registro escalar da análise. A repetição
            idempotente e a consulta devolvem o snapshot gravado na análise original.
        risk_level:
          type: string
          description: Faixa de risco em caixa alta.
          example: HIGH
        sanctions:
          type: object
          required:
            - hit
          description: >-
            Resultado da triagem de sanções da contraparte. `hit` e `state` distinguem ausência de
            medição de resultado negativo: `hit` só sai booleano quando `state` é `MEASURED`, e sai
            nulo quando a sanção não foi medida. Categorias adversas não provam sanção.
          properties:
            hit:
              type: boolean
              nullable: true
              description: >-
                Nulo quando `state` é `NOT_MEASURED`. Falso com `state` `MEASURED` é consulta
                negativa medida; nulo nunca vale como limpo.
            state:
              $ref: '#/components/schemas/KYTMeasurementState'
            categories:
              type: array
              description: Ausente quando vazio, nunca nulo.
              items:
                type: string
            provider:
              type: string
            provider_status:
              type: string
            evidence:
              type: array
              nullable: true
              description: >-
                Fatos de sanção verificados para a contraparte. Nulo ou vazio quando não há fato
                verificado.
              items:
                $ref: '#/components/schemas/KYTSanctionEvidence'
            coverage:
              type: array
              nullable: true
              description: Estado de cada fonte de sanções configurada nesta consulta.
              items:
                $ref: '#/components/schemas/SanctionsSourceCoverage'
        capabilities:
          $ref: '#/components/schemas/KYTMeasuredCapabilities'
        label_evidence:
          type: array
          description: >-
            Etiquetas de bloqueio de emissor de stablecoin (USDT e USDC) lidas de eventos públicos
            do contrato. Omitido quando nenhum evento foi coletado para o endereço: ausência de
            evento não prova que o endereço esteja livre. Efeito sempre `LABEL_ONLY`, nunca sanção,
            nunca score.
          items:
            $ref: '#/components/schemas/KYTIssuerBlockLabel'
        direct_exposure:
          type: object
          nullable: true
          additionalProperties:
            type: number
          description: >-
            Exposição direta por categoria. Nulo quando `capabilities.exposures` é `NOT_MEASURED`,
            sem percentual fabricado a partir de rótulo.
        indirect_exposure:
          type: object
          nullable: true
          additionalProperties:
            type: number
          description: Exposição indireta por categoria. Nulo quando `capabilities.exposures` é `NOT_MEASURED`.
        rule_hits:
          type: array
          items:
            type: object
            required:
              - rule_id
              - name
              - severity
              - weight
            properties:
              rule_id:
                type: string
              name:
                type: string
              severity:
                type: string
              weight:
                type: number
                format: double
        coverage:
          $ref: '#/components/schemas/RuleCoverageSnapshot'
        not_covered:
          type: array
          description: >-
            Dimensões que esta versão não analisa por desenho. Cada item traz `rule_id`, `source` e
            `cause`. Triagens gravadas antes desta versão não trazem o campo. `ALLOW` com entradas
            aqui significa que aquelas dimensões não foram analisadas, e não que o resultado delas
            foi negativo. Falha real do provedor, timeout ou erro de consulta continua em
            `coverage.degraded_rules` e retém a triagem; não entra neste campo.
          items:
            type: object
            additionalProperties: false
            required:
              - rule_id
              - source
              - cause
            properties:
              rule_id:
                type: string
                maxLength: 80
              source:
                type: string
                maxLength: 80
              cause:
                type: string
                enum:
                  - CAPABILITY_NOT_OFFERED
                  - NETWORK_NOT_COVERED
        obligations:
          type: object
          required:
            - travel_rule
            - coaf_sar
            - decripto
          properties:
            travel_rule:
              type: string
              enum:
                - UNKNOWN
                - REQUIRED
                - NOT_REQUIRED
            coaf_sar:
              type: string
              description: >-
                Campo legado. O motor devolve `MONITOR` e não produz `REPORTABLE` por limiar
                monetário, portanto o valor não comprova obrigação objetiva de comunicação nem
                decisão de comunicar suspeição. Use `coaf_objective` para o estado objetivo
                (prévia).
            decripto:
              type: string
            coaf_objective:
              $ref: '#/components/schemas/CoafObjectivePreview'
        amount_brl:
          type: number
          format: double
          nullable: true
          description: >-
            Valor em reais, não em centavos, calculado pelo servidor. Nulo quando a quantidade ou a
            cotação não estão disponíveis ou a cotação está vencida, o que retém a análise em `HOLD`
            temporário.
          example: 7500.5
        declared_fiat_value_brl:
          type: number
          format: double
          nullable: true
          description: >-
            Valor declarado pelo integrador em `fiat_value_brl`, preservado para conferência. Não
            decide regras nem recomendação.
        score_ledger:
          $ref: '#/components/schemas/KYTScoreLedger'
        shadow_observations:
          type: array
          items:
            $ref: '#/components/schemas/KYTShadowObservation'
        fail_closed:
          type: boolean
        reasons:
          type: array
          items:
            type: string
        latency_ms:
          type: integer
          format: int64
          minimum: 0
          description: Latência total em milissegundos.
        evaluated_at:
          type: string
          format: date-time
          description: Data e hora em RFC 3339, UTC.
          example: '2026-09-21T13:40:45Z'
        api_version:
          type: string
          enum:
            - v1
        holder_resolution:
          type: string
          enum:
            - RESOLVED
            - NOT_FOUND
          description: |
            Obrigatório na resposta de sucesso quando a requisição contém
            `customer_id`, independentemente do adicional. `RESOLVED` significa
            que o titular pertence à empresa autenticada; não atesta enriquecimento.
            `NOT_FOUND` inclui UUID desconhecido e UUID de outra empresa, sem
            alterar recomendação, score, motivos ou cobrança. Com cruzamento
            desligado, nenhuma fonte de KYC é lida. Sem `customer_id`, o campo
            não vem.
        transaction:
          type: object
          description: |
            Presente somente depois de `PATCH /vasp/screenings/{id}/tx-hash`. O hash
            vindo no `POST` original não gera este objeto.
          properties:
            tx_hash:
              type: string
            attached_at:
              type: string
              format: date-time
            origin:
              type: string
              enum:
                - TX_HASH_PATCH
    VASPScreeningLegacyDecision:
      type: object
      description: Projeção mínima para linha N-1 sem snapshot de resposta.
      required:
        - screening_id
        - recommendation
        - client_decision
        - risk_score
        - risk_level
        - amount_brl
        - created_at
        - api_version
      properties:
        screening_id:
          type: string
          format: uuid
        recommendation:
          type: string
          enum:
            - ALLOW
            - REVIEW
            - HOLD
            - DENY
        client_decision:
          $ref: '#/components/schemas/KYTClientDecision'
        risk_score:
          type: integer
        risk_level:
          type: string
        amount_brl:
          type: number
          format: double
        created_at:
          type: string
          format: date-time
        api_version:
          type: string
          enum:
            - v1
    KYTClientDecision:
      type: object
      required:
        - status
      description: Decisão e desfecho atribuídos exclusivamente ao cliente.
      properties:
        status:
          type: string
          enum:
            - PENDING_CLIENT_DECISION
            - RECORDED
        authority:
          type: string
          enum:
            - CLIENT_EXCHANGE
        operational_action:
          type: string
          enum:
            - ALLOW
            - HOLD
            - DENY
        suspicion_outcome:
          type: string
          enum:
            - SUSPICIOUS
            - NOT_SUSPICIOUS
        rationale:
          type: string
        recorded_at:
          type: string
          format: date-time
          description: Data e hora em RFC 3339, UTC.
    KYTTxHashAttachRequest:
      type: object
      additionalProperties: false
      required:
        - tx_hash
      properties:
        tx_hash:
          type: string
          minLength: 1
          maxLength: 128
          description: >
            Hash da transação transmitida. O formato é validado conforme a rede do screening (EVM:
            32 bytes em hex, com ou sem `0x`, gravado com `0x` e minúsculo; Tron, Bitcoin e demais
            redes de hash hex: 64 hex minúsculo sem `0x`; Solana: base58 de 64 a 90 caracteres com a
            caixa preservada; outras redes: identificador opaco de 32 a 128 caracteres). A grafia
            canônica é a gravada e a devolvida.
    KYTTxHashAttachResponse:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
        idempotent_replay:
          type: boolean
          description: Presente e verdadeiro quando o hash já estava gravado e é o mesmo.
        data:
          type: object
          required:
            - screening_id
            - tx_hash
            - tx_hash_state
            - origin
            - recorded_at
            - on_chain_verification
            - recommendation
            - client_decision
          properties:
            screening_id:
              type: string
              format: uuid
            tx_hash:
              type: string
            tx_hash_state:
              type: string
              enum:
                - ATTACHED
            origin:
              type: string
              enum:
                - TX_HASH_PATCH
                - SCREENING_REQUEST
              description: >
                `TX_HASH_PATCH` quando o hash veio deste anexo; `SCREENING_REQUEST` quando já veio
                no `POST /vasp/screenings`.
            recorded_at:
              type: string
              format: date-time
              nullable: true
              description: Instante do anexo; nulo quando o hash veio no POST original.
            on_chain_verification:
              type: string
              enum:
                - NOT_MEASURED
              description: A existência da transação na rede não é medida pela Sentinex neste passo.
            recommendation:
              type: string
              enum:
                - ALLOW
                - REVIEW
                - HOLD
                - DENY
              description: A recomendação original, inalterada.
            client_decision:
              $ref: '#/components/schemas/KYTClientDecision'
    KYTClientDecisionRequest:
      type: object
      required:
        - client_decision
      properties:
        client_decision:
          type: object
          required:
            - operational_action
            - suspicion_outcome
          properties:
            operational_action:
              type: string
              enum:
                - ALLOW
                - HOLD
                - DENY
            suspicion_outcome:
              type: string
              enum:
                - SUSPICIOUS
                - NOT_SUSPICIOUS
            rationale:
              type: string
              maxLength: 4000
    KYTClientDecisionResponse:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          enum:
            - true
        data:
          type: object
          required:
            - screening_id
            - recommendation
            - client_decision
          properties:
            screening_id:
              type: string
              format: uuid
            recommendation:
              type: string
              enum:
                - ALLOW
                - REVIEW
                - HOLD
                - DENY
              description: Recomendação original da Sentinex, que não é alterada pela decisão do cliente.
            client_decision:
              $ref: '#/components/schemas/KYTClientDecision'
    KYTReportRequest:
      type: object
      required:
        - wallet
      properties:
        profile:
          type: string
          enum:
            - operational
            - complete
            - regulatory
        wallet:
          type: object
          required:
            - address
            - blockchain
          properties:
            address:
              type: string
              minLength: 1
            blockchain:
              type: string
              description: Identificador da rede em minúsculas.
              example: ethereum
    KYTReportDocumentV2:
      type: object
      additionalProperties: false
      required:
        - report_id
        - case_id
        - profile
        - metadata
        - sections
      description: >-
        Projeção do perfil emitido, sem o snapshot factual privado. Não é compatível com
        KYTReportJSON v1.
      properties:
        report_id:
          type: string
        case_id:
          type: string
        profile:
          type: string
          enum:
            - operacional
            - completo
            - regulatorio
        metadata:
          type: object
          additionalProperties:
            type: string
        sections:
          type: array
          items:
            type: object
            required:
              - narrative
              - tables
            properties:
              narrative:
                type: object
                additionalProperties: true
              tables:
                type: array
                nullable: true
                items:
                  type: object
                  properties:
                    headers:
                      type: array
                      items:
                        type: string
                    rows:
                      type: array
                      items:
                        type: array
                        items:
                          type: string
        graph:
          type: object
          additionalProperties: true
          description: Grafo público, omitido no perfil operacional.
    KYTReportJSON:
      type: object
      required:
        - api_version
        - service_role
        - report
        - case
        - verdict
        - conclusion
        - client_decision
      description: |
        Laudo KYT estruturado. Campos monetários estão em unidades decimais e datas
        usam RFC 3339 UTC.

        `report.id` é o identificador único da emissão (ULID, cujo instante é o da
        emissão) e é o mesmo do nome do arquivo e da auditoria. `case.case_id` é o
        agrupador estável da carteira e vem separado, na forma
        `KYT-WALLET-<REDE>-<hash do endereço canônico>`, com a rede em maiúsculas no
        nome canônico (`ETHEREUM`, não o apelido digitado) e o hash sobre o endereço
        na forma canônica: grafias do mesmo endereço (checksum ou minúsculo) e da
        mesma rede dão o mesmo `case_id`. Compatibilidade: laudos emitidos antes da
        2.16.0 e lidos do histórico trazem o `case_id` na forma antiga, calculada
        sobre a rede e o endereço como digitados, e `report.id` igual ao `case_id`.
        A mesma carteira pode, por isso, aparecer em dois grupos (um antigo e um
        novo) até o histórico anterior expirar. Não una os dois pelo texto do
        `case_id`.

        Valores em reais (`*_brl`) só existem com cotação completa. Sem ela,
        `scope.brl_status` é `nao_cotado` e nenhum campo em reais aparece no JSON:
        trate a ausência como "não cotado", nunca como zero.
      properties:
        api_version:
          type: string
          enum:
            - v1
        service_role:
          type: string
          enum:
            - AUXILIARY_ANALYSIS_SERVICE
        report:
          type: object
          additionalProperties: true
          properties:
            id:
              type: string
              pattern: ^[0-9A-HJKMNP-TV-Z]{26}$
              description: |
                Identificador único da emissão (ULID). Até a 2.15.x repetia o
                `case_id`.
            case_id:
              type: string
              description: Mesmo valor de `case.case_id`.
        case:
          type: object
          additionalProperties: true
          properties:
            case_id:
              type: string
              example: KYT-WALLET-ETHEREUM-0000000000000000
        scope:
          $ref: '#/components/schemas/KYTReportScope'
        monitoring:
          $ref: '#/components/schemas/KYTReportMonitoring'
        attribution:
          type: array
          items:
            type: object
            additionalProperties: true
            properties:
              vasp:
                type: boolean
                nullable: true
                description: |
                  `true`, `false` ou `null` (não determinado). Contraparte observada
                  on-chain sem atribuição de entidade traz `null`: nunca leia
                  ausência de classificação como "não é VASP".
        exposure_summary:
          type: array
          items:
            type: object
            additionalProperties: true
            properties:
              direct_brl:
                type: number
                format: double
              indirect_brl:
                type: number
                format: double
              total_brl:
                type: number
                format: double
        hop_ladder:
          type: array
          items:
            type: object
            additionalProperties: true
            properties:
              volume_brl:
                type: number
                format: double
        source_of_funds:
          type: array
          items:
            type: object
            additionalProperties: true
            properties:
              value_brl:
                type: number
                format: double
        destination_of_funds:
          type: array
          items:
            type: object
            additionalProperties: true
            properties:
              value_brl:
                type: number
                format: double
        verdict:
          type: object
          required:
            - recommendation
            - risk_tier
            - score_total
          properties:
            recommendation:
              type: string
              enum:
                - ALLOW
                - REVIEW
                - HOLD
                - DENY
            risk_tier:
              type: string
            score_total:
              type: integer
              minimum: 0
              maximum: 100
          additionalProperties: true
        conclusion:
          type: object
          required:
            - recommendation
          properties:
            recommendation:
              type: string
              enum:
                - ALLOW
                - REVIEW
                - HOLD
                - DENY
          additionalProperties: true
        client_decision:
          $ref: '#/components/schemas/KYTClientDecision'
      additionalProperties: true
    KYTReportScope:
      type: object
      additionalProperties: true
      description: Escopo do laudo (carteiras, redes, janela e valores).
      properties:
        brl_status:
          type: string
          enum:
            - medido
            - nao_cotado
          description: |
            Estado de TODOS os valores em reais do laudo. `medido` exige cotação
            completa (`brl_quote`). Em `nao_cotado`, nenhum campo `*_brl`
            (`volume_brl`, `direct_brl`, `indirect_brl`, `total_brl`, `value_brl`)
            sai no JSON e o documento imprime `NÃO COTADO`, nunca `R$ 0,00`.
        brl_quote:
          $ref: '#/components/schemas/KYTReportBRLQuote'
        volume_brl:
          type: number
          format: double
          description: Ausente quando `brl_status` é `nao_cotado`.
    KYTReportBRLQuote:
      type: object
      description: |
        Cotação que sustenta os valores em reais. Presente somente com os quatro
        elementos; faltando qualquer um, `brl_status` é `nao_cotado`.
      required:
        - origin_currency
        - rate
        - source
        - quoted_at
      properties:
        origin_currency:
          type: string
        rate:
          type: string
          description: Taxa em texto decimal, sem arredondamento.
        source:
          type: string
        quoted_at:
          type: string
          format: date-time
    KYTReportMonitoring:
      type: object
      description: |
        Estado de monitoramento da carteira comprovado por uma fonte nesta emissão.
        Ausente significa que nenhuma fonte o comprovou, e o documento imprime
        `NÃO COMPROVADO NESTA EMISSÃO` em vez de afirmar monitoramento contínuo.
      required:
        - status
        - source
        - observed_at
      properties:
        status:
          type: string
        source:
          type: string
        observed_at:
          type: string
          format: date-time
    KYTIdentityUnavailableResponse:
      description: >
        Identidade indisponível: o envelope público de erro acrescido de `data.recommendation`
        `HOLD`

        e `data.fail_closed` `true`. Nenhuma análise é criada, nenhuma regra é avaliada e nenhum
        consumo é registrado.
      type: object
      required:
        - success
        - error
        - data
      properties:
        success:
          type: boolean
          enum:
            - false
        data:
          type: object
          required:
            - recommendation
            - fail_closed
          properties:
            recommendation:
              type: string
              enum:
                - HOLD
            fail_closed:
              type: boolean
              enum:
                - true
        error:
          type: object
          required:
            - code
            - http_status
            - message
            - action
            - retriable
            - retry_after_seconds
          properties:
            code:
              type: string
              enum:
                - KYT_IDENTITY_UNAVAILABLE
            http_status:
              type: integer
              enum:
                - 503
            message:
              type: string
            action:
              type: string
            retriable:
              type: boolean
              enum:
                - true
            retry_after_seconds:
              type: integer
              enum:
                - 30
            request_id:
              type: string
    KYTPublicErrorResponse:
      type: object
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          enum:
            - false
        error:
          type: object
          required:
            - code
            - http_status
            - message
            - action
            - retriable
          properties:
            code:
              type: string
              example: KYT_DEPENDENCY_UNAVAILABLE
            http_status:
              type: integer
              example: 503
            message:
              type: string
            action:
              type: string
            retriable:
              type: boolean
            retry_after_seconds:
              type: integer
              minimum: 1
            request_id:
              type: string
            reason:
              type: string
              enum:
                - address_chain_mismatch
              description: |
                Causa do 400 `KYT_WALLET_INVALID` no laudo: `address_chain_mismatch` diz
                que o endereço não é válido para a rede informada (rede desconhecida,
                checksum inválido ou forma de outra rede). O par é validado antes de
                qualquer consulta externa. Ausente nos demais erros.
    VASPScreeningGetResponse:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          enum:
            - true
        data:
          oneOf:
            - $ref: '#/components/schemas/VASPScreeningDecision'
            - $ref: '#/components/schemas/VASPScreeningLegacyDecision'
    TenantAccessRefusalResponse:
      type: object
      required:
        - success
        - error
        - code
      properties:
        success:
          type: boolean
          enum:
            - false
        error:
          type: string
          example: >-
            Seu período de teste terminou. Fale com a Sentinex Risk para estender o teste ou
            contratar um plano.
        code:
          type: string
          enum:
            - TRIAL_EXPIRED
            - TENANT_SUSPENDED
            - TENANT_NOT_OPERATIONAL
            - API_ACCESS_DISABLED
            - TENANT_ACCESS_DENIED
            - TENANT_ACCESS_STATE_UNAVAILABLE
        details:
          type: object
          properties:
            reason:
              type: string
              enum:
                - TRIAL_WITHOUT_END_DATE
                - TRIAL_ENDED
                - POC_TRIAL_RETIRED
                - DEMO_WITHOUT_COMMERCIAL_ACCESS
                - NO_COMMERCIAL_ACCESS
              description: Presente em `TRIAL_EXPIRED`.
            trial_ends_at:
              type: string
              format: date-time
              description: Presente em `TRIAL_EXPIRED` quando o trial tem data.
            lifecycle_state:
              type: string
              description: Presente em `empresa_NOT_OPERATIONAL`.
