# OpenAPI da API pública v1 do CardSign, derivado do código (frontend/src/lib/api/**, lib/webhooks/**).
# Fonte única da página /docs/api e das coleções Postman. Depois de editar, rode:
#   node frontend/scripts/gerar-docs-api.mjs
# (gera frontend/src/lib/api/docs/openapi.gerado.json, postman/*.json e a cópia em frontend/public/docs/api/postman).
# O teste frontend/src/test/apiOpenapi.test.ts confere este arquivo com o roteador (ROTAS) e com CODIGOS_ERRO.
openapi: 3.1.0
info:
  title: CardSign API
  version: "1.0.1"
  summary: Assinatura eletrônica multiempresa, servidor a servidor.
  description: |
    API REST do CardSign para criar, enviar e acompanhar documentos para assinatura eletrônica a partir do seu
    sistema, baixar o PDF final selado, a trilha de auditoria e o pacote de provas, e receber webhooks assinados a
    cada passo.

    - **Base:** `https://cardsign.gopixeltecnologiaa.com.br/api/v1`
    - **Formato:** JSON UTF-8; datas ISO 8601 em UTC; ids são UUID.
    - **Limite:** 120 chamadas por minuto por chave (acima disso, `429` com `retry-after`).
    - **Escrita:** todo `POST` exige o cabeçalho `Idempotency-Key`.
  contact:
    name: CardSign · Grupo Dias
    url: https://cardsign.gopixeltecnologiaa.com.br/docs/api
  x-limite-por-minuto: 120
  x-codigos-erro:
    - { status: 400, codigo: sem_idempotency_key, quando: "POST de escrita sem o cabeçalho Idempotency-Key" }
    - { status: 400, codigo: json_invalido, quando: "Corpo que não é JSON (ou multipart sem o campo dados)" }
    - { status: 400, codigo: parametro_invalido, quando: "Filtro da lista inválido" }
    - { status: 400, codigo: cursor_invalido, quando: "\"depois\" que não veio de \"proximo\"" }
    - { status: 400, codigo: pagina_alta_demais, quando: "pagina × limite acima de 10.000 (use o cursor)" }
    - { status: 401, codigo: sem_chave, quando: "Sem Authorization: Bearer cs_…" }
    - { status: 401, codigo: chave_invalida, quando: "Chave que não existe" }
    - { status: 401, codigo: chave_revogada, quando: "Chave revogada" }
    - { status: 401, codigo: chave_expirada, quando: "Chave vencida" }
    - { status: 401, codigo: chave_sem_responsavel, quando: "Quem criou a chave saiu do grupo" }
    - { status: 403, codigo: ip_nao_autorizado, quando: "IP fora da lista da chave" }
    - { status: 403, codigo: sem_escopo, quando: "A chave não tem o escopo da rota" }
    - { status: 403, codigo: sem_permissao, quando: "Quem criou a chave não tem a permissão (nesta empresa)" }
    - { status: 404, codigo: nao_encontrado, quando: "Rota que não existe" }
    - { status: 404, codigo: documento_nao_encontrado, quando: "Documento/envelope fora do alcance da chave" }
    - { status: 404, codigo: signatario_nao_encontrado, quando: "Pessoa que não é deste envelope" }
    - { status: 404, codigo: webhook_nao_encontrado, quando: "Destino de webhook de outro grupo ou removido" }
    - { status: 404, codigo: entrega_nao_encontrada, quando: "Entrega de outro destino" }
    - { status: 405, codigo: metodo, quando: "Método não aceito nesta rota (veja Allow)" }
    - { status: 409, codigo: nao_concluido, quando: "PDF final ou provas antes da conclusão" }
    - { status: 409, codigo: estado_invalido, quando: "Ação que a situação do envelope não permite" }
    - { status: 409, codigo: external_id_em_uso, quando: "external_id já usado por esta chave (detalhes traz o envelope_id)" }
    - { status: 409, codigo: idempotencia_em_andamento, quando: "A mesma Idempotency-Key ainda está sendo processada" }
    - { status: 413, codigo: grande_demais, quando: "Corpo acima de 30 MB, PDF acima de 20 MB ou PDFs do pedido somando mais de 30 MB" }
    - { status: 415, codigo: tipo_nao_suportado, quando: "Content-Type que não é JSON nem multipart/form-data" }
    - { status: 422, codigo: validacao, quando: "Campos inválidos (veja detalhes)" }
    - { status: 422, codigo: idempotencia_conflito, quando: "Idempotency-Key repetida com outro corpo" }
    - { status: 422, codigo: arquivo_invalido, quando: "Arquivo que não é PDF (%PDF) ou está corrompido" }
    - { status: 422, codigo: download_falhou, quando: "Não deu para baixar o PDF da URL informada" }
    - { status: 422, codigo: url_invalida, quando: "URL que não é https pública" }
    - { status: 422, codigo: ancora_nao_encontrada, quando: "Texto da âncora não está no PDF" }
    - { status: 422, codigo: regra_violada, quando: "Regra do CardSign (ex.: falta e-mail, falta campo de assinatura)" }
    - { status: 429, codigo: limite, quando: "Mais de 120 chamadas por minuto (veja retry-after)" }
    - { status: 500, codigo: erro_interno, quando: "Erro inesperado" }
    - { status: 503, codigo: indisponivel, quando: "Não deu para conferir o acesso agora, servidor ocupado lendo PDFs ou pedido acima de 90 s (veja retry-after)" }
    - { status: 503, codigo: trilha_indisponivel, quando: "Não deu para ler a trilha inteira agora" }
  x-changelog:
    - data: "2026-10-21"
      versao: "1.0.1"
      itens:
        - "Webhooks: mudar url ou empresas (PATCH /webhooks/{id}) faz do dono da chave o responsável pelo destino e troca o segredo, que volta na resposta (campo segredo) só nessa vez. Novo POST /webhooks/{id}/segredo para trocar o segredo."
        - "Webhooks: gerir pela API exige que quem criou a chave tenha org.api com alcance de grupo (a mesma regra da tela). Reenvio só de entregas que o responsável de agora poderia receber; pendentes há mais de 72 h viram falhou."
        - "PDFs: no máximo 30 MB somando todos os arquivos de um pedido (413 grande_demais) e 90 s para baixar e ler (503 indisponivel com retry-after). Com o servidor ocupado lendo PDFs de outros pedidos, 503 indisponivel com retry-after."
        - "Idempotency-Key no multipart: o hash é do campo dados (JSON canônico) e do nome + SHA-256 de cada arquivo, não do corpo bruto (o boundary não conta). Reserva sem resposta só é liberada depois de 10 min."
    - data: "2026-10-20"
      versao: "1.0.0"
      itens:
        - "API de escrita: criar envelope (JSON ou multipart), arquivos por URL ou base64, signatários com campos por posição ou âncora, enviar (com devolver_links), cancelar, pausar, retomar e lembrete."
        - "Webhooks assinados (HMAC-SHA256) com reenvio automático, teste (ping), histórico de entregas e reenvio manual."
        - "Novas leituras: /eu, /empresas, /modelos, /envelopes (com external_id) e /envelopes/{id}/provas."
        - "Idempotency-Key obrigatória em todo POST; paginação por cursor em /documentos e /envelopes."
        - "Documentação pública em /docs/api, OpenAPI 3.1 e coleções Postman."
    - data: "2026-09-01"
      versao: "0.9.0"
      itens:
        - "API de leitura: /documentos, /documentos/{id}, /documentos/{id}/trilha e /documentos/{id}/pdf."
servers:
  - url: https://cardsign.gopixeltecnologiaa.com.br/api/v1
    description: Produção
  - url: http://localhost:8080/api/v1
    description: Sandbox (CardSign local, `npm run dev`; ambiente de testes hospedado ainda não existe)
security:
  - chaveApi: []
tags:
  - name: Autorização
    description: |
      Toda chamada leva a chave no cabeçalho `Authorization: Bearer cs_<prefixo>_<segredo>`. A chave é criada em
      **Configurações › API** e mostrada uma única vez; o CardSign guarda só o SHA-256 dela.

      A chave **age em nome de quem a criou**: em cada chamada valem as permissões que essa pessoa tem *agora* no
      painel, empresa por empresa. Cada rota exige um escopo (`x-escopo` em cada operação), e cada escopo exige
      uma permissão do painel de quem criou a chave.

      Tirar a permissão no painel leva a chave junto em até 30 s. A chave pode ter validade e lista de IPs
      permitidos (IPv4 ou CIDR).
  - name: Envelopes
    description: |
      Um **envelope** é o documento a assinar: um ou mais PDFs, as pessoas (signatários) e os campos de cada uma.
      Ele nasce como `rascunho`, vai para `andamento` ao ser enviado e termina como `concluido`, `cancelado`,
      `expirado` ou `recusado` (`pausado` é temporário).
  - name: Arquivos
    description: |
      PDFs do envelope (até 20 MB cada, 20 por envelope). Mande por **URL https pública** (o CardSign baixa, sem
      seguir redirecionamento), em **base64** ou como **multipart** (`arquivo`). O servidor confere a assinatura
      `%PDF`, conta as páginas e calcula o SHA-256.
  - name: Signatários
    description: |
      Pessoas do envelope. Campos por **posição** (em % da página, `y` a partir do topo) ou por **âncora** (texto
      procurado nos PDFs, mesmo invisível; cada ocorrência vira um campo). Quem assina sem `campos` ganha uma caixa
      de assinatura automática na última página (até 30 pessoas, empilhadas por ordem).
  - name: Ações
    description: |
      Mudanças de situação do envelope. Todas passam pelas mesmas conferências do painel e ficam na trilha de
      auditoria.
  - name: Consultas
    description: |
      Leitura: listas paginadas, detalhe, trilha de auditoria encadeada, PDF final selado e pacote de provas.
      Os links de download valem 5 minutos.
  - name: Webhooks
    description: |
      Destinos que recebem um `POST` a cada evento. No máximo 20 por grupo. O segredo `whsec_…` volta **só na
      criação** (e quando é trocado): guarde-o para conferir o cabeçalho `CardSign-Signature`.

      Gerir webhooks exige o escopo `webhooks:gerir` **e** que quem criou a chave tenha `org.api` com alcance de
      grupo. Cada destino tem um responsável: ele só recebe eventos das empresas em que o responsável vê os
      documentos. Mudar `url` ou `empresas` faz do dono da chave o novo responsável e troca o segredo (que volta na
      resposta); as entregas pendentes que o novo responsável não poderia receber são descartadas.
paths:
  /:
    get:
      tags: [Autorização]
      operationId: indice
      summary: Índice da API
      description: Lista as rotas, as regras de paginação e de escrita. Não exige chave.
      security: []
      x-escopo: null
      responses:
        "200":
          description: Índice.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Indice" }
              example:
                api: CardSign
                versao: v1
                documentacao: https://cardsign.gopixeltecnologiaa.com.br/docs/api
                rotas: ["GET /api/v1/documentos", "POST /api/v1/envelopes", "GET /api/v1/envelopes/{id}"]
                paginacao:
                  cursor: Use "depois" com o valor de "proximo" da resposta anterior (ou siga "proxima_url"); "proximo" null = acabou.
                  limite: '"limite" de 1 a 200 (padrão 50).'
                escrita: "Todo POST exige o cabeçalho Idempotency-Key (1 a 200 caracteres): a mesma chave com o mesmo corpo em 24 h devolve a mesma resposta."
                cache: Permissões de quem criou a chave são conferidas no banco e guardadas por até 30 s.
  /eu:
    get:
      tags: [Autorização]
      operationId: eu
      summary: Quem sou eu
      description: A chave (escopos, validade), o grupo e as empresas em que ela age (união de todos os escopos), e o limite por minuto. Bom para testar a chave.
      x-escopo: null
      responses:
        "200":
          description: A chave e o alcance dela.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Eu" }
              example:
                chave:
                  id: 0e1d2c3b-4a59-4687-9a8b-7c6d5e4f3a2b
                  prefixo: a1b2c3d4
                  nome: Programa DIAS — produção
                  expira_em: "2027-10-20T02:59:59.000Z"
                  escopos: [documentos:ler, documentos:baixar, trilha:ler, envelopes:criar, envelopes:enviar, envelopes:gerir, provas:baixar, webhooks:gerir]
                grupo: { id: 9a2e7c41-5d3b-4e8f-b1a6-7c0d2f9e8b13, nome: Grupo Dias }
                empresas:
                  - { id: 3f6c1d2a-8b4e-4f1a-9c7d-2e5b8a1f4c90, nome: Clínica Dias Saúde Ocupacional, cnpj: "12345678000190" }
                  - { id: 4a7d2e3b-9c5f-4a2b-8d8e-3f6c9b2a5d01, nome: Clínica Dias Ipatinga, cnpj: "12345678000271" }
                limite_por_minuto: 120
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /empresas:
    get:
      tags: [Autorização]
      operationId: empresas
      summary: Empresas da chave
      description: Empresas do grupo em que a chave age (para qualquer um dos escopos dela), por nome.
      x-escopo: null
      responses:
        "200":
          description: Lista de empresas.
          content:
            application/json:
              schema:
                type: object
                required: [empresas]
                properties:
                  empresas: { type: array, items: { $ref: "#/components/schemas/Empresa" } }
              example:
                empresas:
                  - { id: 3f6c1d2a-8b4e-4f1a-9c7d-2e5b8a1f4c90, nome: Clínica Dias Saúde Ocupacional, cnpj: "12345678000190" }
                  - { id: 4a7d2e3b-9c5f-4a2b-8d8e-3f6c9b2a5d01, nome: Clínica Dias Ipatinga, cnpj: "12345678000271" }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /modelos:
    get:
      tags: [Autorização]
      operationId: modelos
      summary: Modelos de documento
      description: Modelos não arquivados das empresas liberadas (somente leitura; criar documento a partir de modelo fica para uma próxima versão).
      x-escopo: modelos:ler
      responses:
        "200":
          description: Lista de modelos.
          content:
            application/json:
              schema:
                type: object
                required: [modelos]
                properties:
                  modelos: { type: array, items: { $ref: "#/components/schemas/Modelo" } }
              example:
                modelos:
                  - id: 2b7e9c4d-1f3a-4b5c-8d6e-9f0a1b2c3d4e
                    nome: Termo de ciência de POP
                    tipo_id: 6d1c8e2f-4a5b-4c3d-9e8f-0a1b2c3d4e5f
                    versao: 3
                    empresa_id: 3f6c1d2a-8b4e-4f1a-9c7d-2e5b8a1f4c90
                    status: ativo
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /pastas:
    get:
      tags: [Autorização]
      operationId: pastas
      summary: Pastas
      description: |
        As pastas das empresas liberadas, com o caminho completo. Serve para o seu sistema ligar cada pasta do
        CardSign a uma pasta dele (o envelope traz `pasta.id`). `?empresa_id=` filtra uma empresa.
      x-escopo: documentos:ler
      parameters:
        - name: empresa_id
          in: query
          required: false
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Lista de pastas.
          content:
            application/json:
              schema:
                type: object
                required: [pastas]
                properties:
                  pastas: { type: array, items: { $ref: "#/components/schemas/Pasta" } }
              example:
                pastas:
                  - id: 7a1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e
                    empresa_id: 3f6c1d2a-8b4e-4f1a-9c7d-2e5b8a1f4c90
                    pai_id: null
                    codigo: "01"
                    nome: Recursos Humanos
                    caminho: Recursos Humanos
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /envelopes:
    get:
      tags: [Consultas]
      operationId: listarEnvelopes
      summary: Listar envelopes
      description: |
        Envelopes das empresas liberadas, mais recentes primeiro (`criado_em desc, id desc`). Igual a
        `GET /documentos`, com `external_id` em cada item e o filtro `external_id`. Rascunhos só aparecem com
        `status=rascunho`.
      x-escopo: documentos:ler
      parameters:
        - $ref: "#/components/parameters/Limite"
        - $ref: "#/components/parameters/Depois"
        - $ref: "#/components/parameters/Pagina"
        - $ref: "#/components/parameters/Contar"
        - $ref: "#/components/parameters/Status"
        - $ref: "#/components/parameters/Empresa"
        - $ref: "#/components/parameters/Desde"
        - $ref: "#/components/parameters/Ate"
        - $ref: "#/components/parameters/ExternalId"
      responses:
        "200":
          description: Uma página de envelopes.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ListaEnvelopes" }
              example:
                pagina: null
                limite: 50
                total: null
                proximo: "2026-10-19T13:42:07.481233+00:00,7b1e4c2d-9f3a-4d6e-8a5b-1c2d3e4f5a6b"
                proxima_url: "https://cardsign.gopixeltecnologiaa.com.br/api/v1/envelopes?limite=50&depois=2026-10-19T13%3A42%3A07.481233%2B00%3A00%2C7b1e4c2d-9f3a-4d6e-8a5b-1c2d3e4f5a6b"
                envelopes:
                  - id: 7b1e4c2d-9f3a-4d6e-8a5b-1c2d3e4f5a6b
                    titulo: "POP 014 — Higienização das mãos (ciência do colaborador)"
                    status: andamento
                    empresa_id: 3f6c1d2a-8b4e-4f1a-9c7d-2e5b8a1f4c90
                    criado_em: "2026-10-19T13:42:07.481233+00:00"
                    concluido_em: null
                    prazo: "2026-10-31T02:59:00+00:00"
                    external_id: "dias:pop:4821"
                    origem: api
        "400": { $ref: "#/components/responses/ParametroInvalido" }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
    post:
      tags: [Envelopes]
      operationId: criarEnvelope
      summary: Criar envelope
      description: |
        Cria o **rascunho completo** (arquivos, pessoas e campos) ou nada. Aceita JSON ou `multipart/form-data`
        (campo `dados` com o JSON e `arquivo` repetido com os PDFs). Depois, envie com
        `POST /envelopes/{id}/enviar`. `external_id` repetido para a mesma chave → `409 external_id_em_uso`, com o
        `envelope_id` existente em `detalhes`.
      x-escopo: envelopes:criar
      x-idempotente: true
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/EnvelopeCriar" }
            example:
              empresa_id: 3f6c1d2a-8b4e-4f1a-9c7d-2e5b8a1f4c90
              titulo: "POP 014 — Higienização das mãos (ciência do colaborador)"
              external_id: "dias:pop:4821"
              external_ref: { origem: programa-dias, pop: "014", turma: "2026-10" }
              prazo: "2026-10-30"
              mensagem: "Olá! Leia o POP de higienização das mãos e assine a ciência até sexta."
              arquivos:
                - { nome: POP-014-higienizacao-das-maos.pdf, url: "https://treinamento.gopixeltecnologia.com/arquivos/pop-014.pdf" }
              signatarios:
                - nome: Mariana Costa Ribeiro
                  email: mariana.ribeiro@clinicadias.com.br
                  cpf: "52998224725"
                  canal: email
                  nivel: avancada
                  campos:
                    - { tipo: assinatura, ancora: "{{assinatura:1}}" }
                - nome: Rafael Almeida Pinto
                  funcao: testemunha
                  ordem: 2
                  canal: whatsapp
                  email: rafael.pinto@clinicadias.com.br
                  telefone: "+55 31 99876-5432"
          multipart/form-data:
            schema:
              type: object
              required: [dados]
              properties:
                dados:
                  type: string
                  description: O mesmo JSON do corpo `application/json` (sem `arquivos`, ou com eles por URL/base64).
                arquivo:
                  type: array
                  description: Um ou mais PDFs (campo repetido).
                  items: { type: string, format: binary }
      responses:
        "201":
          description: Rascunho criado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Envelope" }
              examples: { exemplo: { $ref: "#/components/examples/EnvelopeRascunho" } }
        "400": { $ref: "#/components/responses/RequisicaoInvalida" }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "409": { $ref: "#/components/responses/ConflitoCriacao" }
        "413": { $ref: "#/components/responses/GrandeDemais" }
        "415": { $ref: "#/components/responses/TipoNaoSuportado" }
        "422": { $ref: "#/components/responses/ValidacaoArquivo" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /envelopes/{id}:
    parameters:
      - $ref: "#/components/parameters/Id"
    get:
      tags: [Envelopes]
      operationId: detalheEnvelope
      summary: Consultar envelope
      description: O envelope completo (rascunho incluído) — situação, arquivos, pessoas (contato mascarado) e PDFs finais com o link de verificação.
      x-escopo: documentos:ler
      responses:
        "200":
          description: O envelope.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Envelope" }
              examples: { exemplo: { $ref: "#/components/examples/EnvelopeConcluido" } }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/DocumentoNaoEncontrado" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
    patch:
      tags: [Envelopes]
      operationId: editarEnvelope
      summary: Editar rascunho
      description: Só no rascunho. Envie ao menos um de `titulo`, `prazo` (AAAA-MM-DD = 23:59 de Brasília, ou ISO 8601) e `mensagem`.
      x-escopo: envelopes:criar
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/EnvelopeEditar" }
            example: { titulo: "POP 014 — Higienização das mãos (revisão 2)", prazo: "2026-11-07" }
      responses:
        "200":
          description: O envelope atualizado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Envelope" }
              examples: { exemplo: { $ref: "#/components/examples/EnvelopeRascunho" } }
        "400": { $ref: "#/components/responses/RequisicaoInvalida" }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/DocumentoNaoEncontrado" }
        "409": { $ref: "#/components/responses/EstadoInvalido" }
        "415": { $ref: "#/components/responses/TipoNaoSuportado" }
        "422": { $ref: "#/components/responses/Validacao" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /envelopes/{id}/arquivos:
    parameters:
      - $ref: "#/components/parameters/Id"
    post:
      tags: [Arquivos]
      operationId: adicionarArquivo
      summary: Adicionar arquivo
      description: Só no rascunho. Um PDF por chamada — JSON `{nome, url}` ou `{nome, base64}`, ou multipart com o campo `arquivo`.
      x-escopo: envelopes:criar
      x-idempotente: true
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ArquivoEntrada" }
            example: { nome: Anexo-I-checklist.pdf, base64: "JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAuLi4=" }
          multipart/form-data:
            schema:
              type: object
              required: [arquivo]
              properties:
                arquivo: { type: string, format: binary, description: Um PDF. }
      responses:
        "201":
          description: Arquivo guardado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Arquivo" }
              example:
                id: 9d4e1f6a-2b3c-4d5e-8f9a-1b2c3d4e5f60
                nome: Anexo-I-checklist.pdf
                sha256: 4f1c9a2b7d3e8f60a1b2c3d4e5f60718293a4b5c6d7e8f9012a3b4c5d6e7f809
                paginas: 1
                ordem: 2
        "400": { $ref: "#/components/responses/RequisicaoInvalida" }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/DocumentoNaoEncontrado" }
        "409": { $ref: "#/components/responses/EstadoInvalidoIdempotente" }
        "413": { $ref: "#/components/responses/GrandeDemais" }
        "415": { $ref: "#/components/responses/TipoNaoSuportado" }
        "422": { $ref: "#/components/responses/ValidacaoArquivo" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /envelopes/{id}/signatarios:
    parameters:
      - $ref: "#/components/parameters/Id"
    post:
      tags: [Signatários]
      operationId: adicionarSignatario
      summary: Adicionar signatário
      description: Só no rascunho. Mesmo formato de cada item de `signatarios` em `POST /envelopes`. Sem `ordem`, entra no fim.
      x-escopo: envelopes:criar
      x-idempotente: true
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SignatarioEntrada" }
            example:
              nome: Juliana Ferreira Lopes
              funcao: aprovar
              canal: email
              email: juliana.lopes@clinicadias.com.br
              nivel: simples
              campos:
                - { tipo: assinatura, pagina: 1, x: 60, y: 82, largura: 26, altura: 7 }
                - { tipo: data, arquivo: 1, pagina: 1, x: 60, y: 90, largura: 14, altura: 3.5 }
      responses:
        "201":
          description: A pessoa criada.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Signatario" }
              example:
                id: 6f1a8b4c-3d5e-4f7a-9b0c-2d3e4f5a6b7c
                nome: Juliana Ferreira Lopes
                funcao: aprovar
                ordem: 3
                canal: email
                nivel: simples
                status: aguardando
                email_mascarado: j***@clinicadias.com.br
                telefone_mascarado: null
                assinado_em: null
                motivo_recusa: null
        "400": { $ref: "#/components/responses/RequisicaoInvalida" }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/DocumentoNaoEncontrado" }
        "409": { $ref: "#/components/responses/EstadoInvalidoIdempotente" }
        "415": { $ref: "#/components/responses/TipoNaoSuportado" }
        "422": { $ref: "#/components/responses/ValidacaoSignatario" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /envelopes/{id}/signatarios/{sid}:
    parameters:
      - $ref: "#/components/parameters/Id"
      - $ref: "#/components/parameters/Sid"
    delete:
      tags: [Signatários]
      operationId: removerSignatario
      summary: Remover signatário
      description: Só no rascunho. Remove a pessoa e os campos dela.
      x-escopo: envelopes:criar
      responses:
        "204": { description: Removida (sem corpo). }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/SignatarioNaoEncontrado" }
        "409": { $ref: "#/components/responses/EstadoInvalido" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /envelopes/{id}/enviar:
    parameters:
      - $ref: "#/components/parameters/Id"
    post:
      tags: [Ações]
      operationId: enviarEnvelope
      summary: Enviar envelope
      description: |
        As mesmas conferências do painel (e-mail ou telefone de quem assina, campo de assinatura, CPF…; falhou →
        `422 regra_violada` com a explicação) e os convites. Com `{"devolver_links": true}` e uma chave que também
        tem `envelopes:gerir`, a resposta traz `links` de quem está na vez, para abrir a assinatura no seu sistema
        (fica registrado na trilha).
      x-escopo: envelopes:enviar
      x-idempotente: true
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: "#/components/schemas/EnviarCorpo" }
            example: { devolver_links: true }
      responses:
        "202":
          description: >-
            Guardado para revisão (chave com "Revisar no CardSign antes de enviar"). O documento continua em
            `rascunho`, com `revisao_pendente` true e `url_painel`; uma pessoa revisa e envia no painel, e o envio
            chega pelo webhook `envelope.enviado`.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Envelope" }
        "200":
          description: Enviado. Com `devolver_links`, inclui `links`.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - type: object
                    properties:
                      links:
                        type: array
                        description: Só com `devolver_links` true.
                        items: { $ref: "#/components/schemas/LinkAssinatura" }
                      links_omitidos:
                        type: boolean
                        description: Repetição pela mesma Idempotency-Key num servidor sem chave de cifragem — os links não são guardados.
              examples: { exemplo: { $ref: "#/components/examples/EnvelopeEnviado" } }
        "400": { $ref: "#/components/responses/RequisicaoInvalida" }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/DocumentoNaoEncontrado" }
        "409": { $ref: "#/components/responses/EstadoInvalidoIdempotente" }
        "415": { $ref: "#/components/responses/TipoNaoSuportado" }
        "422": { $ref: "#/components/responses/RegraViolada" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /envelopes/{id}/cancelar:
    parameters:
      - $ref: "#/components/parameters/Id"
    post:
      tags: [Ações]
      operationId: cancelarEnvelope
      summary: Cancelar envelope
      description: Exige `motivo` (5 a 500 caracteres), que vai para a trilha e para `motivo_cancelamento`.
      x-escopo: envelopes:gerir
      x-idempotente: true
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CancelarCorpo" }
            example: { motivo: "POP substituído pela revisão 2; nova ciência será enviada." }
      responses:
        "200":
          description: Cancelado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Envelope" }
              examples: { exemplo: { $ref: "#/components/examples/EnvelopeCancelado" } }
        "400": { $ref: "#/components/responses/RequisicaoInvalida" }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/DocumentoNaoEncontrado" }
        "409": { $ref: "#/components/responses/EstadoInvalidoIdempotente" }
        "415": { $ref: "#/components/responses/TipoNaoSuportado" }
        "422": { $ref: "#/components/responses/Validacao" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /envelopes/{id}/pausar:
    parameters:
      - $ref: "#/components/parameters/Id"
    post:
      tags: [Ações]
      operationId: pausarEnvelope
      summary: Pausar envelope
      description: Suspende as assinaturas de um envelope em andamento. Sem corpo.
      x-escopo: envelopes:gerir
      x-idempotente: true
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: Pausado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Envelope" }
              examples: { exemplo: { $ref: "#/components/examples/EnvelopePausado" } }
        "400": { $ref: "#/components/responses/RequisicaoInvalida" }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/DocumentoNaoEncontrado" }
        "409": { $ref: "#/components/responses/EstadoInvalidoIdempotente" }
        "422": { $ref: "#/components/responses/IdempotenciaConflito" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /envelopes/{id}/retomar:
    parameters:
      - $ref: "#/components/parameters/Id"
    post:
      tags: [Ações]
      operationId: retomarEnvelope
      summary: Retomar envelope
      description: Volta um envelope pausado para `andamento`. Sem corpo.
      x-escopo: envelopes:gerir
      x-idempotente: true
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: Retomado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Envelope" }
              examples: { exemplo: { $ref: "#/components/examples/EnvelopeEnviado" } }
        "400": { $ref: "#/components/responses/RequisicaoInvalida" }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/DocumentoNaoEncontrado" }
        "409": { $ref: "#/components/responses/EstadoInvalidoIdempotente" }
        "422": { $ref: "#/components/responses/IdempotenciaConflito" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /envelopes/{id}/lembrete:
    parameters:
      - $ref: "#/components/parameters/Id"
    post:
      tags: [Ações]
      operationId: lembrete
      summary: Lembrar quem assina
      description: Gera link novo e reenvia o convite a quem está na vez, ou só à pessoa indicada em `signatario_id`. Só em `andamento`.
      x-escopo: envelopes:gerir
      x-idempotente: true
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LembreteCorpo" }
            example: { signatario_id: 4d9e6f2a-1b3c-4d5e-8f9a-0b1c2d3e4f5a }
      responses:
        "200":
          description: Quantos convites saíram.
          content:
            application/json:
              schema:
                type: object
                required: [enviados, nao_enviados]
                properties:
                  enviados: { type: integer, minimum: 0, description: Convites entregues ao canal agora. }
                  nao_enviados:
                    type: array
                    description: >-
                      Quem ficou sem convite agora e por quê (limite de links da pessoa, limite diário de e-mails
                      pelo remetente do CardSign, sem e-mail nem WhatsApp). Com `adiado`, o convite sai sozinho no
                      dia seguinte e o link anterior continua valendo.
                    items:
                      type: object
                      required: [signatario_id, motivo, adiado]
                      properties:
                        signatario_id: { type: string, format: uuid }
                        motivo: { type: string }
                        adiado: { type: boolean }
              example: { enviados: 1, nao_enviados: [] }
        "400": { $ref: "#/components/responses/RequisicaoInvalida" }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/SignatarioNaoEncontrado" }
        "409": { $ref: "#/components/responses/EstadoInvalidoIdempotente" }
        "415": { $ref: "#/components/responses/TipoNaoSuportado" }
        "422": { $ref: "#/components/responses/Validacao" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /envelopes/{id}/trilha:
    parameters:
      - $ref: "#/components/parameters/Id"
    get:
      tags: [Consultas]
      operationId: trilhaEnvelope
      summary: Trilha de auditoria do envelope
      description: Igual a `GET /documentos/{id}/trilha`.
      x-escopo: trilha:ler
      responses:
        "200":
          description: A trilha inteira, conferida.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Trilha" }
              examples: { exemplo: { $ref: "#/components/examples/Trilha" } }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/DocumentoNaoEncontrado" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/TrilhaIndisponivel" }
  /envelopes/{id}/pdf:
    parameters:
      - $ref: "#/components/parameters/Id"
    get:
      tags: [Consultas]
      operationId: pdfEnvelope
      summary: PDF final do envelope
      description: Igual a `GET /documentos/{id}/pdf`.
      x-escopo: documentos:baixar
      responses:
        "200":
          description: Links do PDF final selado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PdfFinal" }
              examples: { exemplo: { $ref: "#/components/examples/PdfFinal" } }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/DocumentoNaoEncontrado" }
        "409": { $ref: "#/components/responses/NaoConcluido" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /envelopes/{id}/provas:
    parameters:
      - $ref: "#/components/parameters/Id"
    get:
      tags: [Consultas]
      operationId: provasEnvelope
      summary: Pacote de provas
      description: ZIP com o PDF final, os originais, o carimbo do tempo, as assinaturas digitais e a trilha. Só para concluídos; link de 5 minutos.
      x-escopo: provas:baixar
      responses:
        "200":
          description: Link do pacote.
          content:
            application/json:
              schema:
                type: object
                required: [documento, arquivo]
                properties:
                  documento: { type: string, format: uuid }
                  arquivo: { $ref: "#/components/schemas/LinkArquivo" }
              example:
                documento: 7b1e4c2d-9f3a-4d6e-8a5b-1c2d3e4f5a6b
                arquivo:
                  nome: provas-7b1e4c2d-9f3a-4d6e-8a5b-1c2d3e4f5a6b.zip
                  sha256: c0ffee12ab34cd56ef7890ab12cd34ef567890ab12cd34ef567890ab12cd34ef
                  url: "https://wkkkfrcjotsbyfggcwro.supabase.co/storage/v1/object/sign/documentos/provas.zip?token=exemplo"
                  expira_em_segundos: 300
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/DocumentoNaoEncontrado" }
        "409": { $ref: "#/components/responses/NaoConcluido" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /documentos:
    get:
      tags: [Consultas]
      operationId: listarDocumentos
      summary: Listar documentos
      description: A lista da API de leitura (formato de antes, sem `external_id`). Sem rascunhos.
      x-escopo: documentos:ler
      parameters:
        - $ref: "#/components/parameters/Limite"
        - $ref: "#/components/parameters/Depois"
        - $ref: "#/components/parameters/Pagina"
        - $ref: "#/components/parameters/Contar"
        - $ref: "#/components/parameters/Status"
        - $ref: "#/components/parameters/Empresa"
        - $ref: "#/components/parameters/Desde"
        - $ref: "#/components/parameters/Ate"
      responses:
        "200":
          description: Uma página de documentos.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ListaDocumentos" }
              example:
                pagina: 1
                limite: 50
                total: 1
                proximo: null
                proxima_url: null
                documentos:
                  - id: 7b1e4c2d-9f3a-4d6e-8a5b-1c2d3e4f5a6b
                    titulo: "POP 014 — Higienização das mãos (ciência do colaborador)"
                    status: concluido
                    empresa_id: 3f6c1d2a-8b4e-4f1a-9c7d-2e5b8a1f4c90
                    criado_em: "2026-10-19T13:42:07.481233+00:00"
                    concluido_em: "2026-10-20T11:05:31.204118+00:00"
                    prazo: "2026-10-31T02:59:00+00:00"
        "400": { $ref: "#/components/responses/ParametroInvalido" }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /documentos/{id}:
    parameters:
      - $ref: "#/components/parameters/Id"
    get:
      tags: [Consultas]
      operationId: detalheDocumento
      summary: Consultar documento (formato de leitura)
      description: O detalhe da API de leitura, com a empresa, as pessoas e os PDFs finais. Para o objeto Envelope completo, use `GET /envelopes/{id}`.
      x-escopo: documentos:ler
      responses:
        "200":
          description: O documento.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DocumentoDetalhe" }
              example:
                id: 7b1e4c2d-9f3a-4d6e-8a5b-1c2d3e4f5a6b
                titulo: "POP 014 — Higienização das mãos (ciência do colaborador)"
                status: concluido
                empresa_id: 3f6c1d2a-8b4e-4f1a-9c7d-2e5b8a1f4c90
                criado_em: "2026-10-19T13:42:07.481233+00:00"
                concluido_em: "2026-10-20T11:05:31.204118+00:00"
                prazo: "2026-10-31T02:59:00+00:00"
                tipo_id: null
                valor_mensal: null
                empresa: { nome: Clínica Dias Saúde Ocupacional, cnpj: "12345678000190" }
                signatarios:
                  - { nome: Mariana Costa Ribeiro, funcao: assinar, status: assinou, nivel: avancada, ordem: 1, assinado_em: "2026-10-20T10:58:12.903311+00:00" }
                  - { nome: Rafael Almeida Pinto, funcao: testemunha, status: assinou, nivel: avancada, ordem: 2, assinado_em: "2026-10-20T11:05:29.118024+00:00" }
                pdfs_finais:
                  - { nome: POP-014-higienizacao-das-maos-assinado.pdf, sha256: 8e3b1f0c9d2a47b6e5f4c3d2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a09f8e, criado_em: "2026-10-20T11:05:40.552910+00:00" }
                verificar: "https://cardsign.gopixeltecnologiaa.com.br/verificar?h=8e3b1f0c9d2a47b6e5f4c3d2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a09f8e"
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/DocumentoNaoEncontrado" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /documentos/{id}/trilha:
    parameters:
      - $ref: "#/components/parameters/Id"
    get:
      tags: [Consultas]
      operationId: trilhaDocumento
      summary: Trilha de auditoria
      description: |
        A trilha inteira (lida em páginas e conferida com a cadeia de hashes): tipo, data, `hash` e `hash_anterior`
        de cada evento, e `integra` (true/false). Sem IP, aparelho, CPF ou destino de código.
      x-escopo: trilha:ler
      responses:
        "200":
          description: A trilha.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Trilha" }
              examples: { exemplo: { $ref: "#/components/examples/Trilha" } }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/DocumentoNaoEncontrado" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/TrilhaIndisponivel" }
  /documentos/{id}/pdf:
    parameters:
      - $ref: "#/components/parameters/Id"
    get:
      tags: [Consultas]
      operationId: pdfDocumento
      summary: PDF final selado
      description: Links de 5 minutos para o PDF final selado, com o SHA-256. Só depois que todos assinam (`409 nao_concluido`).
      x-escopo: documentos:baixar
      responses:
        "200":
          description: Links.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PdfFinal" }
              examples: { exemplo: { $ref: "#/components/examples/PdfFinal" } }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/DocumentoNaoEncontrado" }
        "409": { $ref: "#/components/responses/NaoConcluido" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /webhooks:
    get:
      tags: [Webhooks]
      operationId: listarWebhooks
      summary: Listar webhooks
      description: Os destinos do grupo da chave.
      x-escopo: webhooks:gerir
      responses:
        "200":
          description: Destinos.
          content:
            application/json:
              schema:
                type: object
                required: [webhooks]
                properties:
                  webhooks: { type: array, items: { $ref: "#/components/schemas/Webhook" } }
              example:
                webhooks:
                  - id: 8c3f0a6b-4d2e-4f7a-b9c1-6d5e4f3a2b1c
                    url: https://treinamento.gopixeltecnologia.com/api/webhooks/cardsign
                    eventos: [envelope.concluido, envelope.cancelado, signatario.recusou]
                    empresas: [3f6c1d2a-8b4e-4f1a-9c7d-2e5b8a1f4c90]
                    descricao: Programa DIAS — treinamentos
                    ativo: true
                    falhas_seguidas: 0
                    pausado_em: null
                    pausado_motivo: null
                    segredo_fim: x9Qk
                    criado_em: "2026-10-20T12:00:03.118204+00:00"
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
    post:
      tags: [Webhooks]
      operationId: criarWebhook
      summary: Criar webhook
      description: Cadastra um destino `https://` público. O `segredo` (`whsec_…`) volta **só nesta resposta**.
      x-escopo: webhooks:gerir
      x-idempotente: true
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookCriar" }
            example:
              url: https://treinamento.gopixeltecnologia.com/api/webhooks/cardsign
              eventos: [envelope.enviado, signatario.assinou, signatario.recusou, convite.falhou, envelope.concluido, envelope.cancelado, envelope.expirado]
              empresas: [3f6c1d2a-8b4e-4f1a-9c7d-2e5b8a1f4c90]
              descricao: Programa DIAS — treinamentos
      responses:
        "201":
          description: Criado (com o segredo).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookCriado" }
              example:
                id: 8c3f0a6b-4d2e-4f7a-b9c1-6d5e4f3a2b1c
                url: https://treinamento.gopixeltecnologia.com/api/webhooks/cardsign
                eventos: [envelope.enviado, signatario.assinou, signatario.recusou, convite.falhou, envelope.concluido, envelope.cancelado, envelope.expirado]
                empresas: [3f6c1d2a-8b4e-4f1a-9c7d-2e5b8a1f4c90]
                descricao: Programa DIAS — treinamentos
                ativo: true
                falhas_seguidas: 0
                pausado_em: null
                pausado_motivo: null
                segredo_fim: x9Qk
                criado_em: "2026-10-20T12:00:03.118204+00:00"
                segredo: whsec_EXEMPLO-nao-use-3kP9vT2mQ8wR5yL1zN7bX4cF6hJ0dA9sx9Qk
        "400": { $ref: "#/components/responses/RequisicaoInvalida" }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "409": { $ref: "#/components/responses/IdempotenciaEmAndamento" }
        "415": { $ref: "#/components/responses/TipoNaoSuportado" }
        "422": { $ref: "#/components/responses/ValidacaoWebhook" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /webhooks/{id}:
    parameters:
      - $ref: "#/components/parameters/Id"
    patch:
      tags: [Webhooks]
      operationId: alterarWebhook
      summary: Alterar webhook
      description: |
        Altere `url`, `eventos`, `empresas`, `descricao` ou `ativo` (reativar zera as falhas seguidas). Se `url` ou
        `empresas` mudarem, o dono da chave passa a ser o responsável pelo destino e o segredo é trocado: a resposta
        traz o `segredo` novo **só nessa vez** (o antigo para de valer na hora).
      x-escopo: webhooks:gerir
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEditar" }
            example: { ativo: true, eventos: [envelope.concluido, envelope.cancelado, signatario.recusou] }
      responses:
        "200":
          description: O destino atualizado (com `segredo` quando url ou empresas mudaram).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookAlterado" }
              examples: { exemplo: { $ref: "#/components/examples/Webhook" } }
        "400": { $ref: "#/components/responses/RequisicaoInvalida" }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/WebhookNaoEncontrado" }
        "415": { $ref: "#/components/responses/TipoNaoSuportado" }
        "422": { $ref: "#/components/responses/ValidacaoWebhook" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
    delete:
      tags: [Webhooks]
      operationId: removerWebhook
      summary: Remover webhook
      description: Remove o destino e as entregas pendentes dele.
      x-escopo: webhooks:gerir
      responses:
        "204": { description: Removido (sem corpo). }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/WebhookNaoEncontrado" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /webhooks/{id}/segredo:
    parameters:
      - $ref: "#/components/parameters/Id"
    post:
      tags: [Webhooks]
      operationId: trocarSegredoWebhook
      summary: Trocar o segredo do webhook
      description: |
        Gera um segredo novo (o antigo para de valer na hora) e o devolve **só nesta resposta**. O dono da chave passa
        a ser o responsável pelo destino. Sem corpo.
      x-escopo: webhooks:gerir
      x-idempotente: true
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: O destino, com o segredo novo.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookCriado" }
              example:
                id: 8c3f0a6b-4d2e-4f7a-b9c1-6d5e4f3a2b1c
                url: https://treinamento.gopixeltecnologia.com/api/webhooks/cardsign
                eventos: [envelope.concluido, envelope.cancelado, signatario.recusou]
                empresas: [3f6c1d2a-8b4e-4f1a-9c7d-2e5b8a1f4c90]
                descricao: Programa DIAS — treinamentos
                ativo: true
                falhas_seguidas: 0
                pausado_em: null
                pausado_motivo: null
                segredo_fim: m4Zt
                criado_em: "2026-10-20T12:00:03.118204+00:00"
                segredo: whsec_EXEMPLO-nao-use-9bW2qL7xT4nR1vK8sD5fG3hJ6pA0cZ2em4Zt
        "400": { $ref: "#/components/responses/RequisicaoInvalida" }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/WebhookNaoEncontrado" }
        "409": { $ref: "#/components/responses/IdempotenciaEmAndamento" }
        "422": { $ref: "#/components/responses/IdempotenciaConflito" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /webhooks/{id}/testar:
    parameters:
      - $ref: "#/components/parameters/Id"
    post:
      tags: [Webhooks]
      operationId: testarWebhook
      summary: Testar webhook
      description: Enfileira um evento `ping` para o destino (entregue em até 1 minuto). Sem corpo.
      x-escopo: webhooks:gerir
      x-idempotente: true
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "202":
          description: Na fila.
          content:
            application/json:
              schema:
                type: object
                required: [entrega_id, evento_id]
                properties:
                  entrega_id: { type: string, format: uuid }
                  evento_id: { type: string, format: uuid, description: Vai no cabeçalho CardSign-Event-Id. }
              example: { entrega_id: 1f2e3d4c-5b6a-4978-8a9b-0c1d2e3f4a5b, evento_id: 6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d }
        "400": { $ref: "#/components/responses/RequisicaoInvalida" }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/WebhookNaoEncontrado" }
        "409": { $ref: "#/components/responses/IdempotenciaEmAndamento" }
        "422": { $ref: "#/components/responses/IdempotenciaConflito" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /webhooks/{id}/entregas:
    parameters:
      - $ref: "#/components/parameters/Id"
    get:
      tags: [Webhooks]
      operationId: entregasWebhook
      summary: Entregas do webhook
      description: As últimas 100 entregas do destino, mais recentes primeiro.
      x-escopo: webhooks:gerir
      responses:
        "200":
          description: Entregas.
          content:
            application/json:
              schema:
                type: object
                required: [entregas]
                properties:
                  entregas: { type: array, items: { $ref: "#/components/schemas/Entrega" } }
              example:
                entregas:
                  - id: 1f2e3d4c-5b6a-4978-8a9b-0c1d2e3f4a5b
                    evento_id: 6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d
                    tipo: envelope.concluido
                    status: entregue
                    tentativas: 1
                    http_status: 200
                    ms: 184
                    erro: null
                    criado_em: "2026-10-20T11:05:40.912003+00:00"
                    proxima_em: null
                  - id: 2a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6d
                    evento_id: 7b6c5d4e-3f2a-4b1c-8d9e-0f1a2b3c4d5e
                    tipo: signatario.assinou
                    status: pendente
                    tentativas: 2
                    http_status: 503
                    ms: 10012
                    erro: HTTP 503
                    criado_em: "2026-10-20T10:58:13.004417+00:00"
                    proxima_em: "2026-10-20T11:34:13.004417+00:00"
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/WebhookNaoEncontrado" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
  /webhooks/{id}/entregas/{eid}/reenviar:
    parameters:
      - $ref: "#/components/parameters/Id"
      - $ref: "#/components/parameters/Eid"
    post:
      tags: [Webhooks]
      operationId: reenviarEntrega
      summary: Reenviar entrega
      description: Põe a entrega de novo na fila agora, com o mesmo `evento_id` (o receptor reconhece a repetição). Sem corpo.
      x-escopo: webhooks:gerir
      x-idempotente: true
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "202":
          description: Na fila.
          content:
            application/json:
              schema:
                type: object
                required: [reenviada]
                properties:
                  reenviada: { type: boolean, const: true }
              example: { reenviada: true }
        "400": { $ref: "#/components/responses/RequisicaoInvalida" }
        "401": { $ref: "#/components/responses/NaoAutenticado" }
        "403": { $ref: "#/components/responses/Proibido" }
        "404": { $ref: "#/components/responses/EntregaNaoEncontrada" }
        "409": { $ref: "#/components/responses/IdempotenciaEmAndamento" }
        "422": { $ref: "#/components/responses/IdempotenciaConflito" }
        "429": { $ref: "#/components/responses/Limite" }
        "503": { $ref: "#/components/responses/Indisponivel" }
webhooks:
  envelope.enviado:
    post:
      summary: Documento enviado
      description: O envelope saiu do rascunho e os convites foram disparados.
      x-tipo-webhook: envelope.enviado
      parameters: &parametrosWebhook
        - name: CardSign-Event-Id
          in: header
          required: true
          description: UUID do evento. Igual em todas as tentativas e reenvios — use para ignorar repetições.
          schema: { type: string, format: uuid }
          example: 6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d
        - name: CardSign-Event
          in: header
          required: true
          description: Tipo do evento.
          schema: { type: string }
          example: envelope.concluido
        - name: CardSign-Signature
          in: header
          required: true
          description: "`t=<unix>,v1=<hex HMAC-SHA256(segredo, t + \".\" + corpo_bruto)>`. A chave do HMAC é o segredo inteiro (`whsec_…`) em UTF-8."
          schema: { type: string }
          example: "t=1792494340,v1=5d2c8f0a9b7e6d5c4b3a29180f7e6d5c4b3a29180f7e6d5c4b3a29180f7e6d5c"
        - name: User-Agent
          in: header
          required: true
          description: Sempre `CardSign-Webhooks/1`.
          schema: { type: string, const: CardSign-Webhooks/1 }
      requestBody: { $ref: "#/components/requestBodies/EventoWebhook" }
      responses: &respostasWebhook
        "2XX": { description: "Recebido. Qualquer 2xx em até 10 s encerra a entrega." }
        default: { description: "Falha: novo envio em 1 min, 5 min, 30 min, 2 h, 12 h e 24 h; depois `falhou`. 50 falhas seguidas pausam o destino." }
  signatario.visualizou:
    post:
      summary: Pessoa abriu o link
      description: Uma pessoa abriu o link de assinatura. Traz `signatario`.
      x-tipo-webhook: signatario.visualizou
      parameters: *parametrosWebhook
      requestBody: { $ref: "#/components/requestBodies/EventoWebhook" }
      responses: *respostasWebhook
  signatario.assinou:
    post:
      summary: Pessoa assinou
      description: Uma pessoa assinou. Traz `signatario`.
      x-tipo-webhook: signatario.assinou
      parameters: *parametrosWebhook
      requestBody: { $ref: "#/components/requestBodies/EventoWebhook" }
      responses: *respostasWebhook
  signatario.aprovou:
    post:
      summary: Pessoa aprovou
      description: Uma pessoa com a função `aprovar` aprovou. Traz `signatario`.
      x-tipo-webhook: signatario.aprovou
      parameters: *parametrosWebhook
      requestBody: { $ref: "#/components/requestBodies/EventoWebhook" }
      responses: *respostasWebhook
  signatario.testemunhou:
    post:
      summary: Testemunha assinou
      description: Uma testemunha assinou. Traz `signatario`.
      x-tipo-webhook: signatario.testemunhou
      parameters: *parametrosWebhook
      requestBody: { $ref: "#/components/requestBodies/EventoWebhook" }
      responses: *respostasWebhook
  signatario.recusou:
    post:
      summary: Pessoa recusou
      description: Uma pessoa recusou; o motivo está em `GET /envelopes/{id}` (`signatarios[].motivo_recusa`). Traz `signatario`.
      x-tipo-webhook: signatario.recusou
      parameters: *parametrosWebhook
      requestBody: { $ref: "#/components/requestBodies/EventoWebhook" }
      responses: *respostasWebhook
  convite.falhou:
    post:
      summary: Convite não saiu
      description: O convite (e-mail, WhatsApp ou SMS) de uma pessoa falhou de vez. Traz `signatario`.
      x-tipo-webhook: convite.falhou
      parameters: *parametrosWebhook
      requestBody: { $ref: "#/components/requestBodies/EventoWebhook" }
      responses: *respostasWebhook
  envelope.concluido:
    post:
      summary: Documento concluído
      description: Todos assinaram. Só sai **depois** que o PDF final selado existe — pode baixar com `GET /envelopes/{id}/pdf`.
      x-tipo-webhook: envelope.concluido
      parameters: *parametrosWebhook
      requestBody: { $ref: "#/components/requestBodies/EventoWebhook" }
      responses: *respostasWebhook
  envelope.cancelado:
    post:
      summary: Documento cancelado
      description: O envelope foi cancelado (pelo painel ou pela API).
      x-tipo-webhook: envelope.cancelado
      parameters: *parametrosWebhook
      requestBody: { $ref: "#/components/requestBodies/EventoWebhook" }
      responses: *respostasWebhook
  envelope.expirado:
    post:
      summary: Prazo vencido
      description: O prazo passou sem todas as assinaturas.
      x-tipo-webhook: envelope.expirado
      parameters: *parametrosWebhook
      requestBody: { $ref: "#/components/requestBodies/EventoWebhook" }
      responses: *respostasWebhook
  envelope.pausado:
    post:
      summary: Documento pausado
      description: O envelope foi pausado.
      x-tipo-webhook: envelope.pausado
      parameters: *parametrosWebhook
      requestBody: { $ref: "#/components/requestBodies/EventoWebhook" }
      responses: *respostasWebhook
  envelope.retomado:
    post:
      summary: Documento retomado
      description: O envelope pausado voltou para `andamento`.
      x-tipo-webhook: envelope.retomado
      parameters: *parametrosWebhook
      requestBody: { $ref: "#/components/requestBodies/EventoWebhook" }
      responses: *respostasWebhook
  envelope.exportar:
    post:
      summary: Marcado para a sua integração
      description: |
        Alguém marcou no painel "Enviar a" a sua integração. Vai só para o destino da chave marcada. Busque
        `GET /envelopes/{id}`: com `status` `concluido`, baixe o PDF (`/pdf`) e as provas e guarde na pasta que você
        ligou a `pasta.id`; antes disso, espere o `envelope.concluido` (confira `exportar.marcado`).
      x-tipo-webhook: envelope.exportar
      parameters: *parametrosWebhook
      requestBody: { $ref: "#/components/requestBodies/EventoWebhook" }
      responses: *respostasWebhook
  ping:
    post:
      summary: Teste (ping)
      description: Enviado por `POST /webhooks/{id}/testar`. Não traz `envelope`.
      x-tipo-webhook: ping
      parameters: *parametrosWebhook
      requestBody: { $ref: "#/components/requestBodies/EventoWebhook" }
      responses: *respostasWebhook
components:
  securitySchemes:
    chaveApi:
      type: http
      scheme: bearer
      bearerFormat: "cs_<prefixo 8 [a-z0-9]>_<segredo 40 [A-Za-z0-9]>"
      description: Chave criada em Configurações › API. Mostrada uma vez; guarde num cofre de segredos.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        1 a 200 caracteres ASCII visíveis. A mesma chave com o mesmo corpo em 24 h devolve a mesma resposta (status
        original e cabeçalho `idempotent-replayed: true`); com outro corpo, `422 idempotencia_conflito`. Só o sucesso
        fica guardado: recusas (`4xx`) e falhas (`5xx`) liberam a chave — corrija e tente de novo com a mesma chave. No
        multipart, "o mesmo corpo" é o mesmo campo `dados` (JSON, a ordem das chaves não importa) e os mesmos arquivos
        (nome e conteúdo, na ordem); o boundary não conta. Enquanto o primeiro pedido ainda roda, `409
        idempotencia_em_andamento`; reserva sem resposta é liberada depois de 10 min.
        Sugestão: `<sistema>-<id do seu registro>-<ação>`.
      schema: { type: string, minLength: 1, maxLength: 200, pattern: "^[\\x21-\\x7e]+$" }
      example: dias-4821-criar
    Id:
      name: id
      in: path
      required: true
      description: UUID do envelope (ou do webhook, nas rotas /webhooks).
      schema: { type: string, format: uuid }
      example: 7b1e4c2d-9f3a-4d6e-8a5b-1c2d3e4f5a6b
    Sid:
      name: sid
      in: path
      required: true
      description: UUID do signatário.
      schema: { type: string, format: uuid }
      example: 5e0f7a3b-2c4d-4e6f-9a0b-1c2d3e4f5a6b
    Eid:
      name: eid
      in: path
      required: true
      description: UUID da entrega.
      schema: { type: string, format: uuid }
      example: 2a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6d
    Limite:
      name: limite
      in: query
      description: Itens por página, de 1 a 200.
      schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
    Depois:
      name: depois
      in: query
      description: Cursor — o valor de `proximo` da resposta anterior (`<criado_em>,<id>`). Ignora `pagina`.
      schema: { type: string }
      example: "2026-10-19T13:42:07.481233+00:00,7b1e4c2d-9f3a-4d6e-8a5b-1c2d3e4f5a6b"
    Pagina:
      name: pagina
      in: query
      description: Número da página (compatibilidade). Só até `pagina × limite ≤ 10000`; além disso use `depois`.
      schema: { type: integer, minimum: 1, default: 1 }
    Contar:
      name: contar
      in: query
      description: "`1` para trazer `total` também fora da primeira página."
      schema: { type: string, enum: ["1"] }
    Status:
      name: status
      in: query
      description: Filtra pela situação.
      schema: { $ref: "#/components/schemas/StatusEnvelope" }
    Empresa:
      name: empresa
      in: query
      description: UUID de uma empresa liberada.
      schema: { type: string, format: uuid }
    Desde:
      name: desde
      in: query
      description: Criados a partir de (AAAA-MM-DD = 00:00 de Brasília, ou ISO 8601).
      schema: { type: string }
      example: "2026-10-01"
    Ate:
      name: ate
      in: query
      description: Criados antes de (AAAA-MM-DD = 00:00 de Brasília, ou ISO 8601).
      schema: { type: string }
      example: "2026-11-01"
    ExternalId:
      name: external_id
      in: query
      description: O `external_id` exato informado na criação (1 a 200 caracteres).
      schema: { type: string, minLength: 1, maxLength: 200 }
      example: "dias:pop:4821"
  headers:
    RetryAfter:
      description: Segundos para tentar de novo.
      schema: { type: integer }
    IdempotentReplayed:
      description: "`true` quando a resposta é a repetição guardada de uma Idempotency-Key."
      schema: { type: string, enum: ["true"] }
    Allow:
      description: Métodos aceitos na rota.
      schema: { type: string }
    CardSignEventId:
      description: UUID do evento. Igual em todas as tentativas e reenvios — use para ignorar repetições.
      schema: { type: string, format: uuid }
    CardSignEvent:
      description: Tipo do evento (ex. `envelope.concluido`).
      schema: { type: string }
    CardSignSignature:
      description: "`t=<unix>,v1=<hex HMAC-SHA256(segredo, t + \".\" + corpo_bruto)>`. Recuse se |agora − t| > 300 s."
      schema: { type: string }
  requestBodies:
    EventoWebhook:
      required: true
      content:
        application/json:
          schema: { $ref: "#/components/schemas/EventoWebhook" }
          examples:
            envelope.concluido:
              summary: envelope.concluido
              value:
                id: 6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d
                tipo: envelope.concluido
                criado_em: "2026-10-20T11:05:31.204Z"
                grupo_id: 9a2e7c41-5d3b-4e8f-b1a6-7c0d2f9e8b13
                envelope: { id: 7b1e4c2d-9f3a-4d6e-8a5b-1c2d3e4f5a6b, external_id: "dias:pop:4821", status: concluido, empresa_id: 3f6c1d2a-8b4e-4f1a-9c7d-2e5b8a1f4c90 }
            signatario.assinou:
              summary: signatario.assinou
              value:
                id: 7b6c5d4e-3f2a-4b1c-8d9e-0f1a2b3c4d5e
                tipo: signatario.assinou
                criado_em: "2026-10-20T10:58:12.903Z"
                grupo_id: 9a2e7c41-5d3b-4e8f-b1a6-7c0d2f9e8b13
                envelope: { id: 7b1e4c2d-9f3a-4d6e-8a5b-1c2d3e4f5a6b, external_id: "dias:pop:4821", status: andamento, empresa_id: 3f6c1d2a-8b4e-4f1a-9c7d-2e5b8a1f4c90 }
                signatario: { id: 4d9e6f2a-1b3c-4d5e-8f9a-0b1c2d3e4f5a, nome: Mariana Costa Ribeiro, funcao: assinar, status: assinou }
            ping:
              summary: ping
              value:
                id: 0a9b8c7d-6e5f-4a3b-9c2d-1e0f9a8b7c6d
                tipo: ping
                criado_em: "2026-10-20T12:01:00.000Z"
                grupo_id: 9a2e7c41-5d3b-4e8f-b1a6-7c0d2f9e8b13
  responses:
    NaoAutenticado:
      description: "Chave ausente ou inválida: `sem_chave`, `chave_invalida`, `chave_revogada`, `chave_expirada`, `chave_sem_responsavel`."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: sem_chave, mensagem: "Envie a chave no cabeçalho Authorization: Bearer cs_…" } }
    Proibido:
      description: "Sem acesso: `sem_escopo`, `sem_permissao`, `ip_nao_autorizado`."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: sem_escopo, mensagem: "A chave não tem o escopo envelopes:criar." } }
    Limite:
      description: "`limite` — mais de 120 chamadas por minuto."
      headers:
        retry-after: { $ref: "#/components/headers/RetryAfter" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: limite, mensagem: "Limite de 120 chamadas por minuto." } }
    Indisponivel:
      description: "`indisponivel` — não deu para conferir o acesso agora, o servidor está ocupado lendo PDFs de outros pedidos ou o pedido passou de 90 s baixando/lendo PDFs (veja `retry-after`); `erro_interno` (500) em falha inesperada. Tente de novo (com a mesma Idempotency-Key)."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: indisponivel, mensagem: "Não foi possível conferir o acesso. Tente de novo." } }
    TrilhaIndisponivel:
      description: "`trilha_indisponivel` — não deu para ler a trilha inteira; `indisponivel` ao conferir o acesso."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: trilha_indisponivel, mensagem: "Não foi possível ler a trilha inteira. Tente de novo." } }
    ParametroInvalido:
      description: "Filtro ou paginação inválidos: `parametro_invalido`, `cursor_invalido`, `pagina_alta_demais`."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: cursor_invalido, mensagem: "Cursor inválido. Use o valor de \"proximo\" da resposta anterior (depois=<criado_em>,<id>)." } }
    RequisicaoInvalida:
      description: "`sem_idempotency_key` (POST sem o cabeçalho) ou `json_invalido` (corpo ilegível)."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: sem_idempotency_key, mensagem: "Envie o cabeçalho Idempotency-Key (1 a 200 caracteres) em todo POST de escrita." } }
    TipoNaoSuportado:
      description: "`tipo_nao_suportado` — use `application/json` ou `multipart/form-data`."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: tipo_nao_suportado, mensagem: "Use Content-Type application/json ou multipart/form-data." } }
    GrandeDemais:
      description: "`grande_demais` — corpo acima de 30 MB, PDF acima de 20 MB ou os PDFs do pedido somando mais de 30 MB (cada download só pode trazer o que ainda cabe)."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: grande_demais, mensagem: "POP-014.pdf: PDF acima de 20 MB.", detalhes: [{ campo: "arquivos[0]", mensagem: "POP-014.pdf: PDF acima de 20 MB." }] } }
    DocumentoNaoEncontrado:
      description: "`documento_nao_encontrado` — não existe, foi apagado ou é de uma empresa fora do alcance da chave."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: documento_nao_encontrado, mensagem: "Documento não encontrado neste grupo." } }
    SignatarioNaoEncontrado:
      description: "`signatario_nao_encontrado` (pessoa que não é deste envelope) ou `documento_nao_encontrado`."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: signatario_nao_encontrado, mensagem: "Pessoa não encontrada neste envelope." } }
    WebhookNaoEncontrado:
      description: "`webhook_nao_encontrado` — destino de outro grupo ou removido."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: webhook_nao_encontrado, mensagem: "Webhook não encontrado." } }
    EntregaNaoEncontrada:
      description: "`entrega_nao_encontrada` (entrega de outro destino) ou `webhook_nao_encontrado`."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: entrega_nao_encontrada, mensagem: "Entrega não encontrada." } }
    EstadoInvalido:
      description: "`estado_invalido` — a situação do envelope não permite (ex.: mudar o que já foi enviado)."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: estado_invalido, mensagem: "Só dá para mudar o documento enquanto ele é rascunho." } }
    EstadoInvalidoIdempotente:
      description: "`estado_invalido` (a situação do envelope não permite) ou `idempotencia_em_andamento` (a mesma Idempotency-Key ainda está sendo processada; veja `retry-after`)."
      headers:
        retry-after: { $ref: "#/components/headers/RetryAfter" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: estado_invalido, mensagem: "Este documento já foi enviado." } }
    IdempotenciaEmAndamento:
      description: "`idempotencia_em_andamento` — a mesma Idempotency-Key ainda está sendo processada."
      headers:
        retry-after: { $ref: "#/components/headers/RetryAfter" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: idempotencia_em_andamento, mensagem: "O pedido com esta Idempotency-Key ainda está sendo processado." } }
    ConflitoCriacao:
      description: "`external_id_em_uso` (o `envelope_id` existente vem em `detalhes`) ou `idempotencia_em_andamento`."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: external_id_em_uso, mensagem: "Este external_id já foi usado por esta chave.", detalhes: [{ campo: envelope_id, mensagem: 7b1e4c2d-9f3a-4d6e-8a5b-1c2d3e4f5a6b }] } }
    NaoConcluido:
      description: "`nao_concluido` — o PDF final e o pacote de provas só existem depois que todos assinam."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: nao_concluido, mensagem: "O PDF final só existe depois que todos assinam." } }
    IdempotenciaConflito:
      description: "`idempotencia_conflito` — Idempotency-Key repetida com outro corpo (ou outra rota) em 24 h."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: idempotencia_conflito, mensagem: "Esta Idempotency-Key já foi usada com outro corpo (ou outra rota) nas últimas 24 h." } }
    Validacao:
      description: "`validacao` (campos inválidos, veja `detalhes`) ou `idempotencia_conflito`."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: validacao, mensagem: "Corpo inválido.", detalhes: [{ campo: motivo, mensagem: "Too small: expected string to have >=5 characters" }] } }
    ValidacaoArquivo:
      description: "`validacao`, `arquivo_invalido`, `download_falhou`, `url_invalida`, `ancora_nao_encontrada`, `regra_violada` ou `idempotencia_conflito`."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          examples:
            validacao:
              summary: Campo inválido
              value: { erro: { codigo: validacao, mensagem: "Corpo inválido.", detalhes: [{ campo: "signatarios[0].email", mensagem: "Invalid email address" }] } }
            arquivo_invalido:
              summary: Não é PDF
              value: { erro: { codigo: arquivo_invalido, mensagem: "POP-014.pdf: não é um PDF.", detalhes: [{ campo: "arquivos[0]", mensagem: "POP-014.pdf: não é um PDF." }] } }
            ancora_nao_encontrada:
              summary: Âncora ausente
              value: { erro: { codigo: ancora_nao_encontrada, mensagem: "Âncora não encontrada no PDF.", detalhes: [{ campo: "signatarios[0].campos[0]", mensagem: "Texto \"{{assinatura:1}}\" não encontrado no PDF." }] } }
            download_falhou:
              summary: URL não baixou
              value: { erro: { codigo: download_falhou, mensagem: "POP-014.pdf: HTTP 404 ao baixar.", detalhes: [{ campo: "arquivos[0]", mensagem: "POP-014.pdf: HTTP 404 ao baixar." }] } }
    ValidacaoSignatario:
      description: "`validacao`, `ancora_nao_encontrada`, `regra_violada` ou `idempotencia_conflito`."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: validacao, mensagem: "Campo fora do documento.", detalhes: [{ campo: "campos[0]", mensagem: "O arquivo 1 tem 1 página(s)." }] } }
    RegraViolada:
      description: "`regra_violada` (conferência do painel não passou — a mensagem explica), `validacao` ou `idempotencia_conflito`."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: regra_violada, mensagem: "Informe o e-mail de Rafael Almeida Pinto. O convite e o código de confirmação seguem por e-mail." } }
    ValidacaoWebhook:
      description: "`validacao` (inclui o teto de 20 destinos por grupo), `url_invalida` (não é https pública) ou `idempotencia_conflito`."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Erro" }
          example: { erro: { codigo: url_invalida, mensagem: "O endereço aponta para uma rede interna." } }
  examples:
    EnvelopeRascunho:
      value:
        id: 7b1e4c2d-9f3a-4d6e-8a5b-1c2d3e4f5a6b
        titulo: "POP 014 — Higienização das mãos (ciência do colaborador)"
        status: rascunho
        empresa_id: 3f6c1d2a-8b4e-4f1a-9c7d-2e5b8a1f4c90
        external_id: "dias:pop:4821"
        external_ref: { origem: programa-dias, pop: "014", turma: "2026-10" }
        origem: api
        prazo: "2026-10-31T02:59:00+00:00"
        mensagem: "Olá! Leia o POP de higienização das mãos e assine a ciência até sexta."
        criado_em: "2026-10-19T13:42:07.481233+00:00"
        concluido_em: null
        motivo_cancelamento: null
        arquivos:
          - { id: 2c8d5e1f-3a4b-4c6d-9e7f-8a9b0c1d2e3f, nome: POP-014-higienizacao-das-maos.pdf, sha256: 3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b, paginas: 3, ordem: 1 }
        signatarios:
          - { id: 4d9e6f2a-1b3c-4d5e-8f9a-0b1c2d3e4f5a, nome: Mariana Costa Ribeiro, funcao: assinar, ordem: 1, canal: email, nivel: avancada, status: aguardando, email_mascarado: m***@clinicadias.com.br, telefone_mascarado: null, assinado_em: null, motivo_recusa: null }
          - { id: 5e0f7a3b-2c4d-4e6f-9a0b-1c2d3e4f5a6b, nome: Rafael Almeida Pinto, funcao: testemunha, ordem: 2, canal: whatsapp, nivel: avancada, status: aguardando, email_mascarado: r***@clinicadias.com.br, telefone_mascarado: "+55 31 9****-5432", assinado_em: null, motivo_recusa: null }
        pdfs_finais: []
    EnvelopeEnviado:
      value:
        id: 7b1e4c2d-9f3a-4d6e-8a5b-1c2d3e4f5a6b
        titulo: "POP 014 — Higienização das mãos (ciência do colaborador)"
        status: andamento
        empresa_id: 3f6c1d2a-8b4e-4f1a-9c7d-2e5b8a1f4c90
        external_id: "dias:pop:4821"
        external_ref: { origem: programa-dias, pop: "014", turma: "2026-10" }
        origem: api
        prazo: "2026-10-31T02:59:00+00:00"
        mensagem: "Olá! Leia o POP de higienização das mãos e assine a ciência até sexta."
        criado_em: "2026-10-19T13:42:07.481233+00:00"
        concluido_em: null
        motivo_cancelamento: null
        arquivos:
          - { id: 2c8d5e1f-3a4b-4c6d-9e7f-8a9b0c1d2e3f, nome: POP-014-higienizacao-das-maos.pdf, sha256: 3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b, paginas: 3, ordem: 1 }
        signatarios:
          - { id: 4d9e6f2a-1b3c-4d5e-8f9a-0b1c2d3e4f5a, nome: Mariana Costa Ribeiro, funcao: assinar, ordem: 1, canal: email, nivel: avancada, status: na_vez, email_mascarado: m***@clinicadias.com.br, telefone_mascarado: null, assinado_em: null, motivo_recusa: null }
          - { id: 5e0f7a3b-2c4d-4e6f-9a0b-1c2d3e4f5a6b, nome: Rafael Almeida Pinto, funcao: testemunha, ordem: 2, canal: whatsapp, nivel: avancada, status: aguardando, email_mascarado: r***@clinicadias.com.br, telefone_mascarado: "+55 31 9****-5432", assinado_em: null, motivo_recusa: null }
        pdfs_finais: []
        links:
          - { signatario_id: 4d9e6f2a-1b3c-4d5e-8f9a-0b1c2d3e4f5a, url: "https://cardsign.gopixeltecnologiaa.com.br/assinar/EXEMPLO-token-de-assinatura", expira_em: "2026-10-31T02:59:00+00:00" }
    EnvelopePausado:
      value:
        id: 7b1e4c2d-9f3a-4d6e-8a5b-1c2d3e4f5a6b
        titulo: "POP 014 — Higienização das mãos (ciência do colaborador)"
        status: pausado
        empresa_id: 3f6c1d2a-8b4e-4f1a-9c7d-2e5b8a1f4c90
        external_id: "dias:pop:4821"
        external_ref: { origem: programa-dias, pop: "014", turma: "2026-10" }
        origem: api
        prazo: "2026-10-31T02:59:00+00:00"
        mensagem: null
        criado_em: "2026-10-19T13:42:07.481233+00:00"
        concluido_em: null
        motivo_cancelamento: null
        arquivos:
          - { id: 2c8d5e1f-3a4b-4c6d-9e7f-8a9b0c1d2e3f, nome: POP-014-higienizacao-das-maos.pdf, sha256: 3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b, paginas: 3, ordem: 1 }
        signatarios:
          - { id: 4d9e6f2a-1b3c-4d5e-8f9a-0b1c2d3e4f5a, nome: Mariana Costa Ribeiro, funcao: assinar, ordem: 1, canal: email, nivel: avancada, status: na_vez, email_mascarado: m***@clinicadias.com.br, telefone_mascarado: null, assinado_em: null, motivo_recusa: null }
        pdfs_finais: []
    EnvelopeCancelado:
      value:
        id: 7b1e4c2d-9f3a-4d6e-8a5b-1c2d3e4f5a6b
        titulo: "POP 014 — Higienização das mãos (ciência do colaborador)"
        status: cancelado
        empresa_id: 3f6c1d2a-8b4e-4f1a-9c7d-2e5b8a1f4c90
        external_id: "dias:pop:4821"
        external_ref: { origem: programa-dias, pop: "014", turma: "2026-10" }
        origem: api
        prazo: "2026-10-31T02:59:00+00:00"
        mensagem: null
        criado_em: "2026-10-19T13:42:07.481233+00:00"
        concluido_em: null
        motivo_cancelamento: "POP substituído pela revisão 2; nova ciência será enviada."
        arquivos:
          - { id: 2c8d5e1f-3a4b-4c6d-9e7f-8a9b0c1d2e3f, nome: POP-014-higienizacao-das-maos.pdf, sha256: 3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b, paginas: 3, ordem: 1 }
        signatarios:
          - { id: 4d9e6f2a-1b3c-4d5e-8f9a-0b1c2d3e4f5a, nome: Mariana Costa Ribeiro, funcao: assinar, ordem: 1, canal: email, nivel: avancada, status: aguardando, email_mascarado: m***@clinicadias.com.br, telefone_mascarado: null, assinado_em: null, motivo_recusa: null }
        pdfs_finais: []
    EnvelopeConcluido:
      value:
        id: 7b1e4c2d-9f3a-4d6e-8a5b-1c2d3e4f5a6b
        titulo: "POP 014 — Higienização das mãos (ciência do colaborador)"
        status: concluido
        empresa_id: 3f6c1d2a-8b4e-4f1a-9c7d-2e5b8a1f4c90
        external_id: "dias:pop:4821"
        external_ref: { origem: programa-dias, pop: "014", turma: "2026-10" }
        origem: api
        prazo: "2026-10-31T02:59:00+00:00"
        mensagem: "Olá! Leia o POP de higienização das mãos e assine a ciência até sexta."
        criado_em: "2026-10-19T13:42:07.481233+00:00"
        concluido_em: "2026-10-20T11:05:31.204118+00:00"
        motivo_cancelamento: null
        arquivos:
          - { id: 2c8d5e1f-3a4b-4c6d-9e7f-8a9b0c1d2e3f, nome: POP-014-higienizacao-das-maos.pdf, sha256: 3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b, paginas: 3, ordem: 1 }
        signatarios:
          - { id: 4d9e6f2a-1b3c-4d5e-8f9a-0b1c2d3e4f5a, nome: Mariana Costa Ribeiro, funcao: assinar, ordem: 1, canal: email, nivel: avancada, status: assinou, email_mascarado: m***@clinicadias.com.br, telefone_mascarado: null, assinado_em: "2026-10-20T10:58:12.903311+00:00", motivo_recusa: null }
          - { id: 5e0f7a3b-2c4d-4e6f-9a0b-1c2d3e4f5a6b, nome: Rafael Almeida Pinto, funcao: testemunha, ordem: 2, canal: whatsapp, nivel: avancada, status: assinou, email_mascarado: r***@clinicadias.com.br, telefone_mascarado: "+55 31 9****-5432", assinado_em: "2026-10-20T11:05:29.118024+00:00", motivo_recusa: null }
        pdfs_finais:
          - { nome: POP-014-higienizacao-das-maos-assinado.pdf, sha256: 8e3b1f0c9d2a47b6e5f4c3d2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a09f8e, criado_em: "2026-10-20T11:05:40.552910+00:00", verificar: "https://cardsign.gopixeltecnologiaa.com.br/verificar?h=8e3b1f0c9d2a47b6e5f4c3d2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a09f8e" }
    Trilha:
      value:
        documento: 7b1e4c2d-9f3a-4d6e-8a5b-1c2d3e4f5a6b
        integra: true
        total: 3
        eventos:
          - { id: 18231, tipo: enviado, criado_em: "2026-10-19T13:42:31.002914+00:00", hash_anterior: null, hash: a1f0c3d2e5b4a7968f0e1d2c3b4a59687f6e5d4c3b2a19087f6e5d4c3b2a1908, dados: {} }
          - { id: 18240, tipo: assinou, criado_em: "2026-10-20T10:58:12.903311+00:00", hash_anterior: a1f0c3d2e5b4a7968f0e1d2c3b4a59687f6e5d4c3b2a19087f6e5d4c3b2a1908, hash: b2e1d4c3f6a5b8079e1f2d3c4b5a6978e7f6d5c4b3a2918e7f6d5c4b3a291807, dados: { nome: Mariana Costa Ribeiro, funcao: assinar, nivel: avancada, metodo: email } }
          - { id: 18247, tipo: concluido, criado_em: "2026-10-20T11:05:31.204118+00:00", hash_anterior: b2e1d4c3f6a5b8079e1f2d3c4b5a6978e7f6d5c4b3a2918e7f6d5c4b3a291807, hash: c3d2e5f4a7b6c9180f2a3e4d5c6b7a89f8e7d6c5b4a3920f8e7d6c5b4a392018, dados: { sha256: 8e3b1f0c9d2a47b6e5f4c3d2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a09f8e } }
    PdfFinal:
      value:
        documento: 7b1e4c2d-9f3a-4d6e-8a5b-1c2d3e4f5a6b
        arquivos:
          - nome: POP-014-higienizacao-das-maos-assinado.pdf
            sha256: 8e3b1f0c9d2a47b6e5f4c3d2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a09f8e
            url: "https://wkkkfrcjotsbyfggcwro.supabase.co/storage/v1/object/sign/documentos/final.pdf?token=exemplo"
            expira_em_segundos: 300
    Webhook:
      value:
        id: 8c3f0a6b-4d2e-4f7a-b9c1-6d5e4f3a2b1c
        url: https://treinamento.gopixeltecnologia.com/api/webhooks/cardsign
        eventos: [envelope.concluido, envelope.cancelado, signatario.recusou]
        empresas: [3f6c1d2a-8b4e-4f1a-9c7d-2e5b8a1f4c90]
        descricao: Programa DIAS — treinamentos
        ativo: true
        falhas_seguidas: 0
        pausado_em: null
        pausado_motivo: null
        segredo_fim: x9Qk
        criado_em: "2026-10-20T12:00:03.118204+00:00"
  schemas:
    Erro:
      type: object
      description: Formato único de erro.
      required: [erro]
      properties:
        erro:
          type: object
          required: [codigo, mensagem]
          properties:
            codigo:
              type: string
              description: Código estável (veja a seção Erros).
              enum: [sem_idempotency_key, json_invalido, parametro_invalido, cursor_invalido, pagina_alta_demais, sem_chave, chave_invalida, chave_revogada, chave_expirada, chave_sem_responsavel, ip_nao_autorizado, sem_escopo, sem_permissao, nao_encontrado, documento_nao_encontrado, signatario_nao_encontrado, webhook_nao_encontrado, entrega_nao_encontrada, metodo, nao_concluido, estado_invalido, external_id_em_uso, idempotencia_em_andamento, grande_demais, tipo_nao_suportado, validacao, idempotencia_conflito, arquivo_invalido, download_falhou, url_invalida, ancora_nao_encontrada, regra_violada, limite, erro_interno, indisponivel, trilha_indisponivel]
            mensagem: { type: string, description: Explicação em português, para pessoas. }
            detalhes:
              type: array
              description: Só quando ajuda (ex. qual campo falhou).
              items: { $ref: "#/components/schemas/Detalhe" }
            pedido_id:
              type: string
              description: "Id deste pedido (o mesmo do cabeçalho `x-request-id`). Informe ao suporte para achar o erro no log."
              examples: ["5f0c2a9e8b7d4c1a9e3f2b1c0d9e8f7a"]
    Detalhe:
      type: object
      required: [campo, mensagem]
      properties:
        campo: { type: string, description: "Caminho do campo, ex. `signatarios[0].email`.", examples: ["signatarios[0].email"] }
        mensagem: { type: string }
    StatusEnvelope:
      type: string
      enum: [rascunho, andamento, pausado, concluido, cancelado, expirado, recusado]
    Indice:
      type: object
      properties:
        api: { type: string }
        versao: { type: string }
        documentacao: { type: string, format: uri }
        rotas: { type: array, items: { type: string } }
        paginacao: { type: object, additionalProperties: { type: string } }
        escrita: { type: string }
        cache: { type: string }
    Empresa:
      type: object
      required: [id, nome]
      properties:
        id: { type: string, format: uuid }
        nome: { type: string }
        cnpj: { type: [string, "null"], description: Só dígitos. }
    Eu:
      type: object
      required: [chave, grupo, empresas, limite_por_minuto]
      properties:
        chave:
          type: object
          properties:
            id: { type: string, format: uuid }
            prefixo: { type: string, description: "Os 8 caracteres depois de `cs_`." }
            nome: { type: string }
            expira_em: { type: [string, "null"], format: date-time }
            escopos: { type: array, items: { type: string } }
        grupo:
          type: object
          properties:
            id: { type: string, format: uuid }
            nome: { type: string }
        empresas: { type: array, items: { $ref: "#/components/schemas/Empresa" } }
        limite_por_minuto: { type: integer, const: 120 }
    Modelo:
      type: object
      properties:
        id: { type: string, format: uuid }
        nome: { type: string }
        tipo_id: { type: [string, "null"], format: uuid }
        versao: { type: integer }
        empresa_id: { type: string, format: uuid }
        status: { type: string }
    Arquivo:
      type: object
      description: Um PDF do envelope.
      required: [id, nome, sha256, paginas, ordem]
      properties:
        id: { type: string, format: uuid }
        nome: { type: string }
        sha256: { type: string, description: SHA-256 (hex) do PDF original., pattern: "^[0-9a-f]{64}$" }
        paginas: { type: integer, minimum: 1 }
        ordem: { type: integer, minimum: 1 }
    Signatario:
      type: object
      description: Pessoa do envelope (resposta). Contatos voltam mascarados.
      required: [id, nome, funcao, ordem, canal, nivel, status]
      properties:
        id: { type: string, format: uuid }
        nome: { type: string }
        funcao: { type: string, enum: [assinar, aprovar, testemunha, observador] }
        ordem: { type: integer, description: Pessoas com a mesma ordem assinam em paralelo. }
        canal: { type: string, enum: [whatsapp, sms, email, link] }
        nivel: { type: string, enum: [simples, avancada, qualificada] }
        status: { type: string, enum: [aguardando, na_vez, assinou, recusou, expirou] }
        email_mascarado: { type: [string, "null"], examples: ["m***@clinicadias.com.br"] }
        telefone_mascarado: { type: [string, "null"], examples: ["+55 31 9****-5432"] }
        assinado_em: { type: [string, "null"], format: date-time }
        motivo_recusa: { type: [string, "null"] }
    PdfFinalItem:
      type: object
      required: [nome, sha256, criado_em, verificar]
      properties:
        nome: { type: string }
        sha256: { type: string }
        criado_em: { type: string, format: date-time }
        verificar: { type: string, format: uri, description: Página pública de verificação. }
    Envelope:
      type: object
      description: O documento a assinar, completo.
      required: [id, titulo, status, empresa_id, external_ref, origem, criado_em, arquivos, signatarios, pdfs_finais]
      properties:
        id: { type: string, format: uuid }
        titulo: { type: string }
        status: { $ref: "#/components/schemas/StatusEnvelope" }
        empresa_id: { type: string, format: uuid }
        external_id: { type: [string, "null"], description: Seu identificador (único por chave). }
        external_ref: { type: object, description: Dados livres seus (até 4 KB). }
        origem: { type: string, enum: [painel, api] }
        revisao_pendente:
          type: boolean
          description: Rascunho esperando uma pessoa revisar e enviar no painel (chave com revisão ligada).
        url_painel: { type: string, format: uri, description: Onde o documento é revisado e enviado no CardSign. }
        prazo: { type: [string, "null"], format: date-time }
        mensagem: { type: [string, "null"] }
        assinatura_como: { type: string, enum: [cpf, cnpj, misto], description: Como as pessoas assinam. }
        criado_em: { type: string, format: date-time }
        concluido_em: { type: [string, "null"], format: date-time }
        motivo_cancelamento: { type: [string, "null"] }
        pasta:
          oneOf:
            - type: "null"
            - type: object
              properties:
                id: { type: string, format: uuid }
                codigo: { type: [string, "null"] }
                nome: { type: string }
                pai_id: { type: [string, "null"], format: uuid }
          description: Pasta do documento no CardSign (ligue à sua com `GET /pastas`).
        exportar:
          type: object
          description: Se alguém marcou no painel "Enviar a" esta integração (a chave que pergunta).
          properties:
            marcado: { type: boolean }
            marcado_em: { type: [string, "null"], format: date-time }
        arquivos: { type: array, items: { $ref: "#/components/schemas/Arquivo" } }
        signatarios: { type: array, items: { $ref: "#/components/schemas/Signatario" } }
        pdfs_finais: { type: array, items: { $ref: "#/components/schemas/PdfFinalItem" } }
    Pasta:
      type: object
      required: [id, empresa_id, nome, caminho]
      properties:
        id: { type: string, format: uuid }
        empresa_id: { type: string, format: uuid }
        pai_id: { type: [string, "null"], format: uuid }
        codigo: { type: [string, "null"] }
        nome: { type: string }
        caminho: { type: string, description: "Do topo até ela: \"Pai › Filha\"." }
    LinkAssinatura:
      type: object
      required: [signatario_id, url]
      properties:
        signatario_id: { type: string, format: uuid }
        url: { type: string, format: uri, description: Link de assinatura de quem está na vez. Trate como segredo. }
        expira_em: { type: [string, "null"], format: date-time }
    ResumoEnvelope:
      type: object
      properties:
        id: { type: string, format: uuid }
        titulo: { type: string }
        status: { $ref: "#/components/schemas/StatusEnvelope" }
        empresa_id: { type: string, format: uuid }
        criado_em: { type: string, format: date-time }
        concluido_em: { type: [string, "null"], format: date-time }
        prazo: { type: [string, "null"], format: date-time }
        external_id: { type: [string, "null"] }
        origem: { type: string, enum: [painel, api] }
    ResumoDocumento:
      type: object
      properties:
        id: { type: string, format: uuid }
        titulo: { type: string }
        status: { $ref: "#/components/schemas/StatusEnvelope" }
        empresa_id: { type: string, format: uuid }
        criado_em: { type: string, format: date-time }
        concluido_em: { type: [string, "null"], format: date-time }
        prazo: { type: [string, "null"], format: date-time }
    Pagina:
      type: object
      required: [limite, proximo, proxima_url]
      properties:
        pagina: { type: [integer, "null"], description: Número da página, ou null quando a chamada usou cursor. }
        limite: { type: integer }
        total: { type: [integer, "null"], description: Só na primeira página sem cursor (ou com contar=1). }
        proximo: { type: [string, "null"], description: "Cursor da próxima página (`depois`); null = acabou." }
        proxima_url: { type: [string, "null"], format: uri }
    ListaEnvelopes:
      allOf:
        - $ref: "#/components/schemas/Pagina"
        - type: object
          required: [envelopes]
          properties:
            envelopes: { type: array, items: { $ref: "#/components/schemas/ResumoEnvelope" } }
    ListaDocumentos:
      allOf:
        - $ref: "#/components/schemas/Pagina"
        - type: object
          required: [documentos]
          properties:
            documentos: { type: array, items: { $ref: "#/components/schemas/ResumoDocumento" } }
    DocumentoDetalhe:
      type: object
      properties:
        id: { type: string, format: uuid }
        titulo: { type: string }
        status: { $ref: "#/components/schemas/StatusEnvelope" }
        empresa_id: { type: string, format: uuid }
        criado_em: { type: string, format: date-time }
        concluido_em: { type: [string, "null"], format: date-time }
        prazo: { type: [string, "null"], format: date-time }
        tipo_id: { type: [string, "null"], format: uuid }
        valor_mensal: { type: [number, "null"] }
        empresa: { type: [object, "null"], properties: { nome: { type: string }, cnpj: { type: [string, "null"] } } }
        signatarios:
          type: array
          items:
            type: object
            properties:
              nome: { type: string }
              funcao: { type: string }
              status: { type: string }
              nivel: { type: string }
              ordem: { type: integer }
              assinado_em: { type: [string, "null"], format: date-time }
        pdfs_finais:
          type: array
          items:
            type: object
            properties:
              nome: { type: string }
              sha256: { type: string }
              criado_em: { type: string, format: date-time }
        verificar: { type: [string, "null"], format: uri }
    EventoTrilha:
      type: object
      properties:
        id: { type: integer }
        tipo: { type: string, description: "Evento interno (enviado, visualizou, assinou, recusou, concluido…)." }
        criado_em: { type: string, format: date-time }
        hash_anterior: { type: [string, "null"] }
        hash: { type: string }
        dados: { type: object, description: "Só nome, funcao, nivel, metodo, termo, em_nome_de, delegado_nome, sha256 e tsa." }
    Trilha:
      type: object
      required: [documento, integra, total, eventos]
      properties:
        documento: { type: string, format: uuid }
        integra: { type: [boolean, "null"], description: A cadeia de hashes confere. }
        total: { type: integer }
        eventos: { type: array, items: { $ref: "#/components/schemas/EventoTrilha" } }
    LinkArquivo:
      type: object
      required: [nome, sha256, url, expira_em_segundos]
      properties:
        nome: { type: string }
        sha256: { type: string }
        url: { type: [string, "null"], format: uri, description: Link assinado de download. }
        expira_em_segundos: { type: integer, const: 300 }
    PdfFinal:
      type: object
      required: [documento, arquivos]
      properties:
        documento: { type: string, format: uuid }
        arquivos: { type: array, items: { $ref: "#/components/schemas/LinkArquivo" } }
        aviso: { type: string, description: Quando o documento não tem PDF anexado. }
    ArquivoEntrada:
      description: Um PDF por URL https pública ou em base64 (até 20 MB).
      oneOf:
        - type: object
          title: Por URL
          required: [nome, url]
          properties:
            nome: { type: string, minLength: 1, maxLength: 200, description: Nome do arquivo (".pdf" é acrescentado se faltar). }
            url: { type: string, format: uri, maxLength: 2000, description: "https pública; sem redirecionamento." }
        - type: object
          title: Em base64
          required: [nome, base64]
          properties:
            nome: { type: string, minLength: 1, maxLength: 200 }
            base64: { type: string, minLength: 8, maxLength: 28000000, description: Conteúdo do PDF em base64. }
    CampoPosicao:
      type: object
      title: Por posição
      required: [tipo, pagina, x, y, largura, altura]
      description: Coordenadas em % da página, `y` a partir do topo. `x + largura` e `y + altura` ≤ 100.
      properties:
        tipo: { type: string, enum: [assinatura, rubrica, data, nome, cpf] }
        arquivo: { type: integer, minimum: 1, maximum: 20, default: 1, description: Índice do arquivo no envelope (1 = primeiro). }
        pagina: { type: integer, minimum: 1, maximum: 5000 }
        x: { type: number, minimum: 0, maximum: 100 }
        y: { type: number, minimum: 0, maximum: 100 }
        largura: { type: number, exclusiveMinimum: 0, maximum: 100 }
        altura: { type: number, exclusiveMinimum: 0, maximum: 100 }
    CampoAncora:
      type: object
      title: Por âncora
      required: [tipo, ancora]
      description: Texto procurado nos PDFs; cada ocorrência vira um campo.
      properties:
        tipo: { type: string, enum: [assinatura, rubrica, data, nome, cpf] }
        ancora: { type: string, minLength: 2, maxLength: 100, examples: ["{{assinatura:1}}"] }
        largura: { type: number, exclusiveMinimum: 0, maximum: 100, description: "Padrão do tipo (assinatura 26 × 7)." }
        altura: { type: number, exclusiveMinimum: 0, maximum: 100 }
        deslocamento_x: { type: number, minimum: -100, maximum: 100, description: "% da página a partir do início do texto." }
        deslocamento_y: { type: number, minimum: -100, maximum: 100 }
    SignatarioEntrada:
      type: object
      description: Pessoa a incluir no envelope.
      required: [nome]
      properties:
        nome: { type: string, minLength: 2, maxLength: 200 }
        funcao: { type: string, enum: [assinar, aprovar, testemunha, observador], default: assinar }
        ordem: { type: integer, minimum: 1, maximum: 50, description: "Padrão: a posição na lista. Mesma ordem = em paralelo." }
        canal: { type: string, enum: [whatsapp, sms, email, link], default: email, description: "Por onde vai o convite. Com `link`, integre a assinatura no seu sistema usando `devolver_links`." }
        email: { type: [string, "null"], format: email, maxLength: 200, description: "Obrigatório para enviar (exceto observador): o código de confirmação segue por e-mail." }
        telefone: { type: [string, "null"], maxLength: 30, description: "Com `+DDI` ou só DDD + número (DDI 55)." }
        cpf: { type: [string, "null"], maxLength: 20, description: Com ou sem pontuação; 11 dígitos. }
        nivel: { type: string, enum: [simples, avancada, qualificada], default: avancada }
        campos:
          type: array
          maxItems: 50
          description: Sem campos, quem assina ganha uma caixa automática na última página.
          items:
            oneOf:
              - $ref: "#/components/schemas/CampoPosicao"
              - $ref: "#/components/schemas/CampoAncora"
    EnvelopeCriar:
      type: object
      required: [empresa_id, titulo]
      properties:
        empresa_id: { type: string, format: uuid, description: Empresa liberada para envelopes:criar. }
        titulo: { type: string, minLength: 3, maxLength: 200 }
        external_id: { type: [string, "null"], minLength: 1, maxLength: 200, description: Seu identificador; repetido para a mesma chave → 409. }
        external_ref: { type: [object, "null"], description: Dados livres (até 4 KB), devolvidos no envelope. }
        prazo: { type: [string, "null"], maxLength: 40, description: "AAAA-MM-DD (23:59 de Brasília) ou ISO 8601; no futuro." }
        mensagem: { type: [string, "null"], maxLength: 2000, description: Vai no convite. }
        assinatura_como: { type: string, enum: [cpf, cnpj, misto], default: cpf, description: "cpf (padrão): todo signatário precisa de CPF. cnpj: cada um assina em nome de uma empresa (CPF e CNPJ). misto: sem exigência fixa; o nível qualificado continua exigindo CPF e certificado." }
        arquivos: { type: array, maxItems: 20, items: { $ref: "#/components/schemas/ArquivoEntrada" } }
        signatarios: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/SignatarioEntrada" } }
    EnvelopeEditar:
      type: object
      minProperties: 1
      properties:
        titulo: { type: string, minLength: 3, maxLength: 200 }
        prazo: { type: [string, "null"], maxLength: 40, description: "AAAA-MM-DD ou ISO 8601; null tira o prazo." }
        mensagem: { type: [string, "null"], maxLength: 2000 }
        assinatura_como: { type: string, enum: [cpf, cnpj, misto] }
    EnviarCorpo:
      type: object
      properties:
        devolver_links: { type: boolean, description: "Devolve os links de assinatura (exige também envelopes:gerir)." }
    CancelarCorpo:
      type: object
      required: [motivo]
      properties:
        motivo: { type: string, minLength: 5, maxLength: 500 }
    LembreteCorpo:
      type: object
      properties:
        signatario_id: { type: string, format: uuid, description: Só esta pessoa (sem ele, quem está na vez). }
    TipoWebhook:
      type: string
      enum: [envelope.enviado, signatario.visualizou, signatario.assinou, signatario.aprovou, signatario.testemunhou, signatario.recusou, convite.falhou, envelope.concluido, envelope.cancelado, envelope.expirado, envelope.pausado, envelope.retomado, envelope.exportar]
    Webhook:
      type: object
      required: [id, url, eventos, empresas, ativo]
      properties:
        id: { type: string, format: uuid }
        url: { type: string, format: uri }
        eventos: { type: array, items: { $ref: "#/components/schemas/TipoWebhook" } }
        empresas: { type: array, items: { type: string, format: uuid }, description: Vazio = todas as empresas em que quem criou tem docs.ver. }
        descricao: { type: [string, "null"] }
        ativo: { type: boolean }
        falhas_seguidas: { type: integer, description: 50 seguidas pausam o destino. }
        pausado_em: { type: [string, "null"], format: date-time }
        pausado_motivo: { type: [string, "null"] }
        segredo_fim: { type: string, description: Os 4 últimos caracteres do segredo. }
        criado_em: { type: string, format: date-time }
    WebhookCriado:
      allOf:
        - $ref: "#/components/schemas/Webhook"
        - type: object
          required: [segredo]
          properties:
            segredo: { type: string, description: "`whsec_…` — só nesta resposta. Guarde num cofre." }
    WebhookAlterado:
      allOf:
        - $ref: "#/components/schemas/Webhook"
        - type: object
          properties:
            segredo: { type: string, description: "`whsec_…` novo — só quando url ou empresas mudaram, e só nesta resposta." }
    WebhookCriar:
      type: object
      required: [url, eventos]
      properties:
        url: { type: string, format: uri, maxLength: 500, description: "https:// pública (IP conferido na conexão)." }
        eventos: { type: array, minItems: 1, maxItems: 12, items: { $ref: "#/components/schemas/TipoWebhook" } }
        empresas: { type: array, maxItems: 200, items: { type: string, format: uuid } }
        descricao: { type: [string, "null"], maxLength: 120 }
    WebhookEditar:
      type: object
      minProperties: 1
      properties:
        url: { type: string, format: uri, maxLength: 500 }
        eventos: { type: array, minItems: 1, maxItems: 12, items: { $ref: "#/components/schemas/TipoWebhook" } }
        empresas: { type: array, maxItems: 200, items: { type: string, format: uuid } }
        descricao: { type: [string, "null"], maxLength: 120 }
        ativo: { type: boolean }
    Entrega:
      type: object
      required: [id, evento_id, tipo, status, tentativas]
      properties:
        id: { type: string, format: uuid }
        evento_id: { type: string, format: uuid }
        tipo: { type: string }
        status: { type: string, enum: [pendente, entregue, falhou] }
        tentativas: { type: integer }
        http_status: { type: [integer, "null"] }
        ms: { type: [integer, "null"] }
        erro: { type: [string, "null"] }
        criado_em: { type: string, format: date-time }
        proxima_em: { type: [string, "null"], format: date-time, description: Só nas pendentes. }
    EventoWebhook:
      type: object
      description: Corpo de cada POST que o CardSign faz ao seu sistema.
      required: [id, tipo, criado_em, grupo_id]
      properties:
        id: { type: string, format: uuid, description: Igual ao cabeçalho CardSign-Event-Id. }
        tipo: { type: string, description: "Um de TipoWebhook, ou `ping`." }
        criado_em: { type: string, format: date-time }
        grupo_id: { type: string, format: uuid }
        envelope:
          type: object
          description: Ausente no ping.
          properties:
            id: { type: string, format: uuid }
            external_id: { type: [string, "null"] }
            status: { $ref: "#/components/schemas/StatusEnvelope" }
            empresa_id: { type: string, format: uuid }
        signatario:
          type: object
          description: Só nos eventos signatario.* e convite.falhou.
          properties:
            id: { type: string, format: uuid }
            nome: { type: string }
            funcao: { type: string }
            status: { type: string }
